@valentinkolb/sync 2.2.0 → 3.0.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.
@@ -0,0 +1,662 @@
1
+ // src/browser/internal/emitter.ts
2
+ class Emitter {
3
+ listeners = new Set;
4
+ on(fn) {
5
+ this.listeners.add(fn);
6
+ return () => {
7
+ this.listeners.delete(fn);
8
+ };
9
+ }
10
+ emit(value) {
11
+ for (const fn of this.listeners) {
12
+ fn(value);
13
+ }
14
+ }
15
+ once() {
16
+ return new Promise((resolve) => {
17
+ const unsub = this.on((value) => {
18
+ unsub();
19
+ resolve(value);
20
+ });
21
+ });
22
+ }
23
+ onceWithSignal(signal) {
24
+ if (!signal)
25
+ return this.once();
26
+ if (signal.aborted)
27
+ return Promise.reject(Object.assign(new Error("aborted"), { name: "AbortError" }));
28
+ return new Promise((resolve, reject) => {
29
+ const unsub = this.on((value) => {
30
+ unsub();
31
+ signal.removeEventListener("abort", onAbort);
32
+ resolve(value);
33
+ });
34
+ const onAbort = () => {
35
+ unsub();
36
+ signal.removeEventListener("abort", onAbort);
37
+ reject(Object.assign(new Error("aborted"), { name: "AbortError" }));
38
+ };
39
+ signal.addEventListener("abort", onAbort, { once: true });
40
+ });
41
+ }
42
+ }
43
+
44
+ // src/browser/internal/event-log.ts
45
+ class EventLog {
46
+ entries = [];
47
+ seq = 0;
48
+ emitter = new Emitter;
49
+ maxLen;
50
+ retentionMs;
51
+ constructor(config = {}) {
52
+ this.maxLen = config.maxLen ?? 50000;
53
+ this.retentionMs = config.retentionMs ?? 5 * 60 * 1000;
54
+ }
55
+ append(fields) {
56
+ const id = String(++this.seq);
57
+ const entry = { id, ts: Date.now(), fields };
58
+ this.entries.push(entry);
59
+ this.trim();
60
+ this.emitter.emit(entry);
61
+ return id;
62
+ }
63
+ range(after, count) {
64
+ const afterNum = Number(after) || 0;
65
+ const result = [];
66
+ for (const entry of this.entries) {
67
+ if (Number(entry.id) <= afterNum)
68
+ continue;
69
+ result.push(entry);
70
+ if (count !== undefined && result.length >= count)
71
+ break;
72
+ }
73
+ return result;
74
+ }
75
+ latest() {
76
+ if (this.entries.length === 0)
77
+ return "0";
78
+ return this.entries[this.entries.length - 1].id;
79
+ }
80
+ earliest() {
81
+ if (this.entries.length === 0)
82
+ return null;
83
+ return this.entries[0].id;
84
+ }
85
+ has(cursor) {
86
+ const num = Number(cursor);
87
+ return this.entries.some((e) => Number(e.id) === num);
88
+ }
89
+ async* subscribe(after, signal) {
90
+ let cursor = after;
91
+ while (!signal?.aborted) {
92
+ const buffered = this.range(cursor);
93
+ if (buffered.length > 0) {
94
+ for (const entry of buffered) {
95
+ cursor = entry.id;
96
+ yield entry;
97
+ }
98
+ continue;
99
+ }
100
+ try {
101
+ const entry = await this.emitter.onceWithSignal(signal);
102
+ if (Number(entry.id) > Number(cursor)) {
103
+ cursor = entry.id;
104
+ yield entry;
105
+ }
106
+ } catch {
107
+ break;
108
+ }
109
+ }
110
+ }
111
+ trim() {
112
+ if (this.entries.length > this.maxLen) {
113
+ this.entries.splice(0, this.entries.length - this.maxLen);
114
+ }
115
+ if (this.retentionMs > 0) {
116
+ const cutoff = Date.now() - this.retentionMs;
117
+ let trimCount = 0;
118
+ for (const entry of this.entries) {
119
+ if (entry.ts >= cutoff)
120
+ break;
121
+ trimCount++;
122
+ }
123
+ if (trimCount > 0) {
124
+ this.entries.splice(0, trimCount);
125
+ }
126
+ }
127
+ }
128
+ get size() {
129
+ return this.entries.length;
130
+ }
131
+ }
132
+
133
+ // src/browser/registry.ts
134
+ var DEFAULT_MAX_ENTRIES = 1e4;
135
+ var DEFAULT_MAX_PAYLOAD_BYTES = 128 * 1024;
136
+ var DEFAULT_EVENT_RETENTION_MS = 5 * 60 * 1000;
137
+ var DEFAULT_EVENT_MAXLEN = 50000;
138
+ var DEFAULT_TOMBSTONE_RETENTION_MS = 5 * 60 * 1000;
139
+ var DEFAULT_LIST_LIMIT = 1000;
140
+ var DEFAULT_TIMEOUT_MS = 30000;
141
+ var MAX_KEY_BYTES = 512;
142
+ var MAX_KEY_DEPTH = 8;
143
+ var textEncoder = new TextEncoder;
144
+
145
+ class RegistryCapacityError extends Error {
146
+ constructor(message = "registry capacity reached") {
147
+ super(message);
148
+ this.name = "RegistryCapacityError";
149
+ }
150
+ }
151
+
152
+ class RegistryPayloadTooLargeError extends Error {
153
+ constructor(message) {
154
+ super(message);
155
+ this.name = "RegistryPayloadTooLargeError";
156
+ }
157
+ }
158
+ var assertLogicalKey = (value) => {
159
+ if (value.length === 0)
160
+ throw new Error("key must be non-empty");
161
+ if (value.startsWith("/"))
162
+ throw new Error("key must not start with '/'");
163
+ if (value.endsWith("/"))
164
+ throw new Error("key must not end with '/'");
165
+ if (value.includes("//"))
166
+ throw new Error("key must not contain '//'");
167
+ const bytes = textEncoder.encode(value).byteLength;
168
+ if (bytes > MAX_KEY_BYTES)
169
+ throw new Error(`key exceeds max length (${MAX_KEY_BYTES} bytes)`);
170
+ const segments = value.split("/").filter(Boolean);
171
+ if (segments.length > MAX_KEY_DEPTH)
172
+ throw new Error(`key depth exceeds max (${MAX_KEY_DEPTH})`);
173
+ };
174
+ var assertIdentifier = (value, label) => {
175
+ if (value.length === 0)
176
+ throw new Error(`${label} must be non-empty`);
177
+ if (value.length > 256)
178
+ throw new Error(`${label} too long (max 256 chars)`);
179
+ };
180
+ var ancestorPrefixes = (key) => {
181
+ const parts = key.split("/").filter(Boolean);
182
+ const prefixes = [];
183
+ let current = "";
184
+ for (const part of parts) {
185
+ current += part + "/";
186
+ prefixes.push(current);
187
+ }
188
+ if (prefixes.length > 0) {
189
+ prefixes.pop();
190
+ }
191
+ return prefixes;
192
+ };
193
+ var registry = (config) => {
194
+ assertIdentifier(config.id, "config.id");
195
+ const defaultTenant = config.tenantId ?? "default";
196
+ assertIdentifier(defaultTenant, "tenantId");
197
+ const maxEntries = config.limits?.maxEntries ?? DEFAULT_MAX_ENTRIES;
198
+ const maxPayloadBytes = config.limits?.maxPayloadBytes ?? DEFAULT_MAX_PAYLOAD_BYTES;
199
+ const eventRetentionMs = config.limits?.eventRetentionMs ?? DEFAULT_EVENT_RETENTION_MS;
200
+ const eventMaxLen = config.limits?.eventMaxLen ?? DEFAULT_EVENT_MAXLEN;
201
+ const tombstoneRetentionMs = config.limits?.tombstoneRetentionMs ?? DEFAULT_TOMBSTONE_RETENTION_MS;
202
+ const resolveTenant = (tenantId) => {
203
+ const resolved = tenantId ?? defaultTenant;
204
+ assertIdentifier(resolved, "tenantId");
205
+ return resolved;
206
+ };
207
+ const tenantStates = new Map;
208
+ const getTenantState = (tenantId) => {
209
+ let state = tenantStates.get(tenantId);
210
+ if (!state) {
211
+ state = {
212
+ seq: 0,
213
+ entries: new Map,
214
+ ttlTimers: new Map,
215
+ tombstones: new Map,
216
+ tombstoneTimers: new Map,
217
+ prefixRefs: new Map,
218
+ rootEventLog: new EventLog({ maxLen: eventMaxLen, retentionMs: eventRetentionMs }),
219
+ keyEventLogs: new Map,
220
+ prefixEventLogs: new Map
221
+ };
222
+ tenantStates.set(tenantId, state);
223
+ }
224
+ return state;
225
+ };
226
+ const getKeyEventLog = (state, key) => {
227
+ let log = state.keyEventLogs.get(key);
228
+ if (!log) {
229
+ log = new EventLog({ maxLen: eventMaxLen, retentionMs: eventRetentionMs });
230
+ state.keyEventLogs.set(key, log);
231
+ }
232
+ return log;
233
+ };
234
+ const getPrefixEventLog = (state, pfx) => {
235
+ let log = state.prefixEventLogs.get(pfx);
236
+ if (!log) {
237
+ log = new EventLog({ maxLen: eventMaxLen, retentionMs: eventRetentionMs });
238
+ state.prefixEventLogs.set(pfx, log);
239
+ }
240
+ return log;
241
+ };
242
+ const prefixRefInc = (state, key) => {
243
+ for (const pfx of ancestorPrefixes(key)) {
244
+ state.prefixRefs.set(pfx, (state.prefixRefs.get(pfx) ?? 0) + 1);
245
+ }
246
+ };
247
+ const prefixRefDec = (state, key) => {
248
+ for (const pfx of ancestorPrefixes(key)) {
249
+ const next = (state.prefixRefs.get(pfx) ?? 1) - 1;
250
+ if (next <= 0) {
251
+ state.prefixRefs.delete(pfx);
252
+ } else {
253
+ state.prefixRefs.set(pfx, next);
254
+ }
255
+ }
256
+ };
257
+ const emitEvent = (state, key, fields) => {
258
+ state.rootEventLog.append(fields);
259
+ getKeyEventLog(state, key).append(fields);
260
+ for (const pfx of ancestorPrefixes(key)) {
261
+ getPrefixEventLog(state, pfx).append(fields);
262
+ }
263
+ };
264
+ const scheduleExpiry = (state, logicalKey, ttlMs) => {
265
+ const existing = state.ttlTimers.get(logicalKey);
266
+ if (existing)
267
+ clearTimeout(existing);
268
+ state.ttlTimers.set(logicalKey, setTimeout(() => {
269
+ const entry = state.entries.get(logicalKey);
270
+ if (!entry)
271
+ return;
272
+ state.entries.delete(logicalKey);
273
+ state.ttlTimers.delete(logicalKey);
274
+ prefixRefDec(state, logicalKey);
275
+ const version = String(++state.seq);
276
+ const tombstone = {
277
+ key: logicalKey,
278
+ version,
279
+ removedAt: Date.now()
280
+ };
281
+ state.tombstones.set(logicalKey, tombstone);
282
+ state.tombstoneTimers.set(logicalKey, setTimeout(() => {
283
+ state.tombstones.delete(logicalKey);
284
+ state.tombstoneTimers.delete(logicalKey);
285
+ }, tombstoneRetentionMs));
286
+ emitEvent(state, logicalKey, {
287
+ type: "expire",
288
+ key: logicalKey,
289
+ version,
290
+ removedAt: Date.now()
291
+ });
292
+ }, ttlMs));
293
+ };
294
+ const upsert = async (cfg) => {
295
+ assertLogicalKey(cfg.key);
296
+ const tenantId = resolveTenant(cfg.tenantId);
297
+ const state = getTenantState(tenantId);
298
+ const isNew = !state.entries.has(cfg.key);
299
+ if (isNew && state.entries.size >= maxEntries) {
300
+ throw new RegistryCapacityError(`maxEntries (${maxEntries}) reached`);
301
+ }
302
+ const parsed = config.schema.safeParse(cfg.value);
303
+ if (!parsed.success)
304
+ throw parsed.error;
305
+ const payloadRaw = JSON.stringify(parsed.data);
306
+ const payloadBytes = textEncoder.encode(payloadRaw).byteLength;
307
+ if (payloadBytes > maxPayloadBytes) {
308
+ throw new RegistryPayloadTooLargeError(`payload exceeds limit (${maxPayloadBytes} bytes)`);
309
+ }
310
+ const now = Date.now();
311
+ const version = String(++state.seq);
312
+ const existingEntry = state.entries.get(cfg.key);
313
+ const stored = {
314
+ key: cfg.key,
315
+ data: parsed.data,
316
+ version,
317
+ createdAt: existingEntry?.createdAt ?? now,
318
+ updatedAt: now,
319
+ ttlMs: cfg.ttlMs ?? null,
320
+ expiresAt: cfg.ttlMs != null ? now + cfg.ttlMs : null
321
+ };
322
+ if (isNew) {
323
+ prefixRefInc(state, cfg.key);
324
+ }
325
+ state.entries.set(cfg.key, stored);
326
+ const tombstoneTimer = state.tombstoneTimers.get(cfg.key);
327
+ if (tombstoneTimer) {
328
+ clearTimeout(tombstoneTimer);
329
+ state.tombstoneTimers.delete(cfg.key);
330
+ }
331
+ state.tombstones.delete(cfg.key);
332
+ if (stored.ttlMs != null) {
333
+ scheduleExpiry(state, cfg.key, stored.ttlMs);
334
+ } else {
335
+ const ttlTimer = state.ttlTimers.get(cfg.key);
336
+ if (ttlTimer) {
337
+ clearTimeout(ttlTimer);
338
+ state.ttlTimers.delete(cfg.key);
339
+ }
340
+ }
341
+ const result = {
342
+ key: cfg.key,
343
+ value: parsed.data,
344
+ version,
345
+ status: "active",
346
+ createdAt: stored.createdAt,
347
+ updatedAt: now,
348
+ ttlMs: stored.ttlMs,
349
+ expiresAt: stored.expiresAt
350
+ };
351
+ emitEvent(state, cfg.key, {
352
+ type: "upsert",
353
+ key: cfg.key,
354
+ version,
355
+ status: "active",
356
+ createdAt: stored.createdAt,
357
+ updatedAt: now,
358
+ ttlMs: stored.ttlMs,
359
+ expiresAt: stored.expiresAt,
360
+ payload: payloadRaw
361
+ });
362
+ return result;
363
+ };
364
+ const touch = async (cfg) => {
365
+ assertLogicalKey(cfg.key);
366
+ const tenantId = resolveTenant(cfg.tenantId);
367
+ const state = getTenantState(tenantId);
368
+ const existing = state.entries.get(cfg.key);
369
+ if (!existing)
370
+ return { ok: false };
371
+ let ttlMs = cfg.ttlMs;
372
+ if (ttlMs == null) {
373
+ if (existing.expiresAt == null)
374
+ return { ok: false };
375
+ ttlMs = existing.expiresAt - existing.updatedAt;
376
+ if (ttlMs <= 0)
377
+ ttlMs = 1;
378
+ }
379
+ if (!Number.isFinite(ttlMs) || ttlMs <= 0)
380
+ throw new Error("ttlMs must be > 0");
381
+ const now = Date.now();
382
+ const expiresAt = now + ttlMs;
383
+ existing.updatedAt = now;
384
+ existing.expiresAt = expiresAt;
385
+ scheduleExpiry(state, cfg.key, ttlMs);
386
+ emitEvent(state, cfg.key, {
387
+ type: "touch",
388
+ key: cfg.key,
389
+ version: existing.version,
390
+ updatedAt: now,
391
+ expiresAt
392
+ });
393
+ return { ok: true, version: existing.version, expiresAt };
394
+ };
395
+ const remove = async (cfg) => {
396
+ assertLogicalKey(cfg.key);
397
+ const tenantId = resolveTenant(cfg.tenantId);
398
+ const state = getTenantState(tenantId);
399
+ const existing = state.entries.get(cfg.key);
400
+ if (!existing)
401
+ return false;
402
+ state.entries.delete(cfg.key);
403
+ prefixRefDec(state, cfg.key);
404
+ const ttlTimer = state.ttlTimers.get(cfg.key);
405
+ if (ttlTimer) {
406
+ clearTimeout(ttlTimer);
407
+ state.ttlTimers.delete(cfg.key);
408
+ }
409
+ const version = String(++state.seq);
410
+ const now = Date.now();
411
+ const tombstone = {
412
+ key: cfg.key,
413
+ version,
414
+ removedAt: now,
415
+ reason: cfg.reason
416
+ };
417
+ state.tombstones.set(cfg.key, tombstone);
418
+ state.tombstoneTimers.set(cfg.key, setTimeout(() => {
419
+ state.tombstones.delete(cfg.key);
420
+ state.tombstoneTimers.delete(cfg.key);
421
+ }, tombstoneRetentionMs));
422
+ const fields = {
423
+ type: "delete",
424
+ key: cfg.key,
425
+ version,
426
+ removedAt: now
427
+ };
428
+ if (cfg.reason)
429
+ fields.reason = cfg.reason;
430
+ emitEvent(state, cfg.key, fields);
431
+ return true;
432
+ };
433
+ const get = async (cfg) => {
434
+ assertLogicalKey(cfg.key);
435
+ const tenantId = resolveTenant(cfg.tenantId);
436
+ const state = getTenantState(tenantId);
437
+ const stored = state.entries.get(cfg.key);
438
+ if (stored) {
439
+ const parsed = config.schema.safeParse(stored.data);
440
+ if (!parsed.success)
441
+ return null;
442
+ return {
443
+ key: stored.key,
444
+ value: parsed.data,
445
+ version: stored.version,
446
+ status: "active",
447
+ createdAt: stored.createdAt,
448
+ updatedAt: stored.updatedAt,
449
+ ttlMs: stored.ttlMs,
450
+ expiresAt: stored.expiresAt
451
+ };
452
+ }
453
+ if (cfg.includeExpired) {
454
+ const tombstone = state.tombstones.get(cfg.key);
455
+ if (tombstone) {
456
+ return {
457
+ key: tombstone.key,
458
+ value: null,
459
+ version: tombstone.version,
460
+ status: "expired",
461
+ createdAt: tombstone.removedAt,
462
+ updatedAt: tombstone.removedAt,
463
+ ttlMs: null,
464
+ expiresAt: null
465
+ };
466
+ }
467
+ }
468
+ return null;
469
+ };
470
+ const list = async (cfg = {}) => {
471
+ const tenantId = resolveTenant(cfg.tenantId);
472
+ const state = getTenantState(tenantId);
473
+ const limit = cfg.limit ?? DEFAULT_LIST_LIMIT;
474
+ const status = cfg.status ?? "active";
475
+ const entries = [];
476
+ if (status === "active") {
477
+ for (const stored of state.entries.values()) {
478
+ if (cfg.prefix && !stored.key.startsWith(cfg.prefix))
479
+ continue;
480
+ const parsed = config.schema.safeParse(stored.data);
481
+ if (!parsed.success)
482
+ continue;
483
+ entries.push({
484
+ key: stored.key,
485
+ value: parsed.data,
486
+ version: stored.version,
487
+ status: "active",
488
+ createdAt: stored.createdAt,
489
+ updatedAt: stored.updatedAt,
490
+ ttlMs: stored.ttlMs,
491
+ expiresAt: stored.expiresAt
492
+ });
493
+ }
494
+ } else {
495
+ for (const tombstone of state.tombstones.values()) {
496
+ if (cfg.prefix && !tombstone.key.startsWith(cfg.prefix))
497
+ continue;
498
+ entries.push({
499
+ key: tombstone.key,
500
+ value: null,
501
+ version: tombstone.version,
502
+ status: "expired",
503
+ createdAt: tombstone.removedAt,
504
+ updatedAt: tombstone.removedAt,
505
+ ttlMs: null,
506
+ expiresAt: null
507
+ });
508
+ }
509
+ }
510
+ entries.sort((a, b) => a.key.localeCompare(b.key));
511
+ let start = 0;
512
+ if (cfg.afterKey) {
513
+ const idx = entries.findIndex((e) => e.key > cfg.afterKey);
514
+ start = idx >= 0 ? idx : entries.length;
515
+ }
516
+ const paginated = entries.slice(start, start + limit);
517
+ const hasMore = start + limit < entries.length;
518
+ return {
519
+ entries: paginated,
520
+ cursor: state.rootEventLog.latest(),
521
+ nextKey: hasMore ? entries[start + limit].key : undefined
522
+ };
523
+ };
524
+ const cas = async (cfg) => {
525
+ assertLogicalKey(cfg.key);
526
+ const tenantId = resolveTenant(cfg.tenantId);
527
+ const state = getTenantState(tenantId);
528
+ const existing = state.entries.get(cfg.key);
529
+ if (!existing)
530
+ return { ok: false };
531
+ if (existing.version !== cfg.version)
532
+ return { ok: false };
533
+ let preservedTtlMs;
534
+ if (existing.expiresAt != null) {
535
+ preservedTtlMs = Math.max(1, existing.expiresAt - Date.now());
536
+ }
537
+ const entry = await upsert({
538
+ key: cfg.key,
539
+ value: cfg.value,
540
+ ttlMs: preservedTtlMs,
541
+ tenantId: cfg.tenantId
542
+ });
543
+ return { ok: true, entry };
544
+ };
545
+ const reader = (readerCfg = {}) => {
546
+ const tenantId = resolveTenant(readerCfg.tenantId);
547
+ const state = getTenantState(tenantId);
548
+ let log;
549
+ if (readerCfg.key) {
550
+ log = getKeyEventLog(state, readerCfg.key);
551
+ } else if (readerCfg.prefix) {
552
+ log = getPrefixEventLog(state, readerCfg.prefix);
553
+ } else {
554
+ log = state.rootEventLog;
555
+ }
556
+ let cursor = readerCfg.after ?? log.latest();
557
+ const parseEvent = (entry) => {
558
+ const type = entry.fields.type;
559
+ if (type === "upsert") {
560
+ const rawPayload = entry.fields.payload;
561
+ if (!rawPayload)
562
+ return null;
563
+ try {
564
+ const payload = JSON.parse(rawPayload);
565
+ const parsed = config.schema.safeParse(payload);
566
+ if (!parsed.success)
567
+ return null;
568
+ return {
569
+ type: "upsert",
570
+ cursor: entry.id,
571
+ entry: {
572
+ key: entry.fields.key ?? "",
573
+ value: parsed.data,
574
+ version: String(entry.fields.version ?? ""),
575
+ status: "active",
576
+ createdAt: Number(entry.fields.createdAt ?? entry.fields.updatedAt),
577
+ updatedAt: Number(entry.fields.updatedAt),
578
+ ttlMs: entry.fields.ttlMs != null ? Number(entry.fields.ttlMs) : null,
579
+ expiresAt: entry.fields.expiresAt != null ? Number(entry.fields.expiresAt) : null
580
+ }
581
+ };
582
+ } catch {
583
+ return null;
584
+ }
585
+ }
586
+ if (type === "touch") {
587
+ return {
588
+ type: "touch",
589
+ cursor: entry.id,
590
+ key: entry.fields.key ?? "",
591
+ version: String(entry.fields.version ?? ""),
592
+ updatedAt: Number(entry.fields.updatedAt ?? entry.fields.expiresAt),
593
+ expiresAt: Number(entry.fields.expiresAt)
594
+ };
595
+ }
596
+ if (type === "delete") {
597
+ return {
598
+ type: "delete",
599
+ cursor: entry.id,
600
+ key: entry.fields.key ?? "",
601
+ version: String(entry.fields.version ?? ""),
602
+ removedAt: Number(entry.fields.removedAt ?? entry.fields.deletedAt),
603
+ reason: entry.fields.reason
604
+ };
605
+ }
606
+ if (type === "expire") {
607
+ return {
608
+ type: "expire",
609
+ cursor: entry.id,
610
+ key: entry.fields.key ?? "",
611
+ version: String(entry.fields.version ?? ""),
612
+ removedAt: Number(entry.fields.removedAt ?? entry.fields.expiredAt)
613
+ };
614
+ }
615
+ return null;
616
+ };
617
+ const recv = async (cfg = {}) => {
618
+ const wait = cfg.wait ?? true;
619
+ const timeoutMs = cfg.timeoutMs ?? DEFAULT_TIMEOUT_MS;
620
+ const entries = log.range(cursor, 1);
621
+ if (entries.length > 0) {
622
+ cursor = entries[0].id;
623
+ return parseEvent(entries[0]);
624
+ }
625
+ if (!wait)
626
+ return null;
627
+ const ac = new AbortController;
628
+ const timeout = setTimeout(() => ac.abort(), timeoutMs);
629
+ try {
630
+ for await (const entry of log.subscribe(cursor, cfg.signal ?? ac.signal)) {
631
+ clearTimeout(timeout);
632
+ cursor = entry.id;
633
+ const parsed = parseEvent(entry);
634
+ if (parsed)
635
+ return parsed;
636
+ }
637
+ } catch {} finally {
638
+ clearTimeout(timeout);
639
+ }
640
+ return null;
641
+ };
642
+ const stream = async function* (cfg = {}) {
643
+ const wait = cfg.wait ?? true;
644
+ while (!cfg.signal?.aborted) {
645
+ const event = await recv(cfg);
646
+ if (event) {
647
+ yield event;
648
+ continue;
649
+ }
650
+ if (!wait)
651
+ break;
652
+ }
653
+ };
654
+ return { recv, stream };
655
+ };
656
+ return { upsert, touch, remove, get, list, cas, reader };
657
+ };
658
+ export {
659
+ registry,
660
+ RegistryPayloadTooLargeError,
661
+ RegistryCapacityError
662
+ };