Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 31 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ jobs:
tidy-check
security
release-check
llms-full
llms-full-check
clean
help
)
Expand Down Expand Up @@ -152,6 +154,30 @@ jobs:

exit "$failed"

llms-full-up-to-date:
name: llms-full.txt is up to date
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.2
- uses: actions/setup-go@v6.4.0
with:
go-version: "1.26"
cache: true
- name: Regenerate and diff
run: make llms-full-check

markdownlint:
name: Markdown lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.2
- uses: DavidAnson/markdownlint-cli2-action@v21.0.0
with:
globs: |
README.md
SECURITY.md
CHANGELOG.md

bdd-strict-mode-guard:
name: BDD strict mode guard
runs-on: ubuntu-latest
Expand Down Expand Up @@ -250,8 +276,11 @@ jobs:
if grep -IiEn "$PATTERN" "$f" >/dev/null 2>&1; then
# Strip benign path references: llms.txt / llms-full.txt product
# filenames, the CLAUDE.md filename token, and .claude/ path
# segments in config files.
match=$(grep -IiEn "$PATTERN" "$f" | grep -viE 'llms(-full)?\.txt|CLAUDE\.md|\.claude/')
# segments in config files. The trailing `|| true` keeps the
# pipeline exit code at 0 when the filter removes every matched
# line (otherwise pipefail + set -e abort the script before the
# `::error::` line has a chance to print the accumulated hits).
match=$(grep -IiEn "$PATTERN" "$f" | { grep -viE 'llms(-full)?\.txt|CLAUDE\.md|\.claude/' || true; })
if [ -n "$match" ]; then
hits="${hits}${f}:\n${match}\n\n"
fi
Expand Down
50 changes: 50 additions & 0 deletions .markdownlint-cli2.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# markdownlint-cli2 configuration for the docs tracked by this repo.
# Rules are tuned for this project's prose style — see
# https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md.

globs:
- "README.md"
- "CONTRIBUTING.md"
- "SECURITY.md"
- "CHANGELOG.md"
- "CLA.md"
- "CODE_OF_CONDUCT.md"
- "CONTRIBUTORS.md"

config:
default: true

# MD013 — long lines are the norm for prose and for inline code/table cells.
MD013: false

# MD033 — README uses <div align="center"> for the hero block and
# <details> for collapsible sections.
MD033: false

# MD041 — the README opens with the centred hero block (HTML + image),
# not an H1. The H1 appears inside the hero.
MD041: false

# MD024 — sibling headings may repeat between unrelated sections.
MD024:
siblings_only: true

# MD036 — emphasis-as-heading is used for the bold tagline in the hero.
MD036: false

# MD034 — bare URLs are acceptable in the generator output bundle;
# prose links use markdown syntax.
MD034: false

# MD010 — Go code blocks in the README use tabs (the language's
# canonical indentation), so exclude fenced code blocks from the
# hard-tab check.
MD010:
code_blocks: false

# MD060 — table pipe padding style is not enforced.
MD060: false

# MD023 — H1/H2 inside the hero are intentionally indented so GitHub
# renders them centred inside the <div align="center"> block.
MD023: false
58 changes: 58 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Changelog

All notable changes to `github.com/axonops/syncmap` are documented in this file.

The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

No unreleased changes.

## Upgrading

From `v1.0.0` onwards `syncmap` follows the standard Go semantic-versioning
compatibility promise: breaking changes to the public API only in a new
major version. Minor and patch releases are always backwards-compatible
for the API surface documented on [pkg.go.dev](https://pkg.go.dev/github.com/axonops/syncmap).
Pin a specific tag in your `go.mod`, review the release notes for the
target version, and run your test suite with `-race` against the new
version before rolling to production.

## [1.0.0] — 2026-04-21

Initial AxonOps release. Forked from [`github.com/rgooding/go-syncmap`](https://github.com/rgooding/go-syncmap) by Robert Gooding and rebuilt to AxonOps library standards — new module path, expanded API, comprehensive tests, CI/CD, and documentation.

### Added

- **Core API** mirroring [`sync.Map`](https://pkg.go.dev/sync#Map): `Load`, `Store`, `LoadOrStore`, `LoadAndDelete`, `Delete`, `Range` — each generic over `K comparable, V any`, returning the typed zero value of `V` on miss so callers never deal with untyped `any`.
- **Extension methods** beyond `sync.Map`: `Len`, `Map` (snapshot as a plain Go map), `Keys`, `Values` — each documented as `O(n)` and a point-in-time approximation under concurrent mutation.
- **`Swap` method** wrapping `sync.Map.Swap` (Go 1.20) with the same typed-zero-on-miss guard as `LoadAndDelete`.
- **`Clear` method** wrapping `sync.Map.Clear` (Go 1.23).
- **`CompareAndSwap` and `CompareAndDelete`** as package-level generic functions with a tighter `[K, V comparable]` constraint, so non-comparable value types (slice, map, func) are rejected at compile time rather than panicking at runtime inside `sync.Map`. The `SyncMap[K, V any]` type signature is unchanged.
- **Package documentation** (`doc.go`) covering relationship to `sync.Map`, when to use `SyncMap` vs `sync.Map` vs `map + sync.RWMutex`, thread safety, zero-value usability, and a runnable Quick Start.
- **Unit tests** — external black-box package (`syncmap_test`) using `testify` and [`go.uber.org/goleak`](https://pkg.go.dev/go.uber.org/goleak). Every test runs under `-race` with `t.Parallel()`. Line coverage is **100%** of the library package.
- **Runnable godoc examples** covering every public symbol, each ending with a deterministic `// Output:` block.
- **Benchmarks** for every public method, plus a concurrent 90/10 read-write pattern and overhead pairs comparing the generic wrapper against raw `sync.Map`. The committed `bench.txt` baseline is the reference the CI `benchstat-regression-guard` job diffs against.
- **BDD suite** — [`godog`](https://pkg.go.dev/github.com/cucumber/godog) feature files under `tests/bdd/` exercising every public symbol plus a concurrent Store scenario. Runs under strict mode enforced by a CI guard.
- **Fuzz targets** — `FuzzLoadStore` (round-trip invariant) and `FuzzConcurrent` (4 goroutines over random op sequences, race-clean).
- **CI** (`.github/workflows/ci.yml`): format check, vet, golangci-lint, unit + BDD tests, 95% coverage threshold, module tidy, govulncheck, cross-platform builds (`linux/amd64`, `darwin/arm64`, `windows/amd64`), benchstat regression guard, BDD strict-mode guard, Apache-header guard, no-local-paths guard, no-AI-attribution guard, Makefile-targets guard, markdown lint, and `llms-full.txt` drift guard.
- **Release workflow** (`.github/workflows/release.yml`): `workflow_dispatch` only; verifies, tags, publishes via GoReleaser, warms the Go module proxy. Local tag creation is forbidden.
- **Dependabot** configuration with weekly updates and auto-merge for patch-level test dependencies.
- **LLM documentation bundle**: `llms.txt` (concise summary) and `llms-full.txt` (concatenated corpus) for AI-assistant ingestion, with a CI guard that fails the build on drift.
- `LICENSE` (Apache 2.0, preserved from upstream), `NOTICE` (crediting Robert Gooding as the upstream author), `SECURITY.md`.

### Changed

- Module path: `github.com/rgooding/go-syncmap` → `github.com/axonops/syncmap`.
- Minimum Go toolchain raised to **1.26**.

### Breaking

- Renamed the `Items()` method on `SyncMap` to `Values()` to match Go stdlib convention (`maps.Values`, Go 1.23). No deprecation shim — the rename lands pre-v1.0 under the new module path.

### Attribution

This release is a fork of [`github.com/rgooding/go-syncmap`](https://github.com/rgooding/go-syncmap) by Robert Gooding, which is distributed under Apache 2.0; this fork continues under the same licence. The original upstream copyright is preserved in git history and credited in `NOTICE`.

[Unreleased]: https://github.com/axonops/syncmap/compare/v1.0.0...HEAD
[1.0.0]: https://github.com/axonops/syncmap/releases/tag/v1.0.0
16 changes: 16 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,22 @@ security: ## Run govulncheck
release-check: ## Validate GoReleaser config without releasing
$(GORELEASER) check

.PHONY: llms-full
llms-full: ## Regenerate llms-full.txt from its canonical sources
@./scripts/gen-llms-full.sh

.PHONY: llms-full-check
llms-full-check: ## Fail if llms-full.txt is out of date
@cp llms-full.txt llms-full.txt.bak
@trap 'mv -f llms-full.txt.bak llms-full.txt 2>/dev/null || true' EXIT; \
./scripts/gen-llms-full.sh >/dev/null; \
if ! diff -q llms-full.txt llms-full.txt.bak >/dev/null; then \
echo "llms-full.txt drift — run 'make llms-full' and commit the result"; \
exit 1; \
fi; \
rm -f llms-full.txt.bak; \
trap - EXIT

.PHONY: clean
clean: ## Remove generated test and coverage artefacts
$(GO) clean -testcache
Expand Down
23 changes: 23 additions & 0 deletions NOTICE
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
syncmap
Copyright 2026 AxonOps Limited.

This product includes software developed at
AxonOps Limited (https://axonops.com/).

This product is a fork of github.com/rgooding/go-syncmap by
Robert Gooding, which is distributed under the Apache License,
Version 2.0. This fork continues under the same licence. The
original upstream copyright notice is preserved in this
repository's git history.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
180 changes: 178 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,178 @@
# go-syncmap
Wrapper around sync.Map using generics to give a cleaner interface
<div align="center">

# syncmap

**Type-safe generic wrapper around Go's `sync.Map` — zero dependencies, zero assertion cost at the call site.**

[![CI](https://github.com/axonops/syncmap/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/axonops/syncmap/actions/workflows/ci.yml)
[![Go Reference](https://pkg.go.dev/badge/github.com/axonops/syncmap.svg)](https://pkg.go.dev/github.com/axonops/syncmap)
[![Go Report Card](https://goreportcard.com/badge/github.com/axonops/syncmap)](https://goreportcard.com/report/github.com/axonops/syncmap)
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](./LICENSE)
![Status](https://img.shields.io/badge/status-pre--release-orange)

[🚀 Quick Start](#-quick-start) | [📖 API](#-api-reference) | [🧵 Thread Safety](#-thread-safety) | [⚡ Performance](#-performance) | [🤖 For AI assistants](#-for-ai-assistants)

</div>

---

**Table of contents**

- [✅ Status](#-status)
- [🔍 Overview](#-overview)
- [✨ Why `syncmap`?](#-why-syncmap)
- [🚀 Quick Start](#-quick-start)
- [📖 API Reference](#-api-reference)
- [🧵 Thread Safety](#-thread-safety)
- [⚡ Performance](#-performance)
- [🧭 When to use what](#-when-to-use-what)
- [🤖 For AI assistants](#-for-ai-assistants)
- [🤝 Contributing](#-contributing)
- [🔐 Security](#-security)
- [📜 Attribution](#-attribution)
- [📄 Licence](#-licence)

---

## ✅ Status

`syncmap` is **stable** from `v1.0.0` onwards and follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html): breaking changes to the public API only in a new major version. Pin a specific tag in your `go.mod` and review the [CHANGELOG](./CHANGELOG.md) on every upgrade.

## 🔍 Overview

`github.com/axonops/syncmap` is a thin, typed layer over Go's standard [`sync.Map`](https://pkg.go.dev/sync#Map). The standard `sync.Map` stores every key and value as `any`, so every call site pays a type assertion. `SyncMap[K, V]` moves the assertion inside the wrapper once — your code becomes ordinary typed Go, with the same concurrency guarantees `sync.Map` already provides and no additional allocations.

```go
var m syncmap.SyncMap[string, int]
m.Store("hits", 1)
v, ok := m.Load("hits")
// v is an int, not interface{}. No `.(int)` at the call site.
```

## ✨ Why `syncmap`?

- **Compile-time type safety.** `SyncMap[K comparable, V any]` is fully generic. `Load`, `Store`, `Range`, and friends accept and return your types directly.
- **Zero runtime dependencies.** Stdlib only. No goroutines spawned by the library. No transitive dependency CVEs to track.
- **Stdlib-parity semantics.** Every method maps one-to-one to a `sync.Map` method. `Load` on a missing key returns the typed zero value of `V` and `ok=false`, matching the stdlib contract.
- **The full modern API.** `Swap` (Go 1.20), `Clear` (Go 1.23), plus `CompareAndSwap` / `CompareAndDelete` as package-level generic functions that reject non-comparable `V` at **compile time** instead of the stdlib's runtime panic.
- **Convenience helpers** beyond `sync.Map`: `Len`, `Map`, `Keys`, `Values`. Each is documented as `O(n)` and a point-in-time approximation — no surprises.
- **Race-clean under scrutiny.** The suite runs with `-race`, `goleak.VerifyTestMain`, 100 % line coverage, 37 BDD scenarios, 2 fuzz targets, and a benchstat regression gate in CI.

## 🚀 Quick Start

```go
package main

import (
"fmt"

"github.com/axonops/syncmap"
)

func main() {
var m syncmap.SyncMap[string, int]

m.Store("hits", 1)
m.Store("misses", 0)

if v, ok := m.Load("hits"); ok {
fmt.Println("hits:", v) // hits: 1
}

// CompareAndSwap is a package-level function — V must be comparable.
if syncmap.CompareAndSwap(&m, "hits", 1, 2) {
fmt.Println("incremented")
}
}
```

Install:

```bash
go get github.com/axonops/syncmap@latest
```

Requires Go 1.26 or later.

## 📖 API Reference

Complete godoc at [pkg.go.dev/github.com/axonops/syncmap](https://pkg.go.dev/github.com/axonops/syncmap). The public surface in full:

| Symbol | Signature | Notes |
|---|---|---|
| `SyncMap[K, V]` | `type SyncMap[K comparable, V any] struct{…}` | Zero value is ready to use; do not copy after first use. |
| `Load` | `(key K) (value V, ok bool)` | Returns typed zero V on miss. |
| `Store` | `(key K, value V)` | Sets value for key. |
| `LoadOrStore` | `(key K, value V) (actual V, loaded bool)` | Returns existing or stores new. |
| `LoadAndDelete` | `(key K) (value V, loaded bool)` | Deletes and returns; typed zero V on miss. |
| `Delete` | `(key K)` | No-op if key absent. |
| `Swap` | `(key K, value V) (previous V, loaded bool)` | Go 1.20 semantics; typed zero V on miss. |
| `Clear` | `()` | Go 1.23 semantics; removes every entry. |
| `Range` | `(f func(K, V) bool)` | Not a consistent snapshot — see godoc. |
| `Len` | `() int` | **O(n)** — point-in-time approximation. |
| `Map` | `() map[K]V` | **O(n)** — snapshot copy; caller owns the result. |
| `Keys` | `() []K` | **O(n)** — order undefined. |
| `Values` | `() []V` | **O(n)** — order undefined; not correlated with `Keys`. |
| `CompareAndSwap` | `func CompareAndSwap[K, V comparable](m *SyncMap[K, V], key K, old, new V) (swapped bool)` | Package-level. `V` must be comparable. |
| `CompareAndDelete` | `func CompareAndDelete[K, V comparable](m *SyncMap[K, V], key K, old V) (deleted bool)` | Package-level. `V` must be comparable. |

Every symbol has a runnable godoc `Example` in [`example_test.go`](./example_test.go) — open any one of them on pkg.go.dev to see the exact usage pattern.

## 🧵 Thread Safety

All methods on `SyncMap` are safe for concurrent use by multiple goroutines **without additional locking**. This guarantee is inherited directly from `sync.Map`.

A few specifics worth calling out:

- `Range` does **not** correspond to a consistent snapshot. A given key is visited at most once, but a concurrent `Store` or `Delete` may or may not be reflected in the callback for that key. Identical to the stdlib `sync.Map.Range` contract.
- `Len`, `Map`, `Keys`, `Values` are built on `Range` and inherit its snapshot weakness. Treat the results as approximations, not atomic views. If you need a consistent multi-key view, serialise through your own lock.
- `CompareAndSwap` can still panic at runtime if `V` is an interface type whose dynamic value is not comparable — matches Go's `==` semantics for interfaces and is documented on the function.

## ⚡ Performance

Overhead against raw `sync.Map` is effectively zero. The committed [`bench.txt`](./bench.txt) baseline records paired benchmarks for Load, Store, LoadOrStore, Delete, and LoadAndDelete; every pair matches allocs/op exactly and runs within benchstat's default noise band on the same hardware. CI re-runs the full suite on every PR and fails the build on any time/op regression ≥ 10 % at p ≤ 0.05 or any positive allocs/op delta.

Run locally:

```bash
make bench # one-shot benchmarks
make bench-regression # compare this tree against bench.txt
```

## 🧭 When to use what

`sync.Map` (and therefore `SyncMap`) is the right tool for specific workloads. It is **not** a general-purpose concurrent map.

| Workload | Pick |
|---|---|
| Many readers, rare writers, stable key set | `SyncMap` |
| Goroutines own disjoint key sets | `SyncMap` |
| Single-goroutine owner, no concurrent access | plain `map` |
| Mixed read/write with small maps | `map` + `sync.RWMutex` |
| Mixed read/write with bounded high churn | `map` + `sync.RWMutex` (or a sharded map) |
| Need atomic snapshot-consistent reads of multiple keys | external lock around a plain `map` |
| Need TTL, eviction, bounded size | a cache library (e.g. `ristretto`, `otter`) |

`SyncMap` inherits every performance characteristic of `sync.Map`, including the caveats the stdlib itself lists in its package documentation. Read the stdlib docs first; the only thing this wrapper changes is call-site ergonomics.

## 🤖 For AI assistants

This repository ships [`llms.txt`](./llms.txt) (concise summary) and [`llms-full.txt`](./llms-full.txt) (full concatenated corpus) at the repo root. Ingest either when the assistant is expected to generate code against this library. The files are regenerated by `make llms-full` and CI-guarded against drift.

**Attribution rule.** Commit messages, PR descriptions, code comments, and any other tracked artefact must not reference AI-tooling product names or mark content as AI-produced. The specific token list and enforcement regex live in [`llms.txt`](./llms.txt) and the CI `attribution-guard` job; tooling-produced PRs that trip the guard are rejected.

## 🤝 Contributing

See [`CONTRIBUTING.md`](./CONTRIBUTING.md) for the branching model, commit message format, test requirements, and the AxonOps [Contributor License Agreement](./CLA.md). Every merged PR goes through the agent gate stack described in that file.

## 🔐 Security

Report suspected vulnerabilities privately to **oss@axonops.com**. Do not open a public issue. See [`SECURITY.md`](./SECURITY.md) for the full policy, supported-version table, and threat model.

## 📜 Attribution

This project is a fork of [`github.com/rgooding/go-syncmap`](https://github.com/rgooding/go-syncmap) by Robert Gooding, originally released under Apache 2.0. The original upstream copyright is preserved in git history and credited in [`NOTICE`](./NOTICE). Every change from the fork is enumerated in [`CHANGELOG.md`](./CHANGELOG.md).

## 📄 Licence

Apache License, Version 2.0. See [`LICENSE`](./LICENSE) for the full text.
Loading
Loading