@valentinkolb/sync 2.1.1 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -5
- package/index.d.ts +2 -1
- package/index.js +1633 -16
- package/package.json +1 -1
- package/src/registry.d.ts +130 -0
- package/src/scheduler.d.ts +23 -0
package/README.md
CHANGED
|
@@ -6,10 +6,10 @@ Distributed synchronization primitives for Bun and TypeScript, backed by Redis.
|
|
|
6
6
|
|
|
7
7
|
- **Bun-native** - Built for [Bun](https://bun.sh). Uses `Bun.redis`, `Bun.sleep`, `RedisClient` directly. No Node.js compatibility layers.
|
|
8
8
|
- **Minimal dependencies** - Only `zod` as a peer dependency. Everything else is Bun built-ins and Redis Lua scripts.
|
|
9
|
-
- **Composable building blocks** -
|
|
9
|
+
- **Composable building blocks** - Nine focused primitives that work independently or together. `job` composes `queue` + `topic` internally, `scheduler` composes durable dispatch on top of `job`.
|
|
10
10
|
- **Consistent API** - Every module follows the same pattern: `moduleName({ id, ...config })` returns an instance. No classes, no `.create()`, no `new`.
|
|
11
11
|
- **Atomic by default** - All Redis operations use Lua scripts for atomicity. No multi-step race conditions at the Redis level.
|
|
12
|
-
- **Schema-validated** - Queue, topic, job, and ephemeral payloads are validated with Zod at the boundary. Invalid data never enters Redis.
|
|
12
|
+
- **Schema-validated** - Queue, topic, job, registry, and ephemeral payloads are validated with Zod at the boundary. Invalid data never enters Redis.
|
|
13
13
|
|
|
14
14
|
## Features
|
|
15
15
|
|
|
@@ -19,10 +19,11 @@ Distributed synchronization primitives for Bun and TypeScript, backed by Redis.
|
|
|
19
19
|
- **Topic** - Pub/sub with consumer groups, at-least-once delivery, and live streaming
|
|
20
20
|
- **Job** - Durable job processing built on queue + topic with retries, cancellation, and event sourcing
|
|
21
21
|
- **Scheduler** - Distributed cron scheduler with idempotent registration, leader fencing, and durable dispatch via job submission
|
|
22
|
+
- **Registry** - Generic typed service/config registry with native prefix queries, CAS, TTL-backed liveness, and push-based change streams
|
|
22
23
|
- **Ephemeral** - TTL-based ephemeral key/value store with typed snapshots and event stream for upsert/touch/delete/expire
|
|
23
24
|
- **Retry utility** - Small transport-aware retry helper with sensible defaults and per-call override
|
|
24
25
|
|
|
25
|
-
For complete API details, use the modular skills in [`skills/`](./skills), especially each feature's `references/api.md`.
|
|
26
|
+
For complete API details, use the modular skills in [`skills/`](./skills), especially each feature's `references/api.md` such as `sync-registry`, `sync-scheduler`, `sync-job`, and `sync-topic`.
|
|
26
27
|
|
|
27
28
|
## Installation
|
|
28
29
|
|
|
@@ -67,7 +68,7 @@ const value2 = await retry(
|
|
|
67
68
|
```
|
|
68
69
|
|
|
69
70
|
Internal default behavior:
|
|
70
|
-
- Queue/topic/ephemeral `stream({ wait: true })` and topic `live()` use transport retries to stay alive across brief outages.
|
|
71
|
+
- Queue/topic/registry/ephemeral `stream({ wait: true })` and topic `live()` use transport retries to stay alive across brief outages.
|
|
71
72
|
- One-shot calls (`recv()`, `stream({ wait: false })`, `send()/pub()/submit()`) keep explicit failure semantics unless you wrap them with `retry(...)`.
|
|
72
73
|
|
|
73
74
|
Requires [Bun](https://bun.sh) and a Redis-compatible server (Redis 6.2+, Valkey, Dragonfly).
|
|
@@ -86,6 +87,7 @@ bunx skills add https://github.com/valentinkolb/sync --skill '*'
|
|
|
86
87
|
|
|
87
88
|
# install selected skills (example)
|
|
88
89
|
bunx skills add https://github.com/valentinkolb/sync \
|
|
90
|
+
--skill sync-registry \
|
|
89
91
|
--skill sync-scheduler \
|
|
90
92
|
--skill sync-job \
|
|
91
93
|
--skill sync-retry
|
|
@@ -351,6 +353,11 @@ await sched.register({
|
|
|
351
353
|
misfire: "skip", // default: do not replay backlog
|
|
352
354
|
meta: { owner: "ops" },
|
|
353
355
|
});
|
|
356
|
+
|
|
357
|
+
await sched.triggerNow({
|
|
358
|
+
id: "cleanup-hourly",
|
|
359
|
+
key: "ops-manual-run-1", // optional but recommended for retry-safe manual triggers
|
|
360
|
+
});
|
|
354
361
|
```
|
|
355
362
|
|
|
356
363
|
### Scheduler features
|
|
@@ -358,10 +365,66 @@ await sched.register({
|
|
|
358
365
|
- **Idempotent upsert registration**: repeated `register({ id, ... })` creates once, then updates in place.
|
|
359
366
|
- **No fixed leader pod**: leadership uses a renewable Redis lease (`mutex`) with epoch fencing.
|
|
360
367
|
- **Durable dispatch**: each cron slot maps to deterministic job key (`scheduleId:slotTs`) to prevent duplicates.
|
|
368
|
+
- **Durable manual trigger**: `triggerNow({ id, key? })` submits immediately through the same durable job path. Durability begins once `triggerNow()` returns a `jobId`.
|
|
361
369
|
- **Misfire policies**: `skip` (default), `catch_up_one`, `catch_up_all` with cap (`maxCatchUpRuns`).
|
|
362
370
|
- **Failure isolation**: submit retry + backoff, dispatch DLQ, configurable threshold for auto-advance after repeated failures.
|
|
363
371
|
- **Handler safety**: optional `strictHandlers` mode (default `true`) relinquishes leadership when required handlers are missing.
|
|
364
372
|
|
|
373
|
+
Manual trigger notes:
|
|
374
|
+
- `triggerNow()` does not require `start()` and does not alter cron state (`nextRunAt`, misfire handling, due slots).
|
|
375
|
+
- `triggerNow()` reuses the registered schedule input. If you need custom input per run, call the underlying `job.submit(...)` directly.
|
|
376
|
+
- Pass `key` for retry-safe idempotent manual triggering. Without `key`, repeated calls create additional runs.
|
|
377
|
+
|
|
378
|
+
## Registry
|
|
379
|
+
|
|
380
|
+
Typed service/config registry with exact-key reads, native prefix listing, compare-and-swap, optional TTL-backed liveness, and stream-based change notifications.
|
|
381
|
+
|
|
382
|
+
```ts
|
|
383
|
+
import { z } from "zod";
|
|
384
|
+
import { registry } from "@valentinkolb/sync";
|
|
385
|
+
|
|
386
|
+
const services = registry({
|
|
387
|
+
id: "services",
|
|
388
|
+
schema: z.object({
|
|
389
|
+
appId: z.string(),
|
|
390
|
+
kind: z.enum(["instance", "setting", "flag"]),
|
|
391
|
+
url: z.string().url().optional(),
|
|
392
|
+
}),
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
await services.upsert({
|
|
396
|
+
key: "apps/contacts/instances/i-1",
|
|
397
|
+
value: { appId: "contacts", kind: "instance", url: "https://contacts-1.internal" },
|
|
398
|
+
ttlMs: 15_000,
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
await services.touch({ key: "apps/contacts/instances/i-1" });
|
|
402
|
+
|
|
403
|
+
const active = await services.list({
|
|
404
|
+
prefix: "apps/contacts/instances/",
|
|
405
|
+
status: "active",
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
const snap = await services.list({ prefix: "apps/" });
|
|
409
|
+
const ac = new AbortController();
|
|
410
|
+
|
|
411
|
+
for await (const ev of services.reader({ prefix: "apps/", after: snap.cursor }).stream({ signal: ac.signal })) {
|
|
412
|
+
console.log(ev.type);
|
|
413
|
+
}
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
### Registry features
|
|
417
|
+
|
|
418
|
+
- **Native prefix listing**: `list({ prefix })` uses Redis-side lex indexing instead of client-side filtering.
|
|
419
|
+
- **Typed payloads**: values are validated with Zod on write and parsed on read.
|
|
420
|
+
- **CAS updates**: `cas({ key, version, value })` is atomic in Redis.
|
|
421
|
+
- **Optional liveness**: records with `ttlMs` are live and refreshed via `touch()`.
|
|
422
|
+
- **Expired-state visibility**: recently expired entries can be queried via `status: "expired"`.
|
|
423
|
+
- **Snapshot + cursor**: `list()` returns a cursor for replay-safe handoff into `reader()`.
|
|
424
|
+
- **Scoped streams**: watch a single key, a namespace prefix, or the whole registry.
|
|
425
|
+
- **Namespace-safe prefixes**: `list({ prefix })` and `reader({ prefix })` expect slash-suffixed namespace prefixes such as `apps/contacts/`.
|
|
426
|
+
- **Bounded growth**: configurable `maxEntries`, payload-size limits, root-stream retention, per-stream max length, and tombstone retention.
|
|
427
|
+
|
|
365
428
|
## Ephemeral
|
|
366
429
|
|
|
367
430
|
Typed ephemeral key/value store with TTL semantics and stream events. Useful for short-lived state like presence, worker heartbeats, or temporary coordination hints.
|
|
@@ -439,6 +502,7 @@ src/
|
|
|
439
502
|
topic.ts # Pub/sub with consumer groups
|
|
440
503
|
job.ts # Job processing (composes queue + topic)
|
|
441
504
|
scheduler.ts # Distributed cron scheduler (durable dispatch via job)
|
|
505
|
+
registry.ts # Typed registry with prefix queries, CAS, and liveness
|
|
442
506
|
ephemeral.ts # TTL-based ephemeral store with event stream
|
|
443
507
|
retry.ts # Generic transport-aware retry utility
|
|
444
508
|
internal/
|
|
@@ -450,7 +514,7 @@ tests/
|
|
|
450
514
|
*-utils.unit.test.ts # Pure unit tests
|
|
451
515
|
preload.ts # Sets REDIS_URL for test environment
|
|
452
516
|
index.ts # Public API exports
|
|
453
|
-
skills/ # Modular agent skills + per-feature API references
|
|
517
|
+
skills/ # Modular agent skills + per-feature API references (including sync-registry)
|
|
454
518
|
```
|
|
455
519
|
|
|
456
520
|
### Guidelines
|
package/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export { mutex, LockError, type Mutex, type Lock, type MutexConfig } from "./src
|
|
|
3
3
|
export { queue, type Queue, type QueueConfig, type QueueReader, type QueueRecvConfig, type QueueSendConfig, type QueueReceived, } from "./src/queue";
|
|
4
4
|
export { topic, type Topic, type TopicConfig, type TopicReader, type TopicRecvConfig, type TopicPubConfig, type TopicDelivery, type TopicLiveConfig, type TopicLiveEvent, } from "./src/topic";
|
|
5
5
|
export { job, type JobId, type JobStatus, type JobTerminal, type SubmitOptions, type JoinOptions, type CancelOptions, type JobEvent, type JobEvents, type JobContext, type JobHandle, type JobDefinition, } from "./src/job";
|
|
6
|
-
export { scheduler, type Scheduler, type SchedulerConfig, type SchedulerRegisterConfig, type SchedulerUnregisterConfig, type SchedulerGetConfig, type SchedulerInfo, type SchedulerMetric, type SchedulerMetricsSnapshot, } from "./src/scheduler";
|
|
6
|
+
export { scheduler, type Scheduler, type SchedulerConfig, type SchedulerRegisterConfig, type SchedulerUnregisterConfig, type SchedulerTriggerNowConfig, type SchedulerGetConfig, type SchedulerInfo, type SchedulerMetric, type SchedulerMetricsSnapshot, } from "./src/scheduler";
|
|
7
7
|
export { ephemeral, EphemeralCapacityError, EphemeralPayloadTooLargeError, type EphemeralConfig, type EphemeralUpsertConfig, type EphemeralTouchConfig, type EphemeralRemoveConfig, type EphemeralEntry, type EphemeralSnapshot, type EphemeralRecvConfig, type EphemeralEvent, type EphemeralReader, type EphemeralStore, } from "./src/ephemeral";
|
|
8
8
|
export { retry, isRetryableTransportError, DEFAULT_RETRY_OPTIONS, type RetryOptions, } from "./src/retry";
|
|
9
|
+
export { registry, RegistryCapacityError, RegistryPayloadTooLargeError, type Registry, type RegistryConfig, type RegistryUpsertConfig, type RegistryTouchConfig, type RegistryRemoveConfig, type RegistryGetConfig, type RegistryListConfig, type RegistryCasConfig, type RegistryEntry, type RegistrySnapshot, type RegistryRecvConfig, type RegistryEvent, type RegistryReader, } from "./src/registry";
|