@valentinkolb/sync 2.0.4 → 2.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@valentinkolb/sync",
3
- "version": "2.0.4",
3
+ "version": "2.1.0",
4
4
  "description": "Distributed synchronization primitives for Bun and TypeScript",
5
5
  "main": "index.js",
6
6
  "module": "index.js",
@@ -0,0 +1,101 @@
1
+ import type { z } from "zod";
2
+ export declare class EphemeralCapacityError extends Error {
3
+ constructor(message?: string);
4
+ }
5
+ export declare class EphemeralPayloadTooLargeError extends Error {
6
+ constructor(message: string);
7
+ }
8
+ export type EphemeralConfig<TSchema extends z.ZodTypeAny> = {
9
+ id: string;
10
+ schema: TSchema;
11
+ ttlMs: number;
12
+ tenantId?: string;
13
+ limits?: {
14
+ maxEntries?: number;
15
+ maxPayloadBytes?: number;
16
+ eventRetentionMs?: number;
17
+ eventMaxLen?: number;
18
+ };
19
+ };
20
+ export type EphemeralUpsertConfig<T> = {
21
+ key: string;
22
+ value: T;
23
+ ttlMs?: number;
24
+ tenantId?: string;
25
+ };
26
+ export type EphemeralTouchConfig = {
27
+ key: string;
28
+ ttlMs?: number;
29
+ tenantId?: string;
30
+ };
31
+ export type EphemeralRemoveConfig = {
32
+ key: string;
33
+ reason?: string;
34
+ tenantId?: string;
35
+ };
36
+ export type EphemeralEntry<T> = {
37
+ key: string;
38
+ value: T;
39
+ version: string;
40
+ updatedAt: number;
41
+ expiresAt: number;
42
+ };
43
+ export type EphemeralSnapshot<T> = {
44
+ entries: EphemeralEntry<T>[];
45
+ cursor: string;
46
+ };
47
+ export type EphemeralRecvConfig = {
48
+ wait?: boolean;
49
+ timeoutMs?: number;
50
+ signal?: AbortSignal;
51
+ };
52
+ export type EphemeralEvent<T> = {
53
+ type: "upsert";
54
+ cursor: string;
55
+ entry: EphemeralEntry<T>;
56
+ } | {
57
+ type: "touch";
58
+ cursor: string;
59
+ key: string;
60
+ version: string;
61
+ expiresAt: number;
62
+ } | {
63
+ type: "delete";
64
+ cursor: string;
65
+ key: string;
66
+ version: string;
67
+ deletedAt: number;
68
+ reason?: string;
69
+ } | {
70
+ type: "expire";
71
+ cursor: string;
72
+ key: string;
73
+ version: string;
74
+ expiredAt: number;
75
+ } | {
76
+ type: "overflow";
77
+ cursor: string;
78
+ after: string;
79
+ firstAvailable: string;
80
+ };
81
+ export type EphemeralReader<T> = {
82
+ recv(cfg?: EphemeralRecvConfig): Promise<EphemeralEvent<T> | null>;
83
+ stream(cfg?: EphemeralRecvConfig): AsyncIterable<EphemeralEvent<T>>;
84
+ };
85
+ export type EphemeralStore<T> = {
86
+ upsert(cfg: EphemeralUpsertConfig<T>): Promise<EphemeralEntry<T>>;
87
+ touch(cfg: EphemeralTouchConfig): Promise<{
88
+ ok: boolean;
89
+ version?: string;
90
+ expiresAt?: number;
91
+ }>;
92
+ remove(cfg: EphemeralRemoveConfig): Promise<boolean>;
93
+ snapshot(cfg?: {
94
+ tenantId?: string;
95
+ }): Promise<EphemeralSnapshot<T>>;
96
+ reader(cfg?: {
97
+ after?: string;
98
+ tenantId?: string;
99
+ }): EphemeralReader<T>;
100
+ };
101
+ export declare const ephemeral: <TSchema extends z.ZodTypeAny>(config: EphemeralConfig<TSchema>) => EphemeralStore<z.infer<TSchema>>;
@@ -0,0 +1,3 @@
1
+ export type MisfirePolicy = "skip" | "catch_up_one" | "catch_up_all";
2
+ export declare const nextCronTimestamp: (cron: string, tz: string, afterTimestampMs: number) => number;
3
+ export declare const assertValidTimeZone: (tz: string) => void;
@@ -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,107 @@
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
+ keyTtlMs?: number;
18
+ delayMs?: number;
19
+ at?: number;
20
+ maxAttempts?: number;
21
+ backoff?: {
22
+ kind: "fixed" | "exp";
23
+ baseMs: number;
24
+ maxMs?: number;
25
+ };
26
+ leaseMs?: number;
27
+ meta?: Record<string, unknown>;
28
+ };
29
+ export type JoinOptions = {
30
+ timeoutMs?: number;
31
+ };
32
+ export type CancelOptions = {
33
+ reason?: string;
34
+ };
35
+ export type JobEvent = {
36
+ type: "submitted";
37
+ id: JobId;
38
+ ts: number;
39
+ } | {
40
+ type: "started";
41
+ id: JobId;
42
+ runId: string;
43
+ attempt: number;
44
+ ts: number;
45
+ } | {
46
+ type: "heartbeat";
47
+ id: JobId;
48
+ runId: string;
49
+ ts: number;
50
+ } | {
51
+ type: "retry";
52
+ id: JobId;
53
+ runId: string;
54
+ nextAt: number;
55
+ reason?: string;
56
+ ts: number;
57
+ } | {
58
+ type: "completed";
59
+ id: JobId;
60
+ ts: number;
61
+ } | {
62
+ type: "failed";
63
+ id: JobId;
64
+ reason?: string;
65
+ ts: number;
66
+ } | {
67
+ type: "cancelled";
68
+ id: JobId;
69
+ reason?: string;
70
+ ts: number;
71
+ };
72
+ export type JobEvents = Pick<Topic<JobEvent>, "reader" | "live">;
73
+ export type JobContext = {
74
+ step<T>(cfg: {
75
+ id: string;
76
+ run: () => Promise<T> | T;
77
+ }): Promise<T>;
78
+ heartbeat(cfg?: {
79
+ leaseMs?: number;
80
+ }): Promise<void>;
81
+ signal: AbortSignal;
82
+ };
83
+ export type JobHandle<Input, Result = unknown> = {
84
+ id: string;
85
+ submit(cfg: {
86
+ input: Input;
87
+ } & SubmitOptions): Promise<JobId>;
88
+ validateInput(input: unknown): void;
89
+ join(cfg: {
90
+ id: JobId;
91
+ } & JoinOptions): Promise<JobTerminal<Result>>;
92
+ cancel(cfg: {
93
+ id: JobId;
94
+ } & CancelOptions): Promise<void>;
95
+ events(id: JobId): JobEvents;
96
+ stop(): void;
97
+ };
98
+ export type JobDefinition<TSchema extends ZodTypeAny, Result = unknown> = {
99
+ id: string;
100
+ schema: TSchema;
101
+ defaults?: Omit<SubmitOptions, "key" | "delayMs" | "at" | "meta">;
102
+ process: (cfg: {
103
+ ctx: JobContext;
104
+ input: z.infer<TSchema>;
105
+ }) => Promise<Result> | Result;
106
+ };
107
+ 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/retry.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ export type RetryOptions = {
2
+ attempts?: number;
3
+ minDelayMs?: number;
4
+ maxDelayMs?: number;
5
+ factor?: number;
6
+ jitter?: number;
7
+ signal?: AbortSignal;
8
+ retryIf?: (error: unknown) => boolean;
9
+ };
10
+ export declare const isRetryableTransportError: (error: unknown) => boolean;
11
+ export declare const DEFAULT_RETRY_OPTIONS: {
12
+ readonly attempts: 8;
13
+ readonly minDelayMs: 100;
14
+ readonly maxDelayMs: 2000;
15
+ readonly factor: 2;
16
+ readonly jitter: 0.2;
17
+ readonly retryIf: (error: unknown) => boolean;
18
+ };
19
+ export declare const retry: <T>(fn: (attempt: number) => Promise<T> | T, opts?: RetryOptions) => Promise<T>;
@@ -0,0 +1,141 @@
1
+ import { type MisfirePolicy } from "./internal/cron";
2
+ type JobSubmitter = {
3
+ id: string;
4
+ submit(cfg: {
5
+ input: unknown;
6
+ key?: string;
7
+ keyTtlMs?: number;
8
+ at?: number;
9
+ delayMs?: number;
10
+ meta?: Record<string, unknown>;
11
+ }): Promise<string>;
12
+ validateInput?(input: unknown): void;
13
+ };
14
+ export type SchedulerMetric = {
15
+ type: "leader_acquired";
16
+ ts: number;
17
+ } | {
18
+ type: "leader_lost";
19
+ ts: number;
20
+ reason: "extend_failed" | "stop";
21
+ } | {
22
+ type: "tick_error";
23
+ ts: number;
24
+ message: string;
25
+ } | {
26
+ type: "schedule_registered";
27
+ ts: number;
28
+ scheduleId: string;
29
+ created: boolean;
30
+ } | {
31
+ type: "schedule_updated";
32
+ ts: number;
33
+ scheduleId: string;
34
+ } | {
35
+ type: "schedule_unregistered";
36
+ ts: number;
37
+ scheduleId: string;
38
+ } | {
39
+ type: "dispatch_submitted";
40
+ ts: number;
41
+ scheduleId: string;
42
+ slotTs: number;
43
+ jobId: string;
44
+ } | {
45
+ type: "dispatch_skipped";
46
+ ts: number;
47
+ scheduleId: string;
48
+ reason: "missing_handler" | "cas_stale";
49
+ } | {
50
+ type: "dispatch_failed";
51
+ ts: number;
52
+ scheduleId: string;
53
+ message: string;
54
+ } | {
55
+ type: "dispatch_dlq";
56
+ ts: number;
57
+ scheduleId: string;
58
+ slotTs: number;
59
+ message: string;
60
+ } | {
61
+ type: "dispatch_advanced_after_failures";
62
+ ts: number;
63
+ scheduleId: string;
64
+ slotTs: number;
65
+ failures: number;
66
+ };
67
+ export type SchedulerConfig = {
68
+ id: string;
69
+ prefix?: string;
70
+ leader?: {
71
+ leaseMs?: number;
72
+ heartbeatMs?: number;
73
+ };
74
+ dispatch?: {
75
+ tickMs?: number;
76
+ batchSize?: number;
77
+ maxSubmitsPerTick?: number;
78
+ submitRetries?: number;
79
+ submitBackoffBaseMs?: number;
80
+ submitBackoffMaxMs?: number;
81
+ scheduledJobKeyTtlMs?: number;
82
+ dlqMaxEntries?: number;
83
+ maxConsecutiveDispatchFailures?: number;
84
+ };
85
+ strictHandlers?: boolean;
86
+ onMetric?: (metric: SchedulerMetric) => void;
87
+ };
88
+ export type SchedulerRegisterConfig = {
89
+ id: string;
90
+ cron: string;
91
+ tz?: string;
92
+ job: JobSubmitter;
93
+ input: unknown;
94
+ misfire?: MisfirePolicy;
95
+ maxCatchUpRuns?: number;
96
+ meta?: Record<string, unknown>;
97
+ };
98
+ export type SchedulerUnregisterConfig = {
99
+ id: string;
100
+ };
101
+ export type SchedulerGetConfig = {
102
+ id: string;
103
+ };
104
+ export type SchedulerInfo = {
105
+ id: string;
106
+ cron: string;
107
+ tz: string;
108
+ misfire: MisfirePolicy;
109
+ maxCatchUpRuns: number;
110
+ jobId: string;
111
+ nextRunAt: number;
112
+ createdAt: number;
113
+ updatedAt: number;
114
+ };
115
+ export type SchedulerMetricsSnapshot = {
116
+ isLeader: boolean;
117
+ leaderEpoch: number;
118
+ leaderChanges: number;
119
+ dispatchSubmitted: number;
120
+ dispatchFailed: number;
121
+ dispatchRetried: number;
122
+ dispatchSkipped: number;
123
+ dispatchDlq: number;
124
+ tickErrors: number;
125
+ lastTickAt: number | null;
126
+ };
127
+ export type Scheduler = {
128
+ id: string;
129
+ start(): void;
130
+ stop(): Promise<void>;
131
+ register(cfg: SchedulerRegisterConfig): Promise<{
132
+ created: boolean;
133
+ updated: boolean;
134
+ }>;
135
+ unregister(cfg: SchedulerUnregisterConfig): Promise<void>;
136
+ get(cfg: SchedulerGetConfig): Promise<SchedulerInfo | null>;
137
+ list(): Promise<SchedulerInfo[]>;
138
+ metrics(): SchedulerMetricsSnapshot;
139
+ };
140
+ export declare const scheduler: (config: SchedulerConfig) => Scheduler;
141
+ export {};
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>>;