@valentinkolb/sync 2.0.2 → 2.0.5

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Valentin Kolb
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,313 @@
1
+ # @valentinkolb/sync
2
+
3
+ Distributed synchronization primitives for Bun and TypeScript, backed by Redis.
4
+
5
+ ## Philosophy
6
+
7
+ - **Bun-native** - Built for [Bun](https://bun.sh). Uses `Bun.redis`, `Bun.sleep`, `RedisClient` directly. No Node.js compatibility layers.
8
+ - **Minimal dependencies** - Only `zod` as a peer dependency. Everything else is Bun built-ins and Redis Lua scripts.
9
+ - **Composable building blocks** - Five focused primitives that work independently or together. `job` composes `queue` + `topic` internally.
10
+ - **Consistent API** - Every module follows the same pattern: `moduleName({ id, ...config })` returns an instance. No classes, no `.create()`, no `new`.
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, and job payloads are validated with Zod at the boundary. Invalid data never enters Redis.
13
+
14
+ ## Features
15
+
16
+ - **Rate limiting** - Sliding window algorithm with atomic Lua scripts
17
+ - **Distributed mutex** - SET NX-based locking with retry, extend, and auto-expiry
18
+ - **Queue** - Durable work queue with leases, DLQ, delayed messages, and idempotency
19
+ - **Topic** - Pub/sub with consumer groups, at-least-once delivery, and live streaming
20
+ - **Job** - Durable job processing built on queue + topic with retries, cancellation, and event sourcing
21
+
22
+ For a complete API reference (types, config options, Redis key patterns, internals), see [`llms.txt`](./llms.txt).
23
+
24
+ ## Installation
25
+
26
+ ```bash
27
+ bun add @valentinkolb/sync zod
28
+ ```
29
+
30
+ Requires [Bun](https://bun.sh) and a Redis-compatible server (Redis 6.2+, Valkey, Dragonfly).
31
+
32
+ ## Rate Limit
33
+
34
+ Sliding window rate limiter. Atomic via Lua script.
35
+
36
+ ```ts
37
+ import { ratelimit, RateLimitError } from "@valentinkolb/sync";
38
+
39
+ const limiter = ratelimit({
40
+ id: "api",
41
+ limit: 100,
42
+ windowSecs: 60,
43
+ });
44
+
45
+ const result = await limiter.check("user:123");
46
+ // { limited: false, remaining: 99, resetIn: 58432 }
47
+
48
+ try {
49
+ await limiter.checkOrThrow("user:123");
50
+ } catch (error) {
51
+ if (error instanceof RateLimitError) {
52
+ console.log(`Retry in ${error.resetIn}ms`);
53
+ }
54
+ }
55
+ ```
56
+
57
+ ## Mutex
58
+
59
+ Distributed lock with retry + jitter, TTL auto-expiry, and Lua-based owner-only release.
60
+
61
+ ```ts
62
+ import { mutex, LockError } from "@valentinkolb/sync";
63
+
64
+ const m = mutex({ id: "checkout", defaultTtl: 5000 });
65
+
66
+ // Automatic acquire + release
67
+ const result = await m.withLock("order:123", async (lock) => {
68
+ await m.extend(lock, 10_000); // extend if needed
69
+ return await processOrder();
70
+ });
71
+
72
+ // Throws LockError if lock cannot be acquired
73
+ await m.withLockOrThrow("order:123", async () => {
74
+ await doExclusiveWork();
75
+ });
76
+
77
+ // Manual acquire/release
78
+ const lock = await m.acquire("order:123");
79
+ if (lock) {
80
+ try { /* work */ } finally { await m.release(lock); }
81
+ }
82
+ ```
83
+
84
+ ## Queue
85
+
86
+ Durable work queue with at-least-once delivery, lease-based visibility, delayed messages, idempotency, and dead-letter queue.
87
+
88
+ ```ts
89
+ import { z } from "zod";
90
+ import { queue } from "@valentinkolb/sync";
91
+
92
+ const q = queue({
93
+ id: "mail.send",
94
+ schema: z.object({ to: z.string().email(), subject: z.string() }),
95
+ delivery: { defaultLeaseMs: 60_000, maxDeliveries: 5 },
96
+ limits: { maxMessageAgeMs: 7 * 24 * 60 * 60 * 1000 },
97
+ });
98
+
99
+ // Send
100
+ await q.send({
101
+ data: { to: "user@example.com", subject: "Welcome" },
102
+ idempotencyKey: "welcome:user@example.com",
103
+ delayMs: 5_000, // optional: deliver after 5s
104
+ meta: { traceId: "abc-123" }, // optional metadata
105
+ });
106
+
107
+ // Receive + process
108
+ const msg = await q.recv({ wait: true, timeoutMs: 30_000 });
109
+ if (msg) {
110
+ try {
111
+ await sendMail(msg.data);
112
+ await msg.ack();
113
+ } catch (error) {
114
+ await msg.nack({ delayMs: 5_000, error: String(error) });
115
+ }
116
+ }
117
+
118
+ // Stream processing
119
+ for await (const m of q.stream()) {
120
+ await handle(m.data);
121
+ await m.ack();
122
+ }
123
+
124
+ // Multiple readers (independent blocking clients)
125
+ const reader = q.reader();
126
+ const msg2 = await reader.recv({ signal: abortController.signal });
127
+ ```
128
+
129
+ ### Queue features
130
+
131
+ - **Lease-based delivery**: Messages are invisible to other consumers while leased. Call `msg.touch()` to extend.
132
+ - **Dead-letter queue**: After `maxDeliveries` failed attempts, messages move to DLQ.
133
+ - **Delayed messages**: `send({ delayMs })` or `nack({ delayMs })` for retry delays.
134
+ - **Idempotency**: `send({ idempotencyKey })` deduplicates within a configurable TTL.
135
+ - **Multi-tenant**: Pass `tenantId` to `send()` and `recv()` for isolated queues.
136
+ - **AbortSignal**: Pass `signal` to `recv()` for graceful shutdown.
137
+
138
+ ## Topic
139
+
140
+ Pub/sub with Redis Streams. Supports consumer groups (at-least-once, load-balanced) and live streaming (best-effort, all events).
141
+
142
+ ```ts
143
+ import { z } from "zod";
144
+ import { topic } from "@valentinkolb/sync";
145
+
146
+ const t = topic({
147
+ id: "order.events",
148
+ schema: z.object({ type: z.string(), orderId: z.string() }),
149
+ retentionMs: 7 * 24 * 60 * 60 * 1000,
150
+ });
151
+
152
+ // Publish
153
+ await t.pub({
154
+ data: { type: "order.confirmed", orderId: "o1" },
155
+ idempotencyKey: "confirm:o1",
156
+ meta: { source: "checkout" },
157
+ });
158
+
159
+ // Consumer group reader (at-least-once, load-balanced across consumers)
160
+ const reader = t.reader("mailer");
161
+ for await (const event of reader.stream()) {
162
+ await sendConfirmationEmail(event.data);
163
+ await event.commit();
164
+ }
165
+
166
+ // Multiple groups receive the same events independently
167
+ const analytics = t.reader("analytics");
168
+ const billing = t.reader("billing");
169
+
170
+ // Live stream (best-effort, no consumer group, no commit needed)
171
+ for await (const event of t.live({ signal: ac.signal })) {
172
+ console.log(event.data);
173
+ }
174
+
175
+ // Replay from a specific cursor
176
+ for await (const event of t.live({ after: "0-0" })) {
177
+ // receives all stored events from the beginning
178
+ }
179
+ ```
180
+
181
+ ### Topic features
182
+
183
+ - **Consumer groups**: Each group tracks its own position. Multiple consumers in the same group load-balance.
184
+ - **Live streaming**: `t.live()` uses XREAD for real-time, best-effort delivery to all listeners.
185
+ - **Replay**: Pass `after: "0-0"` to `live()` to replay all stored events.
186
+ - **Retention**: Automatic XTRIM based on `retentionMs`.
187
+ - **Multi-tenant**: Pass `tenantId` to `pub()` and `recv()` for isolated streams.
188
+ - **AbortSignal**: Pass `signal` to `recv()`, `stream()`, and `live()`.
189
+
190
+ ## Job
191
+
192
+ Durable job processing built on queue + topic. Supports retries with backoff, cancellation, event sourcing, and graceful shutdown.
193
+
194
+ ```ts
195
+ import { z } from "zod";
196
+ import { job } from "@valentinkolb/sync";
197
+
198
+ const sendOrderMail = job({
199
+ id: "mail.send-order",
200
+ schema: z.object({ orderId: z.string(), to: z.string().email() }),
201
+ defaults: { maxAttempts: 3, backoff: { kind: "exp", baseMs: 1000 } },
202
+ process: async ({ ctx, input }) => {
203
+ // ctx.signal is aborted on timeout or error
204
+ if (ctx.signal.aborted) return;
205
+
206
+ await ctx.heartbeat(); // extend lease
207
+ await ctx.step({ id: "send", run: () => mailProvider.send(input) });
208
+ return { ok: true };
209
+ },
210
+ });
211
+
212
+ // Submit
213
+ const id = await sendOrderMail.submit({
214
+ input: { orderId: "o1", to: "user@example.com" },
215
+ key: "mail:o1", // idempotency key
216
+ delayMs: 5_000, // schedule for later
217
+ maxAttempts: 3,
218
+ backoff: { kind: "exp", baseMs: 1000, maxMs: 30_000 },
219
+ });
220
+
221
+ // Wait for completion
222
+ const terminal = await sendOrderMail.join({ id, timeoutMs: 60_000 });
223
+ // terminal.status: "completed" | "failed" | "cancelled" | "timed_out"
224
+
225
+ // Cancel
226
+ await sendOrderMail.cancel({ id, reason: "user-request" });
227
+
228
+ // Event stream
229
+ const events = sendOrderMail.events(id);
230
+ for await (const e of events.reader("orchestrator").stream({ wait: false })) {
231
+ console.log(e.data.type); // "submitted" | "started" | "heartbeat" | "retry" | "completed" | "failed" | "cancelled"
232
+ await e.commit();
233
+ }
234
+
235
+ // Live events
236
+ for await (const e of events.live({ signal: ac.signal })) {
237
+ console.log(e.data.type);
238
+ }
239
+
240
+ // Graceful shutdown
241
+ sendOrderMail.stop();
242
+ ```
243
+
244
+ ### Job features
245
+
246
+ - **Automatic retries**: Fixed or exponential backoff with configurable max attempts.
247
+ - **Lease timeout**: Jobs that exceed `leaseMs` are automatically timed out.
248
+ - **Cancellation**: Cancel in-flight or queued jobs. Workers detect cancellation between steps.
249
+ - **Event sourcing**: Every state transition emits a typed event to a per-job topic.
250
+ - **Idempotent submit**: Pass `key` to deduplicate submissions atomically.
251
+ - **AbortSignal**: `ctx.signal` is aborted on timeout, error, or cancellation.
252
+ - **Graceful shutdown**: `stop()` signals the worker loop to exit.
253
+ - **Per-job state TTL**: Each job's state has its own Redis TTL (7 days default).
254
+
255
+ ## Testing
256
+
257
+ ```bash
258
+ bun test --preload ./tests/preload.ts
259
+ ```
260
+
261
+ Requires a Redis-compatible server on `localhost:6399` (configured in `tests/preload.ts`).
262
+
263
+ ## Contributing
264
+
265
+ ### Setup
266
+
267
+ ```bash
268
+ git clone https://github.com/valentinkolb/sync.git
269
+ cd sync
270
+ bun install
271
+ ```
272
+
273
+ ### Running tests
274
+
275
+ You need a Redis-compatible server on port 6399. The easiest way is Docker/Podman:
276
+
277
+ ```bash
278
+ docker run -d --name valkey -p 6399:6379 valkey/valkey:latest
279
+ bun test --preload ./tests/preload.ts
280
+ ```
281
+
282
+ ### Project structure
283
+
284
+ ```
285
+ src/
286
+ ratelimit.ts # Sliding window rate limiter
287
+ mutex.ts # Distributed lock
288
+ queue.ts # Durable work queue
289
+ topic.ts # Pub/sub with consumer groups
290
+ job.ts # Job processing (composes queue + topic)
291
+ internal/
292
+ job-utils.ts # Job helper functions (retry, timeout, parsing)
293
+ topic-utils.ts # Stream entry parsing helpers
294
+ tests/
295
+ *.test.ts # Integration tests (require Redis)
296
+ *-utils.unit.test.ts # Pure unit tests
297
+ preload.ts # Sets REDIS_URL for test environment
298
+ index.ts # Public API exports
299
+ llms.txt # Complete API reference for LLMs
300
+ ```
301
+
302
+ ### Guidelines
303
+
304
+ - Keep it minimal. No abstractions for one-time operations.
305
+ - Every Redis mutation must be in a Lua script for atomicity.
306
+ - Validate at boundaries (user input), trust internal data.
307
+ - All modules follow the `moduleName({ id, ...config })` factory pattern.
308
+ - Tests go in `tests/`. Use `test:q`, `test:t`, etc. as prefix in tests to avoid collisions. Each test file has a `beforeEach` that cleans up its own keys.
309
+ - Run `bun test --preload ./tests/preload.ts` before submitting a PR. All tests must pass.
310
+
311
+ ## License
312
+
313
+ MIT
package/index.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ export { ratelimit, RateLimitError, type RateLimiter, type RateLimitResult, type RateLimitConfig, } from "./src/ratelimit";
2
+ export { mutex, LockError, type Mutex, type Lock, type MutexConfig } from "./src/mutex";
3
+ export { queue, type Queue, type QueueConfig, type QueueReader, type QueueRecvConfig, type QueueSendConfig, type QueueReceived, } from "./src/queue";
4
+ export { topic, type Topic, type TopicConfig, type TopicReader, type TopicRecvConfig, type TopicPubConfig, type TopicDelivery, type TopicLiveConfig, type TopicLiveEvent, } from "./src/topic";
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";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@valentinkolb/sync",
3
- "version": "2.0.2",
3
+ "version": "2.0.5",
4
4
  "description": "Distributed synchronization primitives for Bun and TypeScript",
5
5
  "main": "index.js",
6
6
  "module": "index.js",
@@ -0,0 +1,11 @@
1
+ export type JobTerminalStatus = "completed" | "failed" | "cancelled" | "timed_out";
2
+ export type RetryBackoff = {
3
+ kind: "fixed" | "exp";
4
+ baseMs: number;
5
+ maxMs?: number;
6
+ } | undefined;
7
+ export declare const isTerminalStatus: (status: string) => status is JobTerminalStatus;
8
+ export declare const parseJsonOrNull: <T>(raw: string | null) => T | null;
9
+ export declare const createTimeoutError: () => Error;
10
+ export declare const withTimeout: <T>(promise: Promise<T>, timeoutMs: number) => Promise<T>;
11
+ export declare const computeRetryDelay: (backoff: RetryBackoff, attempt: number) => number;
@@ -0,0 +1,7 @@
1
+ export type RawFields = Record<string, string>;
2
+ export type ParsedEntry = {
3
+ id: string;
4
+ fields: RawFields;
5
+ };
6
+ export declare const fieldArrayToObject: (value: unknown) => RawFields;
7
+ export declare const parseFirstStreamEntry: (raw: unknown) => ParsedEntry | null;
package/src/job.d.ts ADDED
@@ -0,0 +1,105 @@
1
+ import { z, type ZodTypeAny } from "zod";
2
+ import { type Topic } from "./topic";
3
+ export type JobId = string;
4
+ export type JobStatus = "completed" | "failed" | "cancelled" | "timed_out";
5
+ export type JobTerminal<Result = unknown> = {
6
+ id: JobId;
7
+ status: JobStatus;
8
+ result?: Result;
9
+ error?: {
10
+ message: string;
11
+ code?: string;
12
+ };
13
+ finishedAt: number;
14
+ };
15
+ export type SubmitOptions = {
16
+ key?: string;
17
+ delayMs?: number;
18
+ at?: number;
19
+ maxAttempts?: number;
20
+ backoff?: {
21
+ kind: "fixed" | "exp";
22
+ baseMs: number;
23
+ maxMs?: number;
24
+ };
25
+ leaseMs?: number;
26
+ meta?: Record<string, unknown>;
27
+ };
28
+ export type JoinOptions = {
29
+ timeoutMs?: number;
30
+ };
31
+ export type CancelOptions = {
32
+ reason?: string;
33
+ };
34
+ export type JobEvent = {
35
+ type: "submitted";
36
+ id: JobId;
37
+ ts: number;
38
+ } | {
39
+ type: "started";
40
+ id: JobId;
41
+ runId: string;
42
+ attempt: number;
43
+ ts: number;
44
+ } | {
45
+ type: "heartbeat";
46
+ id: JobId;
47
+ runId: string;
48
+ ts: number;
49
+ } | {
50
+ type: "retry";
51
+ id: JobId;
52
+ runId: string;
53
+ nextAt: number;
54
+ reason?: string;
55
+ ts: number;
56
+ } | {
57
+ type: "completed";
58
+ id: JobId;
59
+ ts: number;
60
+ } | {
61
+ type: "failed";
62
+ id: JobId;
63
+ reason?: string;
64
+ ts: number;
65
+ } | {
66
+ type: "cancelled";
67
+ id: JobId;
68
+ reason?: string;
69
+ ts: number;
70
+ };
71
+ export type JobEvents = Pick<Topic<JobEvent>, "reader" | "live">;
72
+ export type JobContext = {
73
+ step<T>(cfg: {
74
+ id: string;
75
+ run: () => Promise<T> | T;
76
+ }): Promise<T>;
77
+ heartbeat(cfg?: {
78
+ leaseMs?: number;
79
+ }): Promise<void>;
80
+ signal: AbortSignal;
81
+ };
82
+ export type JobHandle<Input, Result = unknown> = {
83
+ id: string;
84
+ submit(cfg: {
85
+ input: Input;
86
+ } & SubmitOptions): Promise<JobId>;
87
+ join(cfg: {
88
+ id: JobId;
89
+ } & JoinOptions): Promise<JobTerminal<Result>>;
90
+ cancel(cfg: {
91
+ id: JobId;
92
+ } & CancelOptions): Promise<void>;
93
+ events(id: JobId): JobEvents;
94
+ stop(): void;
95
+ };
96
+ export type JobDefinition<TSchema extends ZodTypeAny, Result = unknown> = {
97
+ id: string;
98
+ schema: TSchema;
99
+ defaults?: Omit<SubmitOptions, "key" | "delayMs" | "at" | "meta">;
100
+ process: (cfg: {
101
+ ctx: JobContext;
102
+ input: z.infer<TSchema>;
103
+ }) => Promise<Result> | Result;
104
+ };
105
+ export declare const job: <TSchema extends ZodTypeAny, Result = unknown>(definition: JobDefinition<TSchema, Result>) => JobHandle<z.infer<TSchema>, Result>;
package/src/mutex.d.ts ADDED
@@ -0,0 +1,26 @@
1
+ export type Lock = {
2
+ resource: string;
3
+ value: string;
4
+ ttl: number;
5
+ expiration: number;
6
+ };
7
+ export type MutexConfig = {
8
+ id: string;
9
+ prefix?: string;
10
+ retryCount?: number;
11
+ retryDelay?: number;
12
+ defaultTtl?: number;
13
+ };
14
+ export type Mutex = {
15
+ id: string;
16
+ acquire(resource: string, ttl?: number): Promise<Lock | null>;
17
+ release(lock: Lock): Promise<void>;
18
+ withLock<T>(resource: string, fn: (lock: Lock) => Promise<T> | T, ttl?: number): Promise<T | null>;
19
+ withLockOrThrow<T>(resource: string, fn: (lock: Lock) => Promise<T> | T, ttl?: number): Promise<T>;
20
+ extend(lock: Lock, ttl?: number): Promise<boolean>;
21
+ };
22
+ export declare class LockError extends Error {
23
+ readonly resource: string;
24
+ constructor(resource: string);
25
+ }
26
+ export declare const mutex: (config: MutexConfig) => Mutex;
package/src/queue.d.ts ADDED
@@ -0,0 +1,67 @@
1
+ import type { z } from "zod";
2
+ export type QueueConfig<TSchema extends z.ZodTypeAny> = {
3
+ id: string;
4
+ schema: TSchema;
5
+ tenantId?: string;
6
+ prefix?: string;
7
+ ordering?: {
8
+ mode?: "best_effort" | "ordering_key_partitioned";
9
+ partitions?: number;
10
+ };
11
+ limits?: {
12
+ payloadBytes?: number;
13
+ maxMessageAgeMs?: number;
14
+ maxNackDelayMs?: number;
15
+ dlqRetentionMs?: number;
16
+ };
17
+ delivery?: {
18
+ defaultLeaseMs?: number;
19
+ maxDeliveries?: number;
20
+ };
21
+ };
22
+ export type QueueSendConfig<T> = {
23
+ tenantId?: string;
24
+ data: T;
25
+ delayMs?: number;
26
+ orderingKey?: string;
27
+ idempotencyKey?: string;
28
+ idempotencyTtlMs?: number;
29
+ meta?: Record<string, unknown>;
30
+ };
31
+ export type QueueRecvConfig = {
32
+ tenantId?: string;
33
+ wait?: boolean;
34
+ timeoutMs?: number;
35
+ leaseMs?: number;
36
+ consumerId?: string;
37
+ signal?: AbortSignal;
38
+ };
39
+ export type QueueReceived<T> = {
40
+ data: T;
41
+ messageId: string;
42
+ deliveryId: string;
43
+ attempt: number;
44
+ leaseUntil: number;
45
+ orderingKey?: string;
46
+ meta?: Record<string, unknown>;
47
+ ack(): Promise<boolean>;
48
+ nack(cfg?: {
49
+ delayMs?: number;
50
+ reason?: string;
51
+ error?: string;
52
+ }): Promise<boolean>;
53
+ touch(cfg?: {
54
+ leaseMs?: number;
55
+ }): Promise<boolean>;
56
+ };
57
+ export type QueueReader<T> = {
58
+ recv(cfg?: QueueRecvConfig): Promise<QueueReceived<T> | null>;
59
+ stream(cfg?: QueueRecvConfig): AsyncIterable<QueueReceived<T>>;
60
+ };
61
+ export type Queue<T> = QueueReader<T> & {
62
+ send(cfg: QueueSendConfig<T>): Promise<{
63
+ messageId: string;
64
+ }>;
65
+ reader(): QueueReader<T>;
66
+ };
67
+ export declare const queue: <TSchema extends z.ZodTypeAny>(config: QueueConfig<TSchema>) => Queue<z.infer<TSchema>>;
@@ -0,0 +1,22 @@
1
+ export type RateLimitResult = {
2
+ limited: boolean;
3
+ remaining: number;
4
+ resetIn: number;
5
+ };
6
+ export type RateLimitConfig = {
7
+ id: string;
8
+ limit: number;
9
+ windowSecs?: number;
10
+ prefix?: string;
11
+ };
12
+ export type RateLimiter = {
13
+ id: string;
14
+ check(identifier: string): Promise<RateLimitResult>;
15
+ checkOrThrow(identifier: string): Promise<RateLimitResult>;
16
+ };
17
+ export declare class RateLimitError extends Error {
18
+ readonly remaining: number;
19
+ readonly resetIn: number;
20
+ constructor(result: RateLimitResult);
21
+ }
22
+ export declare const ratelimit: (config: RateLimitConfig) => RateLimiter;
package/src/topic.d.ts ADDED
@@ -0,0 +1,63 @@
1
+ import type { z } from "zod";
2
+ export type TopicConfig<TSchema extends z.ZodTypeAny> = {
3
+ id: string;
4
+ schema: TSchema;
5
+ tenantId?: string;
6
+ prefix?: string;
7
+ limits?: {
8
+ payloadBytes?: number;
9
+ };
10
+ retentionMs?: number;
11
+ };
12
+ export type TopicPubConfig<T> = {
13
+ tenantId?: string;
14
+ data: T;
15
+ orderingKey?: string;
16
+ idempotencyKey?: string;
17
+ idempotencyTtlMs?: number;
18
+ meta?: Record<string, unknown>;
19
+ };
20
+ export type TopicRecvConfig = {
21
+ tenantId?: string;
22
+ timeoutMs?: number;
23
+ wait?: boolean;
24
+ signal?: AbortSignal;
25
+ };
26
+ export type TopicDelivery<T> = {
27
+ data: T;
28
+ eventId: string;
29
+ deliveryId: string;
30
+ cursor: string;
31
+ orderingKey?: string;
32
+ publishedAt: number;
33
+ meta?: Record<string, unknown>;
34
+ commit(): Promise<boolean>;
35
+ };
36
+ export type TopicLiveConfig = {
37
+ tenantId?: string;
38
+ after?: string;
39
+ signal?: AbortSignal;
40
+ timeoutMs?: number;
41
+ };
42
+ export type TopicLiveEvent<T> = {
43
+ data: T;
44
+ eventId: string;
45
+ cursor: string;
46
+ orderingKey?: string;
47
+ publishedAt: number;
48
+ meta?: Record<string, unknown>;
49
+ };
50
+ export type TopicReader<T> = {
51
+ group: string;
52
+ recv(cfg?: TopicRecvConfig): Promise<TopicDelivery<T> | null>;
53
+ stream(cfg?: TopicRecvConfig): AsyncIterable<TopicDelivery<T>>;
54
+ };
55
+ export type Topic<T> = {
56
+ pub(cfg: TopicPubConfig<T>): Promise<{
57
+ eventId: string;
58
+ cursor: string;
59
+ }>;
60
+ reader(group?: string): TopicReader<T>;
61
+ live(cfg?: TopicLiveConfig): AsyncIterable<TopicLiveEvent<T>>;
62
+ };
63
+ export declare const topic: <TSchema extends z.ZodTypeAny>(config: TopicConfig<TSchema>) => Topic<z.infer<TSchema>>;