summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDJ O'Leary <dijitol@proton.me>2026-07-08 01:06:20 +0200
committerDJ O'Leary <dijitol@proton.me>2026-07-08 01:06:20 +0200
commit4d088c9354c17c58478cf121349a314b4e0dd322 (patch)
tree4ad0635107bd4ec741ab82fd738662bc32ae073b
parent970b9f872254612b673c3a09579ed902cade3c99 (diff)
set up and add sync package
-rw-r--r--.editorconfig36
-rw-r--r--.envrc.template3
-rw-r--r--.gitattributes21
-rwxr-xr-x.githooks/commit-msg15
-rwxr-xr-x.githooks/pre-commit42
-rwxr-xr-x.githooks/pre-push18
-rw-r--r--.gitignore2
-rw-r--r--.golangci.yaml8
-rw-r--r--CODEOWNERS1
-rw-r--r--CONTRIBUTING.md14
-rw-r--r--README.md1
-rw-r--r--Taskfile.go.yaml30
-rw-r--r--Taskfile.yaml17
-rw-r--r--flake.nix47
-rw-r--r--sync/README.md10
-rw-r--r--sync/chan.go48
-rw-r--r--sync/chan_fan.go79
-rw-r--r--sync/chan_pipeline.go48
18 files changed, 439 insertions, 1 deletions
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..d2f1e21
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,36 @@
+# EditorConfig coding styles definitions. For more information about the
+# properties used in this file, please see the EditorConfig documentation:
+# http://editorconfig.org/
+
+root = true
+
+[*]
+charset = utf-8
+end_of_line = LF
+insert_final_newline = true
+trim_trailing_whitespace = true
+indent_style = space
+
+[*.{md}]
+indent_style = tab
+trim_trailing_whitespace = true
+
+[*.{yml,yaml}]
+indent_style = space
+indent_size = 2
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.{json}]
+trim_trailing_whitespace = true
+indent_style = space
+indent_size = 4
+
+[*.{nix}]
+indent_style = space
+indent_size = 2
+insert_final_newline = false
+trim_trailing_whitespace = true
+
+[{*.go,go.mod,go.sum}]
+indent_style = tab
diff --git a/.envrc.template b/.envrc.template
new file mode 100644
index 0000000..cf8df4b
--- /dev/null
+++ b/.envrc.template
@@ -0,0 +1,3 @@
+dotenv
+
+use flake
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..74fa6b4
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,21 @@
+# Automatically normalize line endings for all text-based files
+# http://git-scm.com/docs/gitattributes#_end_of_line_conversion
+* text=auto
+
+assets/* linguist-vendored
+internal/infrastructure/sqlc/* linguist-generated
+
+# For the following file types, normalize line endings to LF on checking and
+# prevent conversion to CRLF when they are checked out (this is required in
+# order to prevent newline related issues)
+.* text eol=lf
+*.md text eol=lf
+*.go text eol=lf
+*.yml text eol=lf
+*.yaml text eol=lf
+*.html text eol=lf
+*.txt text eol=lf
+*.css text eol=lf
+*.js text eol=lf
+*.json text eol=lf
+LICENSE text eol=lf
diff --git a/.githooks/commit-msg b/.githooks/commit-msg
new file mode 100755
index 0000000..33d6a5f
--- /dev/null
+++ b/.githooks/commit-msg
@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+# Commit message hook - enforce conventional commit format (optional)
+
+commit_regex='^(feat|fix|docs|style|refactor|test|chore|build|ci|perf)(\(.+\))?!?: .{1,50}|^fixup!'
+
+if ! grep -qE "$commit_regex" "$1"; then
+ echo "โŒ Invalid commit message format!"
+ echo ""
+ echo "Format: type(scope): description"
+ echo "Types: feat, fix, docs, style, refactor, test, chore, build, ci, perf"
+ echo "Example: feat(auth): add user login functionality"
+ echo ""
+ echo "Your message: $(cat $1)"
+ exit 1
+fi
diff --git a/.githooks/pre-commit b/.githooks/pre-commit
new file mode 100755
index 0000000..95ecd68
--- /dev/null
+++ b/.githooks/pre-commit
@@ -0,0 +1,42 @@
+#!/usr/bin/env bash
+# Pre-commit hook - fast checks only
+# dependencies: php git bash grep xargs composer
+
+set -e
+
+echo "๐Ÿ” Running pre-commit checks..."
+
+# Check if we have staged files
+if git diff --cached --quiet; then
+ echo "No staged changes found."
+ exit 0
+fi
+
+# Lint only staged files
+echo "๐Ÿ“ Linting staged files..."
+
+# Get staged Go files
+STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.go$' || true)
+
+if [ -n "$STAGED_FILES" ]; then
+ # Run Go linting on staged files
+ go vet ./...
+ if [ $? -eq 0 ]; then
+ echo "โœ… go vet passed"
+ else
+ echo "โŒ go vet failed"
+ exit 1
+ fi
+
+ golangci-lint run ./...
+ if [ $? -eq 0 ]; then
+ echo "โœ… golangci-lint passed"
+ else
+ echo "โŒ golangci-lint failed"
+ exit 1
+ fi
+else
+ echo "โญ๏ธ Nothing to lint, skipping..."
+fi
+
+echo "๐ŸŽ‰ Pre-commit checks completed successfully!"
diff --git a/.githooks/pre-push b/.githooks/pre-push
new file mode 100755
index 0000000..daf4be4
--- /dev/null
+++ b/.githooks/pre-push
@@ -0,0 +1,18 @@
+#!/usr/bin/env bash
+# Pre-push hook - run tests before pushing
+
+set -e
+
+echo "๐Ÿงช Running tests before push..."
+
+# Run the full test suite
+go test ./...
+
+if [ $? -eq 0 ]; then
+ echo "โœ… All tests passed! Push allowed."
+ exit 0
+else
+ echo "โŒ Tests failed! Push blocked."
+ echo "Fix the failing tests before pushing."
+ exit 1
+fi
diff --git a/.gitignore b/.gitignore
index 921d1fe..e2f727c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -27,8 +27,8 @@ go.work.sum
# env file
.env
+.envrc
# Editor/IDE
# .idea/
# .vscode/
-
diff --git a/.golangci.yaml b/.golangci.yaml
new file mode 100644
index 0000000..100f715
--- /dev/null
+++ b/.golangci.yaml
@@ -0,0 +1,8 @@
+version: "2"
+linters:
+ default: standard
+ enable:
+ - testifylint
+formatters:
+ enable:
+ - gofumpt
diff --git a/CODEOWNERS b/CODEOWNERS
new file mode 100644
index 0000000..ecd6e62
--- /dev/null
+++ b/CODEOWNERS
@@ -0,0 +1 @@
+* @dijitol
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..6ddb5e3
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,14 @@
+# Contributing
+
+## Commit Messages
+
+We use conventional commits $^{1}$ for all commit messages.
+
+## Code Conventions
+
+We follow uber's style guide $^{2}$.
+
+## References
+
+- [1](https://www.conventionalcommits.org/en/v1.0.0/)
+- [2](https://github.com/uber-go/guide/blob/master/style.md)
diff --git a/README.md b/README.md
index dc6a8f7..9dc615b 100644
--- a/README.md
+++ b/README.md
@@ -5,3 +5,4 @@ My go standard library
## Resources
- https://go.dev/doc/modules/layout#multiple-packages
+- https://pkg.go.dev/std
diff --git a/Taskfile.go.yaml b/Taskfile.go.yaml
new file mode 100644
index 0000000..495aa59
--- /dev/null
+++ b/Taskfile.go.yaml
@@ -0,0 +1,30 @@
+# yaml-language-server: $schema=https://taskfile.dev/schema.json
+version: "3"
+
+tasks:
+ test:
+ desc: run go tests
+ cmd: go test -v ./...
+
+ test:coverage:
+ desc: run go tests
+ cmds:
+ - cmd: go test -v -coverprofile=tmp/cover.out ./...
+ - cmd: go tool cover -html=tmp/cover.out -o=tmp/cover.html
+ - cmd: xdg-open tmp/cover.html
+
+ docs:
+ desc: open autogenerated code documentation
+ cmd: go tool pkgsite -open
+
+ lint:
+ desc: run various linters
+ cmds:
+ - cmd: golangci-lint version
+ - cmd: golangci-lint run
+
+ lint:fix:
+ desc: run various linters
+ cmds:
+ - cmd: golangci-lint version
+ - cmd: golangci-lint run --fix
diff --git a/Taskfile.yaml b/Taskfile.yaml
new file mode 100644
index 0000000..9c2f746
--- /dev/null
+++ b/Taskfile.yaml
@@ -0,0 +1,17 @@
+# yaml-language-server: $schema=https://taskfile.dev/schema.json
+version: "3"
+
+dotenv:
+ - .env
+
+tasks:
+ default:
+ desc: lists all tasks
+ cmd: task --list-all
+
+ git:hooks:
+ desc: sets up the githooks for the project
+ cmd: git config core.hooksPath .githooks
+
+includes:
+ go: ./Taskfile.go.yaml
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 0000000..f8c6811
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,47 @@
+{
+ inputs = {
+ nixpkgs-unstable.url = "github:nixos/nixpkgs/nixos-unstable";
+ nixpkgs-stable.url = "github:nixos/nixpkgs/nixos-26.05";
+ flake-utils.url = "github:numtide/flake-utils";
+ };
+
+ outputs =
+ {
+ self,
+ nixpkgs-unstable,
+ nixpkgs-stable,
+ flake-utils,
+ ...
+ }@_inputs:
+ flake-utils.lib.eachDefaultSystem (
+ system:
+ let
+ upkgs = import nixpkgs-unstable {
+ inherit system;
+ };
+
+ _spkgs = import nixpkgs-stable {
+ inherit system;
+ };
+
+ pkgs = upkgs;
+ in
+ {
+ devShells.default = pkgs.mkShell {
+ buildInputs =
+ let
+ lang = with pkgs; [
+ go
+ gopls
+ delve
+ golangci-lint
+ ];
+ tools = with pkgs; [
+ go-task
+ ];
+ in
+ lang ++ tools;
+ };
+ }
+ );
+}
diff --git a/sync/README.md b/sync/README.md
new file mode 100644
index 0000000..93a4526
--- /dev/null
+++ b/sync/README.md
@@ -0,0 +1,10 @@
+# Sync
+
+## Resources
+
+- https://gobyexample.com/channels
+- https://go.dev/tour/concurrency/2
+- https://www.oreilly.com/library/view/concurrency-in-go/9781491941294/
+- https://youtu.be/gz4DKBKe58A?si=Ks5VhvUMNaa2NQ60
+- https://go.dev/doc/effective_go#channels
+- https://dave.cheney.net/2014/03/19/channel-axioms
diff --git a/sync/chan.go b/sync/chan.go
new file mode 100644
index 0000000..91501f9
--- /dev/null
+++ b/sync/chan.go
@@ -0,0 +1,48 @@
+package sync
+
+import "context"
+
+func OrDone[T any](ctx context.Context, in <-chan T) <-chan T {
+ if in == nil {
+ // we only expect this to happen during development
+ panic("in-channel is nil")
+ }
+
+ out := make(chan T)
+ go func() {
+ defer close(out)
+ for {
+ v, ok := CancelOrReceive(ctx, in)
+ if !ok {
+ return
+ }
+
+ if !CancelOrSend(ctx, out, v) {
+ return
+ }
+ }
+ }()
+ return out
+}
+
+// CancelOrReceive is trying to solve the same problem as OrDone but does so
+// without an extra goroutine.
+func CancelOrReceive[T any](ctx context.Context, in <-chan T) (v T, ok bool) {
+ select {
+ case <-ctx.Done():
+ return // <zero value of type T>, false
+ case v, ok = <-in:
+ return // v, ok
+ }
+}
+
+// CancelOrSend is a helper function to simplify sending values to channels
+// while keeping the context in mind.
+func CancelOrSend[T any](ctx context.Context, out chan<- T, val T) bool {
+ select {
+ case <-ctx.Done():
+ return false
+ case out <- val:
+ return true
+ }
+}
diff --git a/sync/chan_fan.go b/sync/chan_fan.go
new file mode 100644
index 0000000..13957ed
--- /dev/null
+++ b/sync/chan_fan.go
@@ -0,0 +1,79 @@
+package sync
+
+import (
+ "context"
+ "sync"
+)
+
+func FanOut[T any](ctx context.Context, in <-chan T, channelCount int, buffer int) []<-chan T {
+ if in == nil {
+ // we only expect this to happen during development
+ panic("in-channel is nil")
+ }
+ if channelCount < 1 {
+ channelCount = 1
+ }
+ if buffer < 0 {
+ buffer = 0
+ }
+
+ chs := make([]chan T, channelCount)
+ for i := range channelCount {
+ chs[i] = make(chan T, buffer)
+ }
+
+ go func() {
+ defer func() {
+ for _, out := range chs {
+ close(out)
+ }
+ }()
+
+ for v := range OrDone(ctx, in) {
+ for _, out := range chs {
+ // N.b. this will block if *any* of the out channels has a full buffer
+ if !CancelOrSend(ctx, out, v) {
+ return
+ }
+ }
+ }
+ }()
+
+ outs := make([]<-chan T, channelCount)
+ for i, out := range chs {
+ outs[i] = out
+ }
+
+ return outs
+}
+
+func FanIn[T any](ctx context.Context, buffer int, ins ...<-chan T) <-chan T {
+ if buffer < 0 {
+ buffer = 0
+ }
+
+ wg := sync.WaitGroup{}
+
+ out := make(chan T, buffer)
+
+ for _, in := range ins {
+ wg.Go(func() {
+ if in == nil {
+ return
+ }
+
+ for v := range OrDone(ctx, in) {
+ if !CancelOrSend(ctx, out, v) {
+ return
+ }
+ }
+ })
+ }
+
+ go func() {
+ wg.Wait()
+ close(out)
+ }()
+
+ return out
+}
diff --git a/sync/chan_pipeline.go b/sync/chan_pipeline.go
new file mode 100644
index 0000000..6b5f37f
--- /dev/null
+++ b/sync/chan_pipeline.go
@@ -0,0 +1,48 @@
+package sync
+
+import (
+ "context"
+ "sync"
+)
+
+func Pipe[IN, OUT any](
+ ctx context.Context,
+ in <-chan IN,
+ transform func(context.Context, IN) OUT,
+ workerCount int,
+ buffer int,
+) <-chan OUT {
+ if in == nil {
+ // we only expect this to happen during development
+ panic("in-channel is nil")
+ }
+ if workerCount < 1 {
+ workerCount = 1
+ }
+ if buffer < 0 {
+ buffer = 0
+ }
+
+ wg := sync.WaitGroup{}
+
+ out := make(chan OUT, buffer)
+
+ for range workerCount {
+ wg.Go(func() {
+ for v := range OrDone(ctx, in) {
+ w := transform(ctx, v)
+
+ if !CancelOrSend(ctx, out, w) {
+ return
+ }
+ }
+ })
+ }
+
+ go func() {
+ wg.Wait()
+ close(out)
+ }()
+
+ return out
+}