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
17 changes: 9 additions & 8 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,17 +121,18 @@ test/
└── performance/ # Performance benchmarks

docs/ # VitePress documentation site
specs/ # Technical specifications and ADRs
├── specs/ # Technical specifications
└── adr/ # Architectural decision records
```

## Types of Contributions

- **Bug fixes**: Always welcome (include test case demonstrating the bug)
- **New backends**: Follow `specs/interface.md` and existing patterns (Redis/PostgreSQL/Firestore)
- **New backends**: Follow `docs/specs/interface.md` and existing patterns (Redis/PostgreSQL/Firestore)
- **Performance improvements**: Include benchmarks showing improvement
- **Documentation**: Especially examples, edge cases, and troubleshooting
- **Tests**: Better coverage is always good
- **Spec reviews & improvements**: Review `specs/` directory and propose architectural improvements
- **Spec reviews & improvements**: Review `docs/specs/` and `docs/adr/` directories and propose architectural improvements
- Identify inconsistencies or ambiguities in specs
- Suggest new ADRs for design decisions
- Improve spec clarity and completeness
Expand All @@ -143,7 +144,7 @@ When contributing, follow these key principles from CLAUDE.md:

- **No over-engineering** - Keep it simple and pragmatic
- **Design APIs that are predictable, composable, and hard to misuse**
- **Record decisions in ADRs** (specs/adrs.md) as you go, not retroactively
- **Record decisions in ADRs** (`docs/adr/`) as you go, not retroactively
- **Make testability a first-class design constraint**
- **Prioritize correctness and safety over micro-optimizations**
- **Expose the smallest possible public API that solves the problem**
Expand All @@ -152,22 +153,22 @@ When contributing, follow these key principles from CLAUDE.md:

If contributing a new backend, ensure:

- [ ] Implements full `LockBackend` interface (specs/interface.md)
- [ ] Implements full `LockBackend` interface (`docs/specs/interface.md`)
- [ ] Uses `isLive()` from `common/time-predicates.ts` (no custom time logic)
- [ ] Uses `makeStorageKey()` for key generation with two-step fence pattern (specs/interface.md#fence-key-derivation, ADR-006)
- [ ] Uses `makeStorageKey()` for key generation with two-step fence pattern (`docs/specs/interface.md#fence-key-derivation`, ADR-006)
- [ ] Uses `formatFence()` for 15-digit zero-padded fence tokens (ADR-004)
- [ ] Implements TOCTOU protection for release/extend (ADR-003)
- [ ] Explicit ownership verification after reverse mapping
- [ ] Comprehensive unit and integration tests
- [ ] Backend-specific spec document (follow specs/redis-backend.md, specs/postgres-backend.md, or specs/firestore-backend.md pattern)
- [ ] Backend-specific spec document (follow `docs/specs/redis-backend.md`, `docs/specs/postgres-backend.md`, or `docs/specs/firestore-backend.md` pattern)

## Getting Help

- **Questions**: Open a [discussion](https://github.com/kriasoft/syncguard/discussions)
- **Discord**: Join [Kriasoft Discord](https://discord.gg/EnbEa7Gsxg) #syncguard channel
- **Bugs**: Check [existing issues](https://github.com/kriasoft/syncguard/issues) first
- **Ideas**: Start with a discussion before coding to align on approach
- **Documentation**: See [docs site](https://kriasoft.com/syncguard/) and specs/ directory
- **Documentation**: See [docs site](https://kriasoft.com/syncguard/) and `docs/specs/` directory

## Code of Conduct

Expand Down
109 changes: 35 additions & 74 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,92 +1,53 @@
# SyncGuard - Distributed Lock Library
# SyncGuard Distributed Lock Library

TypeScript library for preventing race conditions across microservices using Redis, PostgreSQL, or Firestore backends.
TypeScript locks for Redis, PostgreSQL, and Firestore.

## Commands
Docs:

```bash
bun run build # Build to dist/
bun run typecheck # Type check without emit
bun run format # Prettier auto-format
bun run dev # Watch mode
```

## Architecture

### Core API Design

See `specs/interface.md` for complete API examples, usage patterns, LockBackend interface specification, and type definitions.
- Specs in `docs/specs/` (start with `interface.md`, then backend deltas, ADRs in `docs/adr/000-template` and `003`–`016`) — see `docs/specs/README.md`
- VitePress site: <https://kriasoft.com/syncguard/>

### Project Structure
Commands:

```text
Core:
index.ts → Public API exports for custom backends
common/ → Shared utilities, types, and core functionality
(See common/README.md for detailed structure)

Backends:
redis/ → Redis backend implementation (see redis/README.md)
postgres/ → PostgreSQL backend implementation (see postgres/README.md)
firestore/ → Firestore backend implementation (see firestore/README.md)

Documentation:
specs/ → Technical specifications
README.md → Spec navigation & reading order
interface.md → LockBackend API contracts & usage examples
redis-backend.md → Redis backend specification
postgres-backend.md → PostgreSQL backend specification
firestore-backend.md → Firestore backend specification
adrs.md → Architectural decision records
docs/ → Documentation site (https://kriasoft.com/syncguard/)
```bash
bun run build # build dist/
bun run typecheck # type check
bun run format # prettier
bun run dev # watch mode
```

## Implementation Requirements

**Backend-specific requirements**: See `specs/interface.md`, `specs/redis-backend.md`, `specs/postgres-backend.md`, and `specs/firestore-backend.md`

### Key Design Principles
Architecture:

- No over-engineering - keep it simple and pragmatic.
- Design APIs that are predictable, composable, and hard to misuse.
- Record decisions in lightweight ADRs as you go, not retroactively.
- Make testability a first-class design constraint, not an afterthought.
- Performance: O(1) acquire/isLocked, O(log n) release/extend acceptable
- Prioritize correctness and safety over micro-optimizations.
- Expose the smallest possible public API that solves the problem.
- Prioritize optimal, simple and elegant API over backwards compatibility.
- Core API and types → `docs/specs/interface.md`
- Project layout: `index.ts` (public API), `common/` (shared utilities), `redis/`, `postgres/`, `firestore/` (backends; each has README), `docs/` (specs + ADRs)

### Module Exports
Implementation guardrails:

- Main: `syncguard` → Core types/utilities
- Submodules: `syncguard/redis`, `syncguard/postgres`, `syncguard/firestore`, `syncguard/common`
- All exports use ES modules with TypeScript declarations
- Follow `docs/specs/interface.md` plus backend specs (`redis-backend.md`, `postgres-backend.md`, `firestore-backend.md`)
- Backend specs restate inherited requirements (ADR-012) and add storage schema, atomicity, error mapping, TTL, and perf notes
- Atomic mutations; TOCTOU-safe release/extend; key- and lockId-based `lookup()`; reuse `common/time-predicates.ts:isLive`

## Testing Approach
Principles:

**Hybrid testing strategy** - See `test/README.md` for details
- Predictable, composable APIs; smallest practical surface
- Correctness and safety over micro-optimizations; testability first
- Record decisions as ADRs while working, not after

Assume that:
Module exports:

- Redis server is already running on localhost:6379
- PostgreSQL server is already running on localhost:5432
- Firestore emulator is already running on localhost:8080
- `syncguard` main types/utilities; subpaths: `syncguard/redis`, `syncguard/postgres`, `syncguard/firestore`, `syncguard/common` (ESM with d.ts)

### Development workflow
Testing:

1. **Unit tests**: `bun run test:unit` (fast, mocked dependencies)
2. **Build/typecheck**: `bun run build && bun run typecheck`
3. **Integration tests**: `bun run test:integration` (requires Redis, PostgreSQL, and Firestore emulator)
4. **Performance validation**: `bun run test:performance` (optional)
- Unit: `bun run test:unit`
- Build/typecheck: `bun run build && bun run typecheck`
- Integration: `bun run test:integration` (needs Redis 6379, Postgres 5432, Firestore emulator 8080)
- Performance: `bun run test:performance` (optional)

## Code Standards
Code standards:

- **Functional style**: Pure functions, immutable data, `const` over `let`, avoid classes
- **TypeScript**: Strict mode, ESNext target, noUncheckedIndexedAccess
- **Formatting**: Prettier with default config
- **Headers**: SPDX license identifiers required
- **Exports**: Named exports preferred, tree-shakable modules
- **Error handling**: Primary API throws `LockError`, manual ops return `LockResult`
- **Error messages**: Include context (key, lockId) in all errors
- **Peer dependencies**: Optional - users install only what they need
- **JSDoc**: Required for all public APIs
- Functional style; strict TypeScript (ESNext, `noUncheckedIndexedAccess`)
- Prettier formatting; SPDX headers required
- Named exports; tree-shakable
- Errors: public API throws `LockError`; manual ops return `LockResult`; include key/lockId in messages
- Peer deps optional; JSDoc on public APIs
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -577,7 +577,7 @@ acquisition: {
We welcome contributions! Here's how you can help:

- 🐛 **Bug fixes** - Include test cases
- 🚀 **New backends** - Follow [specs/interface.md](./specs/interface.md)
- 🚀 **New backends** - Follow [docs/specs/interface.md](./docs/specs/interface.md)
- 📖 **Documentation** - Examples, guides, troubleshooting
- 📋 **Spec reviews** - Validate specs match implementation, propose improvements
- ✅ **Tests** - Improve coverage
Expand All @@ -587,7 +587,7 @@ See [CONTRIBUTING.md](.github/CONTRIBUTING.md) for detailed guidelines.
## Support & Documentation

- **Docs**: [Full documentation](https://kriasoft.com/syncguard/)
- **Specs**: [Technical specifications](./specs/) - Architecture decisions and backend requirements
- **Specs**: [Technical specifications](./docs/specs/) - Architecture decisions and backend requirements
- **Discord**: [Join our community](https://discord.gg/EnbEa7Gsxg)
- **Issues**: [GitHub Issues](https://github.com/kriasoft/syncguard/issues)

Expand Down
9 changes: 2 additions & 7 deletions common/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,10 +228,5 @@ bun run test:unit

## Implementation References

- **Specification**: See `specs/interface.md` for complete requirements
- **ADRs**: See `specs/adrs.md` for design decisions
- ADR-003: Explicit Ownership Verification
- ADR-004: Fence Token Format
- ADR-005: Unified Time Tolerance
- ADR-006: Standardized Storage Key Generation
- ADR-007: Opt-in Telemetry
- **Specification**: See `docs/specs/interface.md` for complete requirements
- **ADRs**: See [`docs/adr/`](../docs/adr/) for design decisions
3 changes: 1 addition & 2 deletions common/auto-lock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ const defaultDisposalErrorHandler: OnReleaseError = (err, ctx) => {
/**
* Auto-managed lock with retry logic for acquisition contention.
* Backends perform single-attempt operations (ADR-009), retries handled here.
* @see specs/adrs.md
*/

/**
Expand Down Expand Up @@ -94,7 +93,7 @@ function calculateRetryDelay(
* @returns Result of fn execution
* @throws {LockError} AcquisitionTimeout, NetworkTimeout, or Internal
* @see common/types.ts for LockConfig
* @see specs/interface.md for usage examples
* @see docs/specs/interface.md for usage examples
*/
/**
* Creates a curried lock function bound to a specific backend.
Expand Down
2 changes: 1 addition & 1 deletion common/backend-semantics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export function mapFirestoreConditions(conditions: {
/**
* Unified backend observation mapper for Redis codes or Firestore conditions.
* @param observation - Redis script code or Firestore query analysis
* @see specs/interface.md
* @see docs/specs/interface.md
*/
export function mapBackendObservation(
observation:
Expand Down
2 changes: 1 addition & 1 deletion common/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* Core module exports for LockBackend implementations.
* Import from `syncguard/common` to build custom backends.
*
* @see specs/interface.md - LockBackend API contracts
* @see docs/specs/interface.md - LockBackend API contracts
*/

// Export createAutoLock for internal use by backend modules only
Expand Down
8 changes: 3 additions & 5 deletions common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,9 @@ export const BACKEND_LIMITS = {
* const fenceDocId = makeStorageKey("", `fence:${baseKey}`, BACKEND_LIMITS.FIRESTORE, RESERVE_BYTES.FIRESTORE);
* ```
*
* @see specs/redis-backend.md#dual-key-storage-pattern - Redis reserve bytes calculation
* @see specs/postgres-backend.md#lock-table-requirements - PostgreSQL reserve bytes (0) rationale
* @see specs/firestore-backend.md#lock-documents - Firestore reserve bytes (0) rationale
* @see docs/specs/redis-backend.md#dual-key-storage-pattern - Redis reserve bytes calculation
* @see docs/specs/postgres-backend.md#lock-table-requirements - PostgreSQL reserve bytes (0) rationale
* @see docs/specs/firestore-backend.md#lock-documents - Firestore reserve bytes (0) rationale
*/
export const RESERVE_BYTES = {
/**
Expand Down Expand Up @@ -137,8 +137,6 @@ export const LOCK_DEFAULTS = {
* and precision safety within Lua's 53-bit float (2^53-1 ≈ 9.007e15).
*
* **Capacity**: 10^15 fence tokens = ~31.7 years at 1M locks/sec.
*
* @see specs/adrs.md ADR-004 - Fence token format and overflow handling
*/
export const FENCE_THRESHOLDS = {
/**
Expand Down
6 changes: 3 additions & 3 deletions common/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,9 @@ function sha256Sync(data: Uint8Array): Uint8Array {
* @param reserveBytes - Bytes reserved for suffixes in derived keys (e.g., Redis index/fence keys)
* @returns Storage key, truncated/hashed if necessary
* @throws {LockError} "InvalidArgument" if prefix + reserve exceeds limit, or if even hashed form exceeds limit
* @see specs/interface.md#storage-key-generation - Normative specification
* @see specs/interface.md#fence-key-derivation - Two-step fence key pattern
* @see specs/adrs.md ADR-006 - Mandatory uniform key truncation rationale
* @see docs/specs/interface.md#storage-key-generation - Normative specification
* @see docs/specs/interface.md#fence-key-derivation - Two-step fence key pattern
* @see docs/adr/006-mandatory-key-truncation.md - Mandatory uniform key truncation rationale
*/
export function makeStorageKey(
prefix: string,
Expand Down
10 changes: 5 additions & 5 deletions common/disposable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,9 @@
* **Important**: disposeTimeoutMs bounds the async disposal wait time, not the actual
* backend cleanup. Use for responsiveness guarantees in high-reliability contexts.
*
* @see specs/interface.md#resource-management - Normative specification
* @see specs/adrs.md#adr-015-async-raii-for-locks - ADR-015: Async RAII for Locks
* @see specs/adrs.md#adr-016-opt-in-disposal-timeout - ADR-016: Opt-In Disposal Timeout
* @see docs/specs/interface.md#resource-management - Normative specification
* @see docs/adr/015-async-raii-locks.md - Async RAII for Locks
* @see docs/adr/016-disposal-timeout.md - Opt-In Disposal Timeout
*/

import type {
Expand Down Expand Up @@ -128,8 +128,8 @@ export type { OnReleaseError } from "./types.js";
* });
* ```
*
* @see specs/interface.md#error-handling - Error handling best practices
* @see specs/adrs.md#adr-015-async-raii-for-locks - Disposal error semantics
* @see docs/specs/interface.md#error-handling - Error handling best practices
* @see docs/adr/015-async-raii-locks.md - Disposal error semantics
*/
const defaultDisposalErrorHandler: OnReleaseError = (err, ctx) => {
// Only log in development or when explicitly enabled via env var
Expand Down
4 changes: 2 additions & 2 deletions common/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ import type {
* @param backend - Base backend to instrument
* @param options - Telemetry configuration with event callback
* @returns Instrumented backend with same capabilities
* @see specs/interface.md Usage patterns and examples
* @see specs/adrs.md ADR-007 for opt-in design rationale
* @see docs/specs/interface.md Usage patterns and examples
* @see docs/adr/007-opt-in-telemetry.md for opt-in design rationale
*/
export function withTelemetry<C extends BackendCapabilities>(
backend: LockBackend<C>,
Expand Down
2 changes: 1 addition & 1 deletion common/time-predicates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
/**
* Canonical time authority predicates for cross-backend consistency.
* ALL backends MUST use these functions - custom time logic is forbidden.
* @see specs/interface.md
* @see docs/specs/interface.md
*/

/**
Expand Down
14 changes: 7 additions & 7 deletions common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export type Fence = string;
/**
* Hash identifier for observability (SHA-256 truncated to 96 bits, 24 hex chars).
*
* @see specs/interface.md#hash-identifier-format - Normative specification
* @see docs/specs/interface.md#hash-identifier-format - Normative specification
*/
export type HashId = string;

Expand Down Expand Up @@ -124,7 +124,7 @@ export type ExtendResult = { ok: true; expiresAtMs: number } | { ok: false };
/**
* Sanitized lock info from lookup(). Hashed identifiers prevent accidental logging.
*
* @see specs/interface.md#lock-information-types - Normative specification
* @see docs/specs/interface.md#lock-information-types - Normative specification
*/
export type LockInfo<C extends BackendCapabilities> = {
/** SHA-256 hash of key (96-bit truncated) */
Expand All @@ -140,7 +140,7 @@ export type LockInfo<C extends BackendCapabilities> = {
/**
* Debug variant with raw identifiers (via getByKeyRaw/getByIdRaw helpers). SECURITY: Contains sensitive data.
*
* @see specs/interface.md#lock-information-types - Normative specification
* @see docs/specs/interface.md#lock-information-types - Normative specification
*/
export type LockInfoDebug<C extends BackendCapabilities> = LockInfo<C> & {
/** Raw key for debugging */
Expand Down Expand Up @@ -207,7 +207,7 @@ export type LockInfoDebug<C extends BackendCapabilities> = LockInfo<C> & {
* @param error Normalized error that occurred during release (LockError or Error)
* @param context Error context with lock identifiers and source (always "disposal")
*
* @see specs/interface.md#error-handling-patterns - Complete error handling guide
* @see docs/specs/interface.md#error-handling-patterns - Complete error handling guide
*/
export type OnReleaseError = (
error: Error,
Expand Down Expand Up @@ -244,7 +244,7 @@ export interface BackendConfig {
* ```
*
* @see OnReleaseError for error handling patterns
* @see specs/interface.md#error-handling-patterns
* @see docs/specs/interface.md#error-handling-patterns
*/
onReleaseError?: OnReleaseError;

Expand Down Expand Up @@ -305,7 +305,7 @@ export interface LockConfig {
* ```
*
* @see OnReleaseError for error handling patterns
* @see specs/interface.md#error-handling-patterns
* @see docs/specs/interface.md#error-handling-patterns
*/
onReleaseError?: OnReleaseError;
}
Expand Down Expand Up @@ -377,7 +377,7 @@ export interface LockBackend<
/**
* Minimal event structure for telemetry. Hashes computed on-demand.
*
* @see specs/interface.md#telemetry-event-types - Normative specification
* @see docs/specs/interface.md#telemetry-event-types - Normative specification
*/
export type LockEvent = {
/** Operation type (acquire, release, extend, isLocked, lookup) */
Expand Down
Loading