@nymbalhq/contract-tests 1.2.0 → 1.4.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/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  CONTRACT_KINDS
3
- } from "./chunk-ABEL44IM.js";
3
+ } from "./chunk-KK53D2KL.js";
4
4
 
5
- // src/ai.ts
5
+ // src/analytics.ts
6
6
  import { describe as describe2, expect as expect2, it as it2 } from "vitest";
7
7
 
8
8
  // src/common.ts
@@ -94,59 +94,24 @@ function healthAndDowntime(build, unreachable, label, operation) {
94
94
  });
95
95
  }
96
96
 
97
- // src/ai.ts
98
- function runAiContract(name, build, options = {}) {
99
- describe2(`AiAdapter contract: ${name}`, () => {
100
- it2(
101
- "embed returns embedding vectors with correct dimensions",
102
- async () => {
103
- const adapter = await build();
104
- const embeddings = await adapter.embed(["hello world", "test text"]);
105
- expect2(Array.isArray(embeddings)).toBe(true);
106
- expect2(embeddings).toHaveLength(2);
107
- expect2(Array.isArray(embeddings[0])).toBe(true);
108
- expect2((embeddings[0] ?? []).length).toBeGreaterThan(0);
109
- },
110
- CASE_MS
111
- );
112
- it2(
113
- "complete returns a non-empty string",
114
- async () => {
115
- const adapter = await build();
116
- const result = await adapter.complete("Write one word: hello", { maxTokens: 10 });
117
- expect2(typeof result).toBe("string");
118
- expect2(result.length).toBeGreaterThan(0);
119
- },
120
- CASE_MS
121
- );
122
- healthAndDowntime(
123
- build,
124
- options.unreachable,
125
- "complete()",
126
- (adapter) => adapter.complete("Write one word: hello", { maxTokens: 10 })
127
- );
128
- });
129
- }
130
-
131
97
  // src/analytics.ts
132
- import { describe as describe3, expect as expect3, it as it3 } from "vitest";
133
98
  function runAnalyticsContract(name, build, options = {}) {
134
- describe3(`AnalyticsAdapter contract: ${name}`, () => {
135
- it3(
99
+ describe2(`AnalyticsAdapter contract: ${name}`, () => {
100
+ it2(
136
101
  "track resolves without throwing",
137
102
  async () => {
138
103
  const adapter = await build();
139
- await expect3(
104
+ await expect2(
140
105
  adapter.track("product_viewed", { productId: "prod-1", source: "test" })
141
106
  ).resolves.not.toThrow();
142
107
  },
143
108
  CASE_MS
144
109
  );
145
- it3(
110
+ it2(
146
111
  "identify resolves without throwing",
147
112
  async () => {
148
113
  const adapter = await build();
149
- await expect3(
114
+ await expect2(
150
115
  adapter.identify("user-1", { email: "test@example.com", plan: "free" })
151
116
  ).resolves.not.toThrow();
152
117
  },
@@ -162,11 +127,11 @@ function runAnalyticsContract(name, build, options = {}) {
162
127
  }
163
128
 
164
129
  // src/document-store.ts
165
- import { describe as describe5, expect as expect6, it as it6 } from "vitest";
130
+ import { describe as describe4, expect as expect5, it as it5 } from "vitest";
166
131
 
167
132
  // src/document-store-model.ts
168
133
  import fc from "fast-check";
169
- import { describe as describe4, expect as expect4, it as it4 } from "vitest";
134
+ import { describe as describe3, expect as expect3, it as it3 } from "vitest";
170
135
  import {
171
136
  compareDocumentValues,
172
137
  matchesSortKey
@@ -296,8 +261,8 @@ async function rejection(promise) {
296
261
  }
297
262
  var TTL_SECONDS = 1;
298
263
  function documentStoreModelCases(make, opts) {
299
- describe4("agrees with the reference model", () => {
300
- it4(
264
+ describe3("agrees with the reference model", () => {
265
+ it3(
301
266
  "every query\u2019s page sequence equals the reference: filter by partition and sortKey, order with nulls last and ties by key, slice by limit",
302
267
  async () => {
303
268
  await fc.assert(
@@ -313,11 +278,11 @@ function documentStoreModelCases(make, opts) {
313
278
  const q = { partition, sortKey, orderBy };
314
279
  const expected = reference(docs, q);
315
280
  const { pages, ended } = await walk(store, q, limit, docs.length + 2);
316
- expect4(ended, "the cursor walk never ended").toBe(true);
317
- for (const page of pages) expect4(page.length).toBeLessThanOrEqual(limit);
318
- expect4(withoutTrailingEmpty(pages.map(idsOf))).toEqual(chunk(expected, limit));
281
+ expect3(ended, "the cursor walk never ended").toBe(true);
282
+ for (const page of pages) expect3(page.length).toBeLessThanOrEqual(limit);
283
+ expect3(withoutTrailingEmpty(pages.map(idsOf))).toEqual(chunk(expected, limit));
319
284
  const byId = new Map(docs.map((d) => [d.id, d]));
320
- for (const item of pages.flat()) expect4(item).toEqual(byId.get(item.id));
285
+ for (const item of pages.flat()) expect3(item).toEqual(byId.get(item.id));
321
286
  });
322
287
  }
323
288
  ),
@@ -326,7 +291,7 @@ function documentStoreModelCases(make, opts) {
326
291
  },
327
292
  SLOW
328
293
  );
329
- it4(
294
+ it3(
330
295
  "without orderBy, a cursor walk returns exactly the reference\u2019s matches, each once",
331
296
  async () => {
332
297
  await fc.assert(
@@ -341,11 +306,11 @@ function documentStoreModelCases(make, opts) {
341
306
  const q = { partition, sortKey, orderBy: void 0 };
342
307
  const { pages, ended } = await walk(store, q, limit, docs.length + 2);
343
308
  const walked = idsOf(pages.flat());
344
- expect4(ended, "the cursor walk never ended").toBe(true);
345
- expect4(walked.length, `duplicates in ${walked.join(",")}`).toBe(
309
+ expect3(ended, "the cursor walk never ended").toBe(true);
310
+ expect3(walked.length, `duplicates in ${walked.join(",")}`).toBe(
346
311
  new Set(walked).size
347
312
  );
348
- expect4([...walked].sort()).toEqual(reference(docs, q));
313
+ expect3([...walked].sort()).toEqual(reference(docs, q));
349
314
  });
350
315
  }
351
316
  ),
@@ -354,7 +319,7 @@ function documentStoreModelCases(make, opts) {
354
319
  },
355
320
  SLOW
356
321
  );
357
- it4(
322
+ it3(
358
323
  "a cursor walk across a mid-walk delete returns every remaining match exactly once, in the reference order",
359
324
  async () => {
360
325
  await fc.assert(
@@ -374,14 +339,14 @@ function documentStoreModelCases(make, opts) {
374
339
  if (n === 1) await store.delete(COLLECTION, deleted);
375
340
  });
376
341
  const walked = idsOf(pages.flat());
377
- expect4(ended, "the cursor walk never ended").toBe(true);
378
- expect4(walked.length, `duplicates in ${walked.join(",")}`).toBe(
342
+ expect3(ended, "the cursor walk never ended").toBe(true);
343
+ expect3(walked.length, `duplicates in ${walked.join(",")}`).toBe(
379
344
  new Set(walked).size
380
345
  );
381
- expect4(walked.filter((id) => !expected.includes(id))).toEqual([]);
382
- expect4(expected.filter((id) => id !== deleted && !walked.includes(id))).toEqual([]);
346
+ expect3(walked.filter((id) => !expected.includes(id))).toEqual([]);
347
+ expect3(expected.filter((id) => id !== deleted && !walked.includes(id))).toEqual([]);
383
348
  if (orderBy !== void 0) {
384
- expect4(walked).toEqual(expected.filter((id) => walked.includes(id)));
349
+ expect3(walked).toEqual(expected.filter((id) => walked.includes(id)));
385
350
  }
386
351
  });
387
352
  }
@@ -391,7 +356,7 @@ function documentStoreModelCases(make, opts) {
391
356
  },
392
357
  SLOW
393
358
  );
394
- it4(
359
+ it3(
395
360
  "a cursor issued under one orderBy is refused under another, and continues the walk under its own",
396
361
  async () => {
397
362
  const generated = ["k1", "k2", "k3", "k4", "k5"].map((key, i) => ({
@@ -408,23 +373,23 @@ function documentStoreModelCases(make, opts) {
408
373
  orderBy: { field: "v", direction: "asc" }
409
374
  };
410
375
  const first = await store.query(COLLECTION, params(asc, 2));
411
- expect4(first.nextCursor).not.toBeNull();
376
+ expect3(first.nextCursor).not.toBeNull();
412
377
  const cursor = first.nextCursor;
413
378
  const otherDirection = { ...asc, orderBy: { field: "v", direction: "desc" } };
414
379
  const otherField = { ...asc, orderBy: { field: "nested.v", direction: "asc" } };
415
- expect4(
380
+ expect3(
416
381
  await rejection(store.query(COLLECTION, params(otherDirection, 2, cursor)))
417
382
  ).toBeInstanceOf(Error);
418
- expect4(
383
+ expect3(
419
384
  await rejection(store.query(COLLECTION, params(otherField, 2, cursor)))
420
385
  ).toBeInstanceOf(Error);
421
386
  const next = await store.query(COLLECTION, params(asc, 2, cursor));
422
- expect4(idsOf(next.items)).toEqual(reference(docs, asc).slice(2, 4));
387
+ expect3(idsOf(next.items)).toEqual(reference(docs, asc).slice(2, 4));
423
388
  });
424
389
  },
425
390
  SLOW
426
391
  );
427
- it4(
392
+ it3(
428
393
  "a document put with a TTL is returned until it expires, then by no get, query or batchGet; one put without stays",
429
394
  async () => {
430
395
  const store = await make();
@@ -434,33 +399,33 @@ function documentStoreModelCases(make, opts) {
434
399
  const putAt = Date.now();
435
400
  await store.put(COLLECTION, short.id, short, { ttl: TTL_SECONDS });
436
401
  await store.put(COLLECTION, long.id, long);
437
- expect4(await store.get(COLLECTION, short.id)).toEqual(short);
402
+ expect3(await store.get(COLLECTION, short.id)).toEqual(short);
438
403
  if (opts.clock !== void 0) {
439
404
  await opts.clock.advance(TTL_SECONDS * 1e3 - 1);
440
- expect4(await store.get(COLLECTION, short.id)).toEqual(short);
405
+ expect3(await store.get(COLLECTION, short.id)).toEqual(short);
441
406
  await opts.clock.advance(2);
442
407
  } else {
443
408
  const deadline = putAt + TTL_SECONDS * 1e3 + 3e3;
444
409
  while (await store.get(COLLECTION, short.id) !== null && Date.now() < deadline) {
445
410
  await new Promise((r) => setTimeout(r, 25));
446
411
  }
447
- expect4(Date.now() - putAt, "expired before its TTL").toBeGreaterThanOrEqual(
412
+ expect3(Date.now() - putAt, "expired before its TTL").toBeGreaterThanOrEqual(
448
413
  TTL_SECONDS * 1e3 - 100
449
414
  );
450
415
  }
451
- expect4(await store.get(COLLECTION, short.id)).toBeNull();
416
+ expect3(await store.get(COLLECTION, short.id)).toBeNull();
452
417
  const all = await store.query(COLLECTION, {
453
418
  partitionKey: { field: "part", value: "*" },
454
419
  limit: 100
455
420
  });
456
- expect4(idsOf(all.items)).toEqual([long.id]);
421
+ expect3(idsOf(all.items)).toEqual([long.id]);
457
422
  const batch = await store.batchGet(COLLECTION, [short.id, long.id]);
458
- expect4([...batch.keys()]).toEqual([long.id]);
423
+ expect3([...batch.keys()]).toEqual([long.id]);
459
424
  });
460
425
  },
461
426
  SLOW
462
427
  );
463
- it4("a conditional put applies when the field matches, and otherwise fails condition-failed and changes nothing", async () => {
428
+ it3("a conditional put applies when the field matches, and otherwise fails condition-failed and changes nothing", async () => {
464
429
  const store = await make();
465
430
  await closing(store, async () => {
466
431
  const v1 = {
@@ -473,7 +438,7 @@ function documentStoreModelCases(make, opts) {
473
438
  const v2 = { ...v1, label: "v2", version: 2 };
474
439
  await store.put(COLLECTION, "c", v1);
475
440
  await store.put(COLLECTION, "c", v2, { conditionKey: "version", conditionValue: 1 });
476
- expect4(await store.get(COLLECTION, "c")).toEqual(v2);
441
+ expect3(await store.get(COLLECTION, "c")).toEqual(v2);
477
442
  const stale = await rejection(
478
443
  store.put(
479
444
  COLLECTION,
@@ -482,16 +447,16 @@ function documentStoreModelCases(make, opts) {
482
447
  { conditionKey: "version", conditionValue: 1 }
483
448
  )
484
449
  );
485
- expect4(stale?.code).toBe("adapter.condition-failed");
486
- expect4(await store.get(COLLECTION, "c")).toEqual(v2);
450
+ expect3(stale?.code).toBe("adapter.condition-failed");
451
+ expect3(await store.get(COLLECTION, "c")).toEqual(v2);
487
452
  const absent = await rejection(
488
453
  store.put(COLLECTION, "none", v1, { conditionKey: "version", conditionValue: 1 })
489
454
  );
490
- expect4(absent?.code).toBe("adapter.condition-failed");
491
- expect4(await store.get(COLLECTION, "none")).toBeNull();
455
+ expect3(absent?.code).toBe("adapter.condition-failed");
456
+ expect3(await store.get(COLLECTION, "none")).toBeNull();
492
457
  });
493
458
  }, CASE_MS);
494
- it4(
459
+ it3(
495
460
  "batchPut stores every item and batchGet returns exactly the stored keys, equal to what was put",
496
461
  async () => {
497
462
  await fc.assert(
@@ -502,10 +467,10 @@ function documentStoreModelCases(make, opts) {
502
467
  await store.batchPut(COLLECTION, new Map(docs.map((d) => [d.id, d])));
503
468
  const asked = [...docs.map((d) => d.id), "contract-absent"];
504
469
  const got = await store.batchGet(COLLECTION, asked);
505
- expect4(got.size).toBe(docs.length);
470
+ expect3(got.size).toBe(docs.length);
506
471
  for (const doc of docs) {
507
- expect4(got.get(doc.id)).toEqual(doc);
508
- expect4(await store.get(COLLECTION, doc.id)).toEqual(doc);
472
+ expect3(got.get(doc.id)).toEqual(doc);
473
+ expect3(await store.get(COLLECTION, doc.id)).toEqual(doc);
509
474
  }
510
475
  });
511
476
  }),
@@ -514,7 +479,7 @@ function documentStoreModelCases(make, opts) {
514
479
  },
515
480
  SLOW
516
481
  );
517
- it4("collections are isolated for get, query and delete", async () => {
482
+ it3("collections are isolated for get, query and delete", async () => {
518
483
  const store = await make();
519
484
  await closing(store, async () => {
520
485
  const a = { id: "same", part: "p", nested: {}, label: "A" };
@@ -523,21 +488,21 @@ function documentStoreModelCases(make, opts) {
523
488
  await store.put("contract-col-a", a.id, a);
524
489
  await store.put("contract-col-a", onlyA.id, onlyA);
525
490
  await store.put("contract-col-b", b.id, b);
526
- expect4(await store.get("contract-col-a", "same")).toEqual(a);
527
- expect4(await store.get("contract-col-b", "same")).toEqual(b);
491
+ expect3(await store.get("contract-col-a", "same")).toEqual(a);
492
+ expect3(await store.get("contract-col-b", "same")).toEqual(b);
528
493
  const inB = await store.query("contract-col-b", {
529
494
  partitionKey: { field: "part", value: "*" },
530
495
  limit: 100
531
496
  });
532
- expect4(idsOf(inB.items)).toEqual([b.id]);
497
+ expect3(idsOf(inB.items)).toEqual([b.id]);
533
498
  await store.delete("contract-col-a", "same");
534
- expect4(await store.get("contract-col-b", "same")).toEqual(b);
535
- expect4(await store.get("contract-col-a", "only-a")).toEqual(onlyA);
499
+ expect3(await store.get("contract-col-b", "same")).toEqual(b);
500
+ expect3(await store.get("contract-col-a", "only-a")).toEqual(onlyA);
536
501
  });
537
502
  }, CASE_MS);
538
503
  if (opts.isolation !== void 0) {
539
504
  const isolation = opts.isolation;
540
- it4("two instances with different prefixes never see each other\u2019s documents", async () => {
505
+ it3("two instances with different prefixes never see each other\u2019s documents", async () => {
541
506
  const [one, two] = await isolation();
542
507
  try {
543
508
  const mine = { id: "shared-key", part: "p", nested: {}, label: "one" };
@@ -545,18 +510,18 @@ function documentStoreModelCases(make, opts) {
545
510
  await one.put(COLLECTION, mine.id, mine);
546
511
  await one.put(COLLECTION, "one-only", { ...mine, id: "one-only" });
547
512
  await two.put(COLLECTION, theirs.id, theirs);
548
- expect4(await one.get(COLLECTION, mine.id)).toEqual(mine);
549
- expect4(await two.get(COLLECTION, theirs.id)).toEqual(theirs);
550
- expect4(await two.get(COLLECTION, "one-only")).toBeNull();
513
+ expect3(await one.get(COLLECTION, mine.id)).toEqual(mine);
514
+ expect3(await two.get(COLLECTION, theirs.id)).toEqual(theirs);
515
+ expect3(await two.get(COLLECTION, "one-only")).toBeNull();
551
516
  const inTwo = await two.query(COLLECTION, {
552
517
  partitionKey: { field: "part", value: "*" },
553
518
  limit: 100
554
519
  });
555
- expect4(idsOf(inTwo.items)).toEqual([theirs.id]);
520
+ expect3(idsOf(inTwo.items)).toEqual([theirs.id]);
556
521
  const batch = await two.batchGet(COLLECTION, [mine.id, "one-only"]);
557
- expect4([...batch.values()]).toEqual([theirs]);
522
+ expect3([...batch.values()]).toEqual([theirs]);
558
523
  await one.delete(COLLECTION, mine.id);
559
- expect4(await two.get(COLLECTION, theirs.id)).toEqual(theirs);
524
+ expect3(await two.get(COLLECTION, theirs.id)).toEqual(theirs);
560
525
  } finally {
561
526
  await one.close();
562
527
  await two.close();
@@ -568,7 +533,7 @@ function documentStoreModelCases(make, opts) {
568
533
 
569
534
  // src/document-store-replace.ts
570
535
  import fc2 from "fast-check";
571
- import { expect as expect5, it as it5 } from "vitest";
536
+ import { expect as expect4, it as it4 } from "vitest";
572
537
  var SLOW2 = 6e4;
573
538
  var KEYS = ["a", "b", "c", "d", "e", "f", "g", "h"];
574
539
  var macrotask = () => new Promise((resolve) => setImmediate(resolve));
@@ -588,7 +553,7 @@ async function putAll(store, collection, docs) {
588
553
  }
589
554
  var docsFrom = (keys, v) => keys.map((id) => ({ id, part: "p", v }));
590
555
  function documentStoreReplaceCases(make, opts) {
591
- it5(
556
+ it4(
592
557
  "replaceCollection leaves exactly the new set, whatever the old and new sets: no stale key of the old set survives, and another collection is untouched (property)",
593
558
  async () => {
594
559
  await using(make, async (store) => {
@@ -605,11 +570,11 @@ function documentStoreReplaceCases(make, opts) {
605
570
  await putAll(store, decoy, docsFrom(oldKeys, 3));
606
571
  const next = docsFrom(newKeys, 2);
607
572
  await store.replaceCollection(collection, entriesOf(next));
608
- expect5(await snapshot(store, collection)).toEqual(shape(next));
573
+ expect4(await snapshot(store, collection)).toEqual(shape(next));
609
574
  for (const key of oldKeys.filter((k) => !newKeys.includes(k))) {
610
- expect5(await store.get(collection, key), `stale key ${key}`).toBeNull();
575
+ expect4(await store.get(collection, key), `stale key ${key}`).toBeNull();
611
576
  }
612
- expect5(await snapshot(store, decoy)).toEqual(shape(docsFrom(oldKeys, 3)));
577
+ expect4(await snapshot(store, decoy)).toEqual(shape(docsFrom(oldKeys, 3)));
613
578
  }
614
579
  ),
615
580
  { numRuns: 25 }
@@ -618,7 +583,7 @@ function documentStoreReplaceCases(make, opts) {
618
583
  },
619
584
  SLOW2
620
585
  );
621
- it5(
586
+ it4(
622
587
  "replaceCollection is atomic to a concurrent reader: polling while the replace runs sees only the full old set or the full new set, never a partial set",
623
588
  async () => {
624
589
  await using(make, async (store) => {
@@ -638,19 +603,19 @@ function documentStoreReplaceCases(make, opts) {
638
603
  done = true;
639
604
  });
640
605
  await reader;
641
- expect5(seen.length, "the reader read while the replace ran").toBeGreaterThan(0);
606
+ expect4(seen.length, "the reader read while the replace ran").toBeGreaterThan(0);
642
607
  for (const observed of seen) {
643
- expect5(
608
+ expect4(
644
609
  allowed.has(JSON.stringify(observed)),
645
610
  `a partial set: ${observed.join(" ")}`
646
611
  ).toBe(true);
647
612
  }
648
- expect5(await snapshot(store, "contract-replace-reader")).toEqual(shape(next));
613
+ expect4(await snapshot(store, "contract-replace-reader")).toEqual(shape(next));
649
614
  });
650
615
  },
651
616
  CASE_MS
652
617
  );
653
- it5(
618
+ it4(
654
619
  "replaceCollection whose source throws part-way rejects with the source\u2019s error and leaves the old set intact",
655
620
  async () => {
656
621
  await using(make, async (store) => {
@@ -665,34 +630,34 @@ function documentStoreReplaceCases(make, opts) {
665
630
  () => null,
666
631
  (error) => error
667
632
  );
668
- expect5(String(failed)).toContain("contract: the source failed");
669
- expect5(await snapshot(store, "contract-replace-failing")).toEqual(shape(old));
633
+ expect4(String(failed)).toContain("contract: the source failed");
634
+ expect4(await snapshot(store, "contract-replace-failing")).toEqual(shape(old));
670
635
  });
671
636
  },
672
637
  CASE_MS
673
638
  );
674
- it5(
639
+ it4(
675
640
  "replaceCollection with an empty source empties the collection",
676
641
  async () => {
677
642
  await using(make, async (store) => {
678
643
  await putAll(store, "contract-replace-empty", docsFrom(["a", "b"], 1));
679
644
  await store.replaceCollection("contract-replace-empty", entriesOf([]));
680
- expect5(await snapshot(store, "contract-replace-empty")).toEqual([]);
681
- expect5(await store.get("contract-replace-empty", "a")).toBeNull();
645
+ expect4(await snapshot(store, "contract-replace-empty")).toEqual([]);
646
+ expect4(await store.get("contract-replace-empty", "a")).toBeNull();
682
647
  });
683
648
  },
684
649
  CASE_MS
685
650
  );
686
651
  const clock = opts.clock;
687
652
  if (clock !== void 0) {
688
- it5(
653
+ it4(
689
654
  "replaceCollection writes documents without a TTL: a key whose old document had one is still there after that TTL passes",
690
655
  async () => {
691
656
  await using(make, async (store) => {
692
657
  await store.put("contract-replace-ttl", "a", { id: "a", part: "p", v: 1 }, { ttl: 60 });
693
658
  await store.replaceCollection("contract-replace-ttl", entriesOf(docsFrom(["a", "b"], 2)));
694
659
  await clock.advance(61e3);
695
- expect5(await snapshot(store, "contract-replace-ttl")).toEqual(["a:2", "b:2"]);
660
+ expect4(await snapshot(store, "contract-replace-ttl")).toEqual(["a:2", "b:2"]);
696
661
  });
697
662
  },
698
663
  CASE_MS
@@ -735,37 +700,37 @@ async function walk2(store, collection, params2, between) {
735
700
  return pages;
736
701
  }
737
702
  function runDocumentStoreContract(name, make, opts = {}) {
738
- describe5(`DocumentStoreAdapter contract: ${name}`, () => {
739
- it6(
703
+ describe4(`DocumentStoreAdapter contract: ${name}`, () => {
704
+ it5(
740
705
  "get is null for a missing key; put then get round-trips; a second put overwrites",
741
706
  async () => {
742
707
  await using2(make, async (store) => {
743
- expect6(await store.get("col", "missing")).toBeNull();
708
+ expect5(await store.get("col", "missing")).toBeNull();
744
709
  await store.put("col", "k1", { name: "test", value: 42, nested: { list: [1, "two"] } });
745
- expect6(await store.get("col", "k1")).toEqual({
710
+ expect5(await store.get("col", "k1")).toEqual({
746
711
  name: "test",
747
712
  value: 42,
748
713
  nested: { list: [1, "two"] }
749
714
  });
750
715
  await store.put("col", "k1", { v: 2 });
751
- expect6(await store.get("col", "k1")).toEqual({ v: 2 });
716
+ expect5(await store.get("col", "k1")).toEqual({ v: 2 });
752
717
  });
753
718
  },
754
719
  CASE_MS
755
720
  );
756
- it6(
721
+ it5(
757
722
  "delete removes the document, and deleting a missing key does not throw",
758
723
  async () => {
759
724
  await using2(make, async (store) => {
760
725
  await store.put("col", "k1", { v: 1 });
761
726
  await store.delete("col", "k1");
762
- expect6(await store.get("col", "k1")).toBeNull();
763
- await expect6(store.delete("col", "nope")).resolves.toBeUndefined();
727
+ expect5(await store.get("col", "k1")).toBeNull();
728
+ await expect5(store.delete("col", "nope")).resolves.toBeUndefined();
764
729
  });
765
730
  },
766
731
  CASE_MS
767
732
  );
768
- it6(
733
+ it5(
769
734
  "query narrows by partition, and '*' returns the whole collection",
770
735
  async () => {
771
736
  await using2(make, async (store) => {
@@ -775,15 +740,15 @@ function runDocumentStoreContract(name, make, opts = {}) {
775
740
  { id: "c", part: "p" }
776
741
  ]);
777
742
  const p = await store.query("col", { partitionKey: { field: "part", value: "p" } });
778
- expect6(ids(p.items).sort()).toEqual(["a", "c"]);
743
+ expect5(ids(p.items).sort()).toEqual(["a", "c"]);
779
744
  const all = await store.query("col", { partitionKey: { field: "part", value: "*" } });
780
- expect6(ids(all.items).sort()).toEqual(["a", "b", "c"]);
781
- expect6(all.nextCursor).toBeNull();
745
+ expect5(ids(all.items).sort()).toEqual(["a", "b", "c"]);
746
+ expect5(all.nextCursor).toBeNull();
782
747
  });
783
748
  },
784
749
  CASE_MS
785
750
  );
786
- it6(
751
+ it5(
787
752
  "sortKey eq is strict (1 is not '1', 'a' is not 'A'); gt, lt and between follow compareDocumentValues",
788
753
  async () => {
789
754
  await using2(make, async (store) => {
@@ -801,20 +766,20 @@ function runDocumentStoreContract(name, make, opts = {}) {
801
766
  sortKey
802
767
  })).items
803
768
  ).sort();
804
- expect6(await where({ field: "v", operator: "eq", value: 1 })).toEqual(["n1"]);
805
- expect6(await where({ field: "v", operator: "eq", value: "a" })).toEqual(["sa"]);
806
- expect6(await where({ field: "v", operator: "lt", value: "0" })).toEqual(["n1", "n5"]);
807
- expect6(await where({ field: "v", operator: "gt", value: 5 })).toEqual(
769
+ expect5(await where({ field: "v", operator: "eq", value: 1 })).toEqual(["n1"]);
770
+ expect5(await where({ field: "v", operator: "eq", value: "a" })).toEqual(["sa"]);
771
+ expect5(await where({ field: "v", operator: "lt", value: "0" })).toEqual(["n1", "n5"]);
772
+ expect5(await where({ field: "v", operator: "gt", value: 5 })).toEqual(
808
773
  ["nul", "s1", "sA", "sa"].sort()
809
774
  );
810
- expect6(await where({ field: "v", operator: "between", value: [2, "a"] })).toEqual(
775
+ expect5(await where({ field: "v", operator: "between", value: [2, "a"] })).toEqual(
811
776
  ["n5", "s1", "sA", "sa"].sort()
812
777
  );
813
778
  });
814
779
  },
815
780
  CASE_MS
816
781
  );
817
- it6(
782
+ it5(
818
783
  "orderBy orders by value with the collator, nulls and missing last, ties by key, on a dotted path too",
819
784
  async () => {
820
785
  await using2(make, async (store) => {
@@ -831,17 +796,17 @@ function runDocumentStoreContract(name, make, opts = {}) {
831
796
  partitionKey: { field: "part", value: "p" },
832
797
  orderBy: { field: "nested.v", direction: "asc", nulls: "last" }
833
798
  });
834
- expect6(ids(asc.items)).toEqual(["k6", "k5", "k2", "k1", "k4", "k0", "k3"]);
799
+ expect5(ids(asc.items)).toEqual(["k6", "k5", "k2", "k1", "k4", "k0", "k3"]);
835
800
  const desc = await store.query("col", {
836
801
  partitionKey: { field: "part", value: "p" },
837
802
  orderBy: { field: "nested.v", direction: "desc", nulls: "last" }
838
803
  });
839
- expect6(ids(desc.items)).toEqual(["k1", "k4", "k2", "k5", "k6", "k0", "k3"]);
804
+ expect5(ids(desc.items)).toEqual(["k1", "k4", "k2", "k5", "k6", "k0", "k3"]);
840
805
  });
841
806
  },
842
807
  CASE_MS
843
808
  );
844
- it6(
809
+ it5(
845
810
  "a limit and cursor walk returns every match exactly once, also when a document is deleted mid-walk",
846
811
  async () => {
847
812
  await using2(make, async (store) => {
@@ -854,20 +819,20 @@ function runDocumentStoreContract(name, make, opts = {}) {
854
819
  ...orderBy !== void 0 && { orderBy }
855
820
  };
856
821
  const whole = (await walk2(store, "col", params2)).flat();
857
- expect6(ids(whole).sort()).toEqual(ids(docs).sort());
822
+ expect5(ids(whole).sort()).toEqual(ids(docs).sort());
858
823
  const anchor = whole[1];
859
824
  const walked = (await walk2(store, "col", params2, async (page) => {
860
825
  if (page === 1) await store.delete("col", anchor.id);
861
826
  })).flat();
862
- expect6(new Set(ids(walked)).size).toBe(walked.length);
863
- expect6(ids(walked).sort()).toEqual(ids(docs).sort());
827
+ expect5(new Set(ids(walked)).size).toBe(walked.length);
828
+ expect5(ids(walked).sort()).toEqual(ids(docs).sort());
864
829
  await store.put("col", anchor.id, anchor);
865
830
  }
866
831
  });
867
832
  },
868
833
  SLOW3
869
834
  );
870
- it6(
835
+ it5(
871
836
  "a cursor issued under one orderBy is refused under another, and continues under its own",
872
837
  async () => {
873
838
  await using2(make, async (store) => {
@@ -882,25 +847,25 @@ function runDocumentStoreContract(name, make, opts = {}) {
882
847
  limit: 2
883
848
  };
884
849
  const first = await store.query("col", base);
885
- expect6(first.nextCursor).not.toBeNull();
850
+ expect5(first.nextCursor).not.toBeNull();
886
851
  const cursor = first.nextCursor;
887
852
  const other = await rejection2(
888
853
  store.query("col", { ...base, orderBy: { field: "v", direction: "desc" }, cursor })
889
854
  );
890
- expect6(other).toBeInstanceOf(Error);
855
+ expect5(other).toBeInstanceOf(Error);
891
856
  const next = await store.query("col", { ...base, cursor });
892
- expect6(ids(next.items)).toEqual(["k2", "k3"]);
857
+ expect5(ids(next.items)).toEqual(["k2", "k3"]);
893
858
  });
894
859
  },
895
860
  CASE_MS
896
861
  );
897
- it6(
862
+ it5(
898
863
  "a document put with a ttl (seconds) is gone from get, query and batchGet once it expires",
899
864
  async () => {
900
865
  await using2(make, async (store) => {
901
866
  await store.put("col", "short", { id: "short", part: "p" }, { ttl: 1 });
902
867
  await store.put("col", "kept", { id: "kept", part: "p" });
903
- expect6(await store.get("col", "short")).toEqual({ id: "short", part: "p" });
868
+ expect5(await store.get("col", "short")).toEqual({ id: "short", part: "p" });
904
869
  if (opts.clock !== void 0) {
905
870
  await opts.clock.advance(1001);
906
871
  } else {
@@ -909,15 +874,15 @@ function runDocumentStoreContract(name, make, opts = {}) {
909
874
  await new Promise((r) => setTimeout(r, 50));
910
875
  }
911
876
  }
912
- expect6(await store.get("col", "short")).toBeNull();
877
+ expect5(await store.get("col", "short")).toBeNull();
913
878
  const all = await store.query("col", { partitionKey: { field: "part", value: "*" } });
914
- expect6(ids(all.items)).toEqual(["kept"]);
915
- expect6([...(await store.batchGet("col", ["short", "kept"])).keys()]).toEqual(["kept"]);
879
+ expect5(ids(all.items)).toEqual(["kept"]);
880
+ expect5([...(await store.batchGet("col", ["short", "kept"])).keys()]).toEqual(["kept"]);
916
881
  });
917
882
  },
918
883
  SLOW3
919
884
  );
920
- it6(
885
+ it5(
921
886
  "a conditional put applies when the field is strictly equal, and otherwise fails condition-failed",
922
887
  async () => {
923
888
  await using2(make, async (store) => {
@@ -928,12 +893,12 @@ function runDocumentStoreContract(name, make, opts = {}) {
928
893
  { version: 2, label: "v2" },
929
894
  { conditionKey: "version", conditionValue: 1 }
930
895
  );
931
- expect6(await store.get("col", "c")).toEqual({ version: 2, label: "v2" });
896
+ expect5(await store.get("col", "c")).toEqual({ version: 2, label: "v2" });
932
897
  for (const conditionValue of [1, "2"]) {
933
898
  const failed = await rejection2(
934
899
  store.put("col", "c", { version: 3 }, { conditionKey: "version", conditionValue })
935
900
  );
936
- expect6(failed?.code).toBe("adapter.condition-failed");
901
+ expect5(failed?.code).toBe("adapter.condition-failed");
937
902
  }
938
903
  const absent = await rejection2(
939
904
  store.put(
@@ -943,14 +908,14 @@ function runDocumentStoreContract(name, make, opts = {}) {
943
908
  { conditionKey: "version", conditionValue: 1 }
944
909
  )
945
910
  );
946
- expect6(absent?.code).toBe("adapter.condition-failed");
947
- expect6(await store.get("col", "c")).toEqual({ version: 2, label: "v2" });
948
- expect6(await store.get("col", "none")).toBeNull();
911
+ expect5(absent?.code).toBe("adapter.condition-failed");
912
+ expect5(await store.get("col", "c")).toEqual({ version: 2, label: "v2" });
913
+ expect5(await store.get("col", "none")).toBeNull();
949
914
  });
950
915
  },
951
916
  CASE_MS
952
917
  );
953
- it6(
918
+ it5(
954
919
  "batchPut stores every item; batchGet returns exactly the stored keys",
955
920
  async () => {
956
921
  await using2(make, async (store) => {
@@ -962,32 +927,32 @@ function runDocumentStoreContract(name, make, opts = {}) {
962
927
  ])
963
928
  );
964
929
  const got = await store.batchGet("col", ["x", "y", "missing"]);
965
- expect6([...got.keys()].sort()).toEqual(["x", "y"]);
966
- expect6(got.get("x")).toEqual({ n: 10 });
967
- expect6(await store.get("col", "y")).toEqual({ n: 20 });
968
- expect6((await store.batchGet("col", [])).size).toBe(0);
930
+ expect5([...got.keys()].sort()).toEqual(["x", "y"]);
931
+ expect5(got.get("x")).toEqual({ n: 10 });
932
+ expect5(await store.get("col", "y")).toEqual({ n: 20 });
933
+ expect5((await store.batchGet("col", [])).size).toBe(0);
969
934
  });
970
935
  },
971
936
  CASE_MS
972
937
  );
973
- it6(
938
+ it5(
974
939
  "collections are isolated for get, query and delete",
975
940
  async () => {
976
941
  await using2(make, async (store) => {
977
942
  await store.put("colA", "key", { id: "key", part: "p", from: "A" });
978
943
  await store.put("colB", "key", { id: "key", part: "p", from: "B" });
979
- expect6(await store.get("colA", "key")).toEqual({ id: "key", part: "p", from: "A" });
944
+ expect5(await store.get("colA", "key")).toEqual({ id: "key", part: "p", from: "A" });
980
945
  const inB = await store.query("colB", { partitionKey: { field: "part", value: "*" } });
981
- expect6(inB.items).toEqual([{ id: "key", part: "p", from: "B" }]);
946
+ expect5(inB.items).toEqual([{ id: "key", part: "p", from: "B" }]);
982
947
  await store.delete("colA", "key");
983
- expect6(await store.get("colB", "key")).toEqual({ id: "key", part: "p", from: "B" });
948
+ expect5(await store.get("colB", "key")).toEqual({ id: "key", part: "p", from: "B" });
984
949
  });
985
950
  },
986
951
  CASE_MS
987
952
  );
988
953
  const isolation = opts.isolation;
989
954
  if (isolation !== void 0) {
990
- it6(
955
+ it5(
991
956
  "two stores under different prefixes never see each other\u2019s documents",
992
957
  async () => {
993
958
  const [one, two] = await isolation();
@@ -995,16 +960,16 @@ function runDocumentStoreContract(name, make, opts = {}) {
995
960
  await one.put("col", "shared", { id: "shared", part: "p", owner: "one" });
996
961
  await one.put("col", "one-only", { id: "one-only", part: "p", owner: "one" });
997
962
  await two.put("col", "shared", { id: "shared", part: "p", owner: "two" });
998
- expect6(await one.get("col", "shared")).toEqual({
963
+ expect5(await one.get("col", "shared")).toEqual({
999
964
  id: "shared",
1000
965
  part: "p",
1001
966
  owner: "one"
1002
967
  });
1003
- expect6(await two.get("col", "one-only")).toBeNull();
968
+ expect5(await two.get("col", "one-only")).toBeNull();
1004
969
  const inTwo = await two.query("col", { partitionKey: { field: "part", value: "*" } });
1005
- expect6(inTwo.items).toEqual([{ id: "shared", part: "p", owner: "two" }]);
970
+ expect5(inTwo.items).toEqual([{ id: "shared", part: "p", owner: "two" }]);
1006
971
  await one.delete("col", "shared");
1007
- expect6(await two.get("col", "shared")).toEqual({
972
+ expect5(await two.get("col", "shared")).toEqual({
1008
973
  id: "shared",
1009
974
  part: "p",
1010
975
  owner: "two"
@@ -1024,28 +989,28 @@ function runDocumentStoreContract(name, make, opts = {}) {
1024
989
  }
1025
990
 
1026
991
  // src/email.ts
1027
- import { describe as describe6, expect as expect7, it as it7 } from "vitest";
992
+ import { describe as describe5, expect as expect6, it as it6 } from "vitest";
1028
993
  var MAIL = {
1029
994
  template: "order-confirmation",
1030
995
  to: "test@example.com",
1031
996
  vars: { orderNumber: "ORD-001" }
1032
997
  };
1033
998
  function runEmailContract(name, build, options = {}) {
1034
- describe6(`EmailAdapter contract: ${name}`, () => {
1035
- it7(
999
+ describe5(`EmailAdapter contract: ${name}`, () => {
1000
+ it6(
1036
1001
  "sendTransactional resolves with accepted or messageId",
1037
1002
  async () => {
1038
1003
  const adapter = await build();
1039
1004
  const result = await adapter.sendTransactional(MAIL);
1040
- expect7(typeof result.accepted).toBe("boolean");
1005
+ expect6(typeof result.accepted).toBe("boolean");
1041
1006
  },
1042
1007
  CASE_MS
1043
1008
  );
1044
- it7(
1009
+ it6(
1045
1010
  "syncCustomerToList resolves without throwing",
1046
1011
  async () => {
1047
1012
  const adapter = await build();
1048
- await expect7(
1013
+ await expect6(
1049
1014
  adapter.syncCustomerToList({
1050
1015
  customerId: "cust-1",
1051
1016
  orderCount: 0,
@@ -1057,11 +1022,11 @@ function runEmailContract(name, build, options = {}) {
1057
1022
  },
1058
1023
  CASE_MS
1059
1024
  );
1060
- it7(
1025
+ it6(
1061
1026
  "removeCustomerFromList resolves without throwing",
1062
1027
  async () => {
1063
1028
  const adapter = await build();
1064
- await expect7(adapter.removeCustomerFromList("cust-1")).resolves.not.toThrow();
1029
+ await expect6(adapter.removeCustomerFromList("cust-1")).resolves.not.toThrow();
1065
1030
  },
1066
1031
  CASE_MS
1067
1032
  );
@@ -1075,13 +1040,13 @@ function runEmailContract(name, build, options = {}) {
1075
1040
  }
1076
1041
 
1077
1042
  // src/event-bus.ts
1078
- import { randomUUID as randomUUID2 } from "crypto";
1043
+ import { randomUUID as randomUUID3 } from "crypto";
1079
1044
  import { describe as describe8, expect as expect9, it as it9 } from "vitest";
1080
1045
 
1081
1046
  // src/event-bus-delivery.ts
1082
1047
  import { randomUUID } from "crypto";
1083
1048
  import fc3 from "fast-check";
1084
- import { describe as describe7, expect as expect8, it as it8 } from "vitest";
1049
+ import { describe as describe6, expect as expect7, it as it7 } from "vitest";
1085
1050
  var SLOW4 = 6e4;
1086
1051
  function event(type, payload = {}) {
1087
1052
  return {
@@ -1118,8 +1083,8 @@ async function closingAll(buses, body) {
1118
1083
  }
1119
1084
  var TYPES = ["order.created.v1", "order.paid.v1", "cart.updated.v1"];
1120
1085
  function eventBusDeliveryCases(make, opts) {
1121
- describe7("keeps the delivery contract", () => {
1122
- it8(
1086
+ describe6("keeps the delivery contract", () => {
1087
+ it7(
1123
1088
  "publish resolves only after every matching handler has finished: a 200 ms handler, named and unnamed, is done first",
1124
1089
  async () => {
1125
1090
  const bus = await make();
@@ -1140,14 +1105,14 @@ function eventBusDeliveryCases(make, opts) {
1140
1105
  const startedAt = Date.now();
1141
1106
  await bus.publish(event("contract.slow.v1"));
1142
1107
  const elapsed = Date.now() - startedAt;
1143
- expect8([...finished].sort()).toEqual(["named", "unnamed"]);
1144
- expect8(elapsed).toBeGreaterThanOrEqual(190);
1108
+ expect7([...finished].sort()).toEqual(["named", "unnamed"]);
1109
+ expect7(elapsed).toBeGreaterThanOrEqual(190);
1145
1110
  });
1146
1111
  },
1147
1112
  SLOW4
1148
1113
  );
1149
- it8(
1150
- "a throwing handler leaves publish resolved and every other matching handler called, event after event",
1114
+ it7(
1115
+ "a throwing named handler rejects publish with handler_failed naming it, and every other matching handler is called, event after event",
1151
1116
  async () => {
1152
1117
  const bus = await make();
1153
1118
  await closingAll([bus], async () => {
@@ -1162,14 +1127,23 @@ function eventBusDeliveryCases(make, opts) {
1162
1127
  await bus.subscribe("contract.boom.v1", async (e) => {
1163
1128
  received.push(e.id);
1164
1129
  });
1165
- const events = [event("contract.boom.v1"), event("contract.boom.v1"), event("contract.boom.v1")];
1166
- for (const e of events) await expect8(bus.publish(e)).resolves.toBeUndefined();
1167
- expect8(received).toEqual(events.map((e) => e.id));
1130
+ const events = [
1131
+ event("contract.boom.v1"),
1132
+ event("contract.boom.v1"),
1133
+ event("contract.boom.v1")
1134
+ ];
1135
+ for (const e of events) {
1136
+ await expect7(bus.publish(e)).rejects.toMatchObject({
1137
+ code: "adapter.handler_failed",
1138
+ context: { eventId: e.id, failed: [{ subscription: "contract-throws" }] }
1139
+ });
1140
+ }
1141
+ expect7(received).toEqual(events.map((e) => e.id));
1168
1142
  });
1169
1143
  },
1170
1144
  SLOW4
1171
1145
  );
1172
- it8(
1146
+ it7(
1173
1147
  "replay reaches derived subscriptions only, in order; a later publish still reaches every subscription",
1174
1148
  async () => {
1175
1149
  const bus = await make();
@@ -1187,19 +1161,19 @@ function eventBusDeliveryCases(make, opts) {
1187
1161
  await bus.subscribe("contract.replay.v1", async (e) => void sideUnnamed.push(e.id));
1188
1162
  const replayed = [event("contract.replay.v1"), event("contract.replay.v1")];
1189
1163
  await bus.replay(replayed);
1190
- expect8(derived).toEqual(replayed.map((e) => e.id));
1191
- expect8(sideNamed).toEqual([]);
1192
- expect8(sideUnnamed).toEqual([]);
1164
+ expect7(derived).toEqual(replayed.map((e) => e.id));
1165
+ expect7(sideNamed).toEqual([]);
1166
+ expect7(sideUnnamed).toEqual([]);
1193
1167
  const live = event("contract.replay.v1");
1194
1168
  await bus.publish(live);
1195
- expect8(derived).toEqual([...replayed, live].map((e) => e.id));
1196
- expect8(sideNamed).toEqual([live.id]);
1197
- expect8(sideUnnamed).toEqual([live.id]);
1169
+ expect7(derived).toEqual([...replayed, live].map((e) => e.id));
1170
+ expect7(sideNamed).toEqual([live.id]);
1171
+ expect7(sideUnnamed).toEqual([live.id]);
1198
1172
  });
1199
1173
  },
1200
1174
  SLOW4
1201
1175
  );
1202
- it8(
1176
+ it7(
1203
1177
  "publishBatch reaches each subscription with exactly its matching events, in batch order (wildcards and pattern arrays)",
1204
1178
  async () => {
1205
1179
  await fc3.assert(
@@ -1211,7 +1185,10 @@ function eventBusDeliveryCases(make, opts) {
1211
1185
  const fence = event("contract.fence.v1");
1212
1186
  const subs = [
1213
1187
  { patterns: ["order.*", fence.type], name: void 0 },
1214
- { patterns: ["cart.updated.v1", "order.paid.v1", fence.type], name: "contract-arr" },
1188
+ {
1189
+ patterns: ["cart.updated.v1", "order.paid.v1", fence.type],
1190
+ name: "contract-arr"
1191
+ },
1215
1192
  { patterns: ["order.created.v1", fence.type], name: "contract-one" }
1216
1193
  ];
1217
1194
  const seen = subs.map(() => []);
@@ -1230,7 +1207,7 @@ function eventBusDeliveryCases(make, opts) {
1230
1207
  );
1231
1208
  for (const [i, sub] of subs.entries()) {
1232
1209
  const expected = events.filter((e) => matches(sub.patterns, e.type));
1233
- expect8(seen[i], sub.patterns.join("|")).toEqual([
1210
+ expect7(seen[i], sub.patterns.join("|")).toEqual([
1234
1211
  ...expected.map((e) => e.id),
1235
1212
  fence.id
1236
1213
  ]);
@@ -1243,7 +1220,7 @@ function eventBusDeliveryCases(make, opts) {
1243
1220
  },
1244
1221
  SLOW4
1245
1222
  );
1246
- it8(
1223
+ it7(
1247
1224
  "an unsubscribed handler, by bus.unsubscribe or by its own unsubscribe(), receives nothing more",
1248
1225
  async () => {
1249
1226
  const bus = await make();
@@ -1262,14 +1239,14 @@ function eventBusDeliveryCases(make, opts) {
1262
1239
  await b.unsubscribe();
1263
1240
  const after = [event("contract.unsub.v1"), event("contract.unsub.v1")];
1264
1241
  for (const e of after) await bus.publish(e);
1265
- expect8(kept).toEqual([before, ...after].map((e) => e.id));
1266
- expect8(gone).toEqual([before.id]);
1267
- expect8(goneToo).toEqual([before.id]);
1242
+ expect7(kept).toEqual([before, ...after].map((e) => e.id));
1243
+ expect7(gone).toEqual([before.id]);
1244
+ expect7(goneToo).toEqual([before.id]);
1268
1245
  });
1269
1246
  },
1270
1247
  SLOW4
1271
1248
  );
1272
- it8(
1249
+ it7(
1273
1250
  "on one instance, a named and an unnamed subscription each handle every event once",
1274
1251
  async () => {
1275
1252
  const bus = await make();
@@ -1282,17 +1259,17 @@ function eventBusDeliveryCases(make, opts) {
1282
1259
  await bus.subscribe("contract.both.v1", async (e) => void unnamed.push(e.id));
1283
1260
  const events = Array.from({ length: 6 }, () => event("contract.both.v1"));
1284
1261
  for (const e of events) await bus.publish(e);
1285
- expect8(named).toEqual(events.map((e) => e.id));
1286
- expect8(unnamed).toEqual(named);
1262
+ expect7(named).toEqual(events.map((e) => e.id));
1263
+ expect7(unnamed).toEqual(named);
1287
1264
  });
1288
1265
  },
1289
1266
  SLOW4
1290
1267
  );
1291
1268
  const second = opts.second;
1292
1269
  if (second === void 0) return;
1293
- describe7("two instances on the same streams", () => {
1270
+ describe6("two instances on the same streams", () => {
1294
1271
  const N = 12;
1295
- it8(
1272
+ it7(
1296
1273
  "one named subscription on both instances handles each of N events exactly once in total",
1297
1274
  async () => {
1298
1275
  const a = await make();
@@ -1312,13 +1289,13 @@ function eventBusDeliveryCases(make, opts) {
1312
1289
  await a.publish(event("contract.named.fence"));
1313
1290
  const ids2 = new Set(events.map((e) => e.id));
1314
1291
  const handled = [...byA, ...byB].filter((id) => ids2.has(id));
1315
- expect8(handled.length).toBe(events.length);
1316
- expect8([...handled].sort()).toEqual(events.map((e) => e.id).sort());
1292
+ expect7(handled.length).toBe(events.length);
1293
+ expect7([...handled].sort()).toEqual(events.map((e) => e.id).sort());
1317
1294
  });
1318
1295
  },
1319
1296
  SLOW4
1320
1297
  );
1321
- it8(
1298
+ it7(
1322
1299
  "an unnamed subscription on each instance receives all N events, in order, on both",
1323
1300
  async () => {
1324
1301
  const a = await make();
@@ -1328,19 +1305,22 @@ function eventBusDeliveryCases(make, opts) {
1328
1305
  const onB = [];
1329
1306
  await a.subscribe("contract.fanout.*", async (e) => void onA.push(e.id));
1330
1307
  await b.subscribe("contract.fanout.*", async (e) => void onB.push(e.id));
1331
- const events = Array.from({ length: N }, (_, seq) => event("contract.fanout.x", { seq }));
1308
+ const events = Array.from(
1309
+ { length: N },
1310
+ (_, seq) => event("contract.fanout.x", { seq })
1311
+ );
1332
1312
  for (const e of events) await a.publish(e);
1333
1313
  const fenceB = event("contract.fanout.fence");
1334
1314
  await b.publish(fenceB);
1335
1315
  const ids2 = events.map((e) => e.id);
1336
- expect8(onA.slice(0, N)).toEqual(ids2);
1337
- expect8(onB.slice(0, N)).toEqual(ids2);
1338
- expect8(onB[N]).toBe(fenceB.id);
1316
+ expect7(onA.slice(0, N)).toEqual(ids2);
1317
+ expect7(onB.slice(0, N)).toEqual(ids2);
1318
+ expect7(onB[N]).toBe(fenceB.id);
1339
1319
  });
1340
1320
  },
1341
1321
  SLOW4
1342
1322
  );
1343
- it8(
1323
+ it7(
1344
1324
  "replay on one instance reaches its own derived handlers and nothing on the other instance",
1345
1325
  async () => {
1346
1326
  const a = await make();
@@ -1364,10 +1344,10 @@ function eventBusDeliveryCases(make, opts) {
1364
1344
  await a.replay(replayed);
1365
1345
  const fence = event("contract.rebuild.fence");
1366
1346
  await b.publish(fence);
1367
- expect8(derivedA.slice(0, replayed.length)).toEqual(replayed.map((e) => e.id));
1347
+ expect7(derivedA.slice(0, replayed.length)).toEqual(replayed.map((e) => e.id));
1368
1348
  const ids2 = new Set(replayed.map((e) => e.id));
1369
- expect8(derivedB.filter((id) => ids2.has(id))).toEqual([]);
1370
- expect8(sideB).toEqual([fence.id]);
1349
+ expect7(derivedB.filter((id) => ids2.has(id))).toEqual([]);
1350
+ expect7(sideB).toEqual([fence.id]);
1371
1351
  });
1372
1352
  },
1373
1353
  SLOW4
@@ -1375,13 +1355,15 @@ function eventBusDeliveryCases(make, opts) {
1375
1355
  const timing = opts.timing;
1376
1356
  if (timing === void 0) return;
1377
1357
  const { claimIdleMs, ackTimeoutMs } = timing;
1378
- it8(
1358
+ it7(
1379
1359
  "groups left by a stopped instance (never unsubscribed) do not delay publish on the other instance",
1380
1360
  async () => {
1381
1361
  const a = await make();
1382
1362
  const b = await second();
1383
1363
  await closingAll([a], async () => {
1384
- await b.subscribe("contract.left.*", async () => void 0, { name: "contract-left-behind" });
1364
+ await b.subscribe("contract.left.*", async () => void 0, {
1365
+ name: "contract-left-behind"
1366
+ });
1385
1367
  await b.subscribe("contract.left.*", async () => void 0);
1386
1368
  await b.close();
1387
1369
  const handled = [];
@@ -1392,13 +1374,13 @@ function eventBusDeliveryCases(make, opts) {
1392
1374
  const startedAt = Date.now();
1393
1375
  await a.publish(e);
1394
1376
  const elapsed = Date.now() - startedAt;
1395
- expect8(handled).toEqual([e.id]);
1396
- expect8(elapsed).toBeLessThan(ackTimeoutMs / 2);
1377
+ expect7(handled).toEqual([e.id]);
1378
+ expect7(elapsed).toBeLessThan(ackTimeoutMs / 2);
1397
1379
  });
1398
1380
  },
1399
1381
  SLOW4
1400
1382
  );
1401
- it8(
1383
+ it7(
1402
1384
  "an entry pending on a stopped consumer of a named group is handled by the live one after claimIdleMs and before ackTimeoutMs",
1403
1385
  async () => {
1404
1386
  const a = await make();
@@ -1438,10 +1420,10 @@ function eventBusDeliveryCases(make, opts) {
1438
1420
  );
1439
1421
  const claimed = byA.find((h) => h.id === stranded.id);
1440
1422
  const idle = claimed.at - receivedByB;
1441
- expect8(idle, "taken over before claimIdleMs").toBeGreaterThanOrEqual(claimIdleMs - 50);
1442
- expect8(idle, "taken over only after ackTimeoutMs").toBeLessThan(ackTimeoutMs);
1443
- expect8(byA.filter((h) => h.id === stranded.id).length).toBe(1);
1444
- expect8(publishMs, "a publish on the group waited out the timeout").toBeLessThan(
1423
+ expect7(idle, "taken over before claimIdleMs").toBeGreaterThanOrEqual(claimIdleMs - 50);
1424
+ expect7(idle, "taken over only after ackTimeoutMs").toBeLessThan(ackTimeoutMs);
1425
+ expect7(byA.filter((h) => h.id === stranded.id).length).toBe(1);
1426
+ expect7(publishMs, "a publish on the group waited out the timeout").toBeLessThan(
1445
1427
  ackTimeoutMs
1446
1428
  );
1447
1429
  } finally {
@@ -1452,7 +1434,7 @@ function eventBusDeliveryCases(make, opts) {
1452
1434
  },
1453
1435
  SLOW4
1454
1436
  );
1455
- it8(
1437
+ it7(
1456
1438
  "an unnamed subscription unsubscribed while publish waits on it lets publish resolve well before ackTimeoutMs",
1457
1439
  async () => {
1458
1440
  const a = await make();
@@ -1471,8 +1453,8 @@ function eventBusDeliveryCases(make, opts) {
1471
1453
  const unsubscribedAt = Date.now();
1472
1454
  const unsubscribing = a.unsubscribe(sub);
1473
1455
  const outcome = await publishing;
1474
- expect8(typeof outcome, `publish rejected: ${String(outcome)}`).toBe("number");
1475
- expect8(outcome - unsubscribedAt).toBeLessThan(ackTimeoutMs / 2);
1456
+ expect7(typeof outcome, `publish rejected: ${String(outcome)}`).toBe("number");
1457
+ expect7(outcome - unsubscribedAt).toBeLessThan(ackTimeoutMs / 2);
1476
1458
  held.open();
1477
1459
  await unsubscribing;
1478
1460
  } finally {
@@ -1486,20 +1468,355 @@ function eventBusDeliveryCases(make, opts) {
1486
1468
  });
1487
1469
  }
1488
1470
 
1471
+ // src/event-bus-failure.ts
1472
+ import { randomUUID as randomUUID2 } from "crypto";
1473
+ import { describe as describe7, expect as expect8, it as it8 } from "vitest";
1474
+ var SLOW5 = 6e4;
1475
+ var SLOW_HANDLER_MS = 200;
1476
+ var IN_PROCESS_WINDOW_MS = 200;
1477
+ function event2(type, payload = {}) {
1478
+ return {
1479
+ id: `evt-${randomUUID2()}`,
1480
+ type,
1481
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1482
+ source: "contract-failure",
1483
+ correlationId: `corr-${randomUUID2()}`,
1484
+ payload,
1485
+ metadata: { storeId: "contract", environment: "development", version: "0.0.0" }
1486
+ };
1487
+ }
1488
+ var delay2 = (ms) => new Promise((r) => setTimeout(r, ms));
1489
+ async function until2(check, label, ms) {
1490
+ const deadline = Date.now() + ms;
1491
+ while (!check()) {
1492
+ if (Date.now() > deadline) throw new Error(`timed out after ${ms} ms waiting for ${label}`);
1493
+ await delay2(10);
1494
+ }
1495
+ }
1496
+ function gate2() {
1497
+ let open;
1498
+ const wait = new Promise((r) => {
1499
+ open = r;
1500
+ });
1501
+ return { wait, open };
1502
+ }
1503
+ async function closing2(buses, body) {
1504
+ try {
1505
+ await body();
1506
+ } finally {
1507
+ for (const bus of buses) await bus.close();
1508
+ }
1509
+ }
1510
+ async function settled(promise) {
1511
+ const started = Date.now();
1512
+ try {
1513
+ await promise;
1514
+ return { state: "resolved", error: void 0, at: Date.now(), ms: Date.now() - started };
1515
+ } catch (error) {
1516
+ return { state: "rejected", error, at: Date.now(), ms: Date.now() - started };
1517
+ }
1518
+ }
1519
+ function failure(outcome) {
1520
+ expect8(outcome.state, "publish resolved, so the failure was swallowed").toBe("rejected");
1521
+ const e = outcome.error;
1522
+ return {
1523
+ name: String(e.name),
1524
+ code: String(e.code),
1525
+ message: String(e.message),
1526
+ context: e.context ?? {}
1527
+ };
1528
+ }
1529
+ var failedNames = (f) => (f.context.failed ?? []).map((x) => x.subscription);
1530
+ function eventBusFailureCases(make, opts) {
1531
+ const window = opts.timing === void 0 ? IN_PROCESS_WINDOW_MS : 3 * opts.timing.claimIdleMs;
1532
+ describe7("reports handler failures", () => {
1533
+ it8(
1534
+ "a named handler that throws rejects publish with handler_failed naming it alone, only after the other named handlers (one of them slow) have each run once and finished; a throwing unnamed handler is named nowhere; replay past a throwing derived handler resolves",
1535
+ async () => {
1536
+ const bus = await make();
1537
+ await closing2([bus], async () => {
1538
+ const type = "contract.failure.v1";
1539
+ const ran = { a: 0, b: 0, c: 0 };
1540
+ const finished = { b: void 0, c: void 0 };
1541
+ await bus.subscribe(
1542
+ type,
1543
+ async () => {
1544
+ ran.a += 1;
1545
+ throw new Error("contract handler A refused");
1546
+ },
1547
+ { name: "contract-failure-a" }
1548
+ );
1549
+ await bus.subscribe(
1550
+ type,
1551
+ async () => {
1552
+ ran.b += 1;
1553
+ await delay2(SLOW_HANDLER_MS);
1554
+ finished.b = Date.now();
1555
+ },
1556
+ { name: "contract-failure-b" }
1557
+ );
1558
+ await bus.subscribe(
1559
+ type,
1560
+ async () => {
1561
+ ran.c += 1;
1562
+ finished.c = Date.now();
1563
+ },
1564
+ { name: "contract-failure-c" }
1565
+ );
1566
+ await bus.subscribe(type, async () => {
1567
+ throw new Error("contract admin tab refused");
1568
+ });
1569
+ const e = event2(type);
1570
+ const outcome = await settled(bus.publish(e));
1571
+ expect8(ran).toEqual({ a: 1, b: 1, c: 1 });
1572
+ expect8(finished.b, "the slow handler finished before the rejection").toBeLessThanOrEqual(
1573
+ outcome.at
1574
+ );
1575
+ expect8(finished.c).toBeLessThanOrEqual(outcome.at);
1576
+ expect8(outcome.ms).toBeGreaterThanOrEqual(SLOW_HANDLER_MS - 10);
1577
+ const rebuilt = [];
1578
+ await bus.subscribe(
1579
+ "contract.failure-replay.v1",
1580
+ async () => {
1581
+ throw new Error("contract projection refused");
1582
+ },
1583
+ { name: "contract-failure-projection-a", derived: true }
1584
+ );
1585
+ await bus.subscribe("contract.failure-replay.v1", async (r) => void rebuilt.push(r.id), {
1586
+ name: "contract-failure-projection-b",
1587
+ derived: true
1588
+ });
1589
+ const replayed = [
1590
+ event2("contract.failure-replay.v1"),
1591
+ event2("contract.failure-replay.v1")
1592
+ ];
1593
+ const replay = await settled(bus.replay(replayed));
1594
+ expect8(replay.state).toBe("resolved");
1595
+ expect8(rebuilt).toEqual(replayed.map((r) => r.id));
1596
+ const f = failure(outcome);
1597
+ expect8([f.name, f.code]).toEqual(["AdapterError", "adapter.handler_failed"]);
1598
+ expect8([f.context.eventId, f.context.eventType]).toEqual([e.id, e.type]);
1599
+ expect8(failedNames(f)).toEqual(["contract-failure-a"]);
1600
+ expect8(f.context.failed?.[0]?.message).toContain("contract handler A refused");
1601
+ });
1602
+ },
1603
+ SLOW5
1604
+ );
1605
+ it8(
1606
+ "publishBatch of three events whose second fails a named handler hands all three, in order, to every subscription and only then rejects with handler_failed naming that event and subscription",
1607
+ async () => {
1608
+ const bus = await make();
1609
+ await closing2([bus], async () => {
1610
+ const type = "contract.failure-batch.v1";
1611
+ const picky = [];
1612
+ const other = [];
1613
+ const unnamed = [];
1614
+ await bus.subscribe(
1615
+ type,
1616
+ async (e) => {
1617
+ picky.push(e.id);
1618
+ if (e.payload.seq === 1)
1619
+ throw new Error("contract picky refused");
1620
+ },
1621
+ { name: "contract-failure-picky" }
1622
+ );
1623
+ await bus.subscribe(
1624
+ type,
1625
+ async (e) => {
1626
+ await delay2(50);
1627
+ other.push(e.id);
1628
+ },
1629
+ { name: "contract-failure-other" }
1630
+ );
1631
+ await bus.subscribe(type, async (e) => {
1632
+ await delay2(50);
1633
+ unnamed.push(e.id);
1634
+ });
1635
+ const events = [0, 1, 2].map((seq) => event2(type, { seq }));
1636
+ const seenAtRejection = [];
1637
+ const outcome = await settled(
1638
+ bus.publishBatch(events).catch((error) => {
1639
+ seenAtRejection.push([...picky], [...other], [...unnamed]);
1640
+ throw error;
1641
+ })
1642
+ );
1643
+ const f = failure(outcome);
1644
+ expect8(f.code).toBe("adapter.handler_failed");
1645
+ expect8(f.context.eventId).toBe(events[1].id);
1646
+ expect8(failedNames(f)).toEqual(["contract-failure-picky"]);
1647
+ const ids2 = events.map((e) => e.id);
1648
+ expect8(
1649
+ seenAtRejection,
1650
+ "what each subscription had been handed when the batch rejected"
1651
+ ).toEqual([ids2, ids2, ids2]);
1652
+ });
1653
+ },
1654
+ SLOW5
1655
+ );
1656
+ it8(
1657
+ "publishing a rejected event again reaches every subscription again and resolves once the failed handler passes; in the window after the rejection the bus ran the failed handler only once per publish",
1658
+ async () => {
1659
+ const bus = await make();
1660
+ await closing2([bus], async () => {
1661
+ const type = "contract.failure-again.v1";
1662
+ const ran = { a: 0, b: 0, c: 0, unnamed: 0 };
1663
+ await bus.subscribe(
1664
+ type,
1665
+ async () => {
1666
+ ran.a += 1;
1667
+ if (ran.a === 1) throw new Error("contract handler A refused once");
1668
+ },
1669
+ { name: "contract-again-a" }
1670
+ );
1671
+ await bus.subscribe(type, async () => void (ran.b += 1), { name: "contract-again-b" });
1672
+ await bus.subscribe(type, async () => void (ran.c += 1), { name: "contract-again-c" });
1673
+ await bus.subscribe(type, async () => void (ran.unnamed += 1));
1674
+ const e = event2(type);
1675
+ const first = await settled(bus.publish(e));
1676
+ expect8(failure(first).code).toBe("adapter.handler_failed");
1677
+ await delay2(window);
1678
+ const afterWindow = { ...ran };
1679
+ expect8(afterWindow.a, "the failed handler ran again without a publish").toBe(1);
1680
+ const second2 = await settled(bus.publish(e));
1681
+ expect8(second2.state, String(second2.error)).toBe("resolved");
1682
+ expect8(ran.a).toBe(2);
1683
+ expect8(ran.b).toBe(2);
1684
+ expect8(ran.c).toBe(ran.b);
1685
+ expect8(ran.unnamed).toBe(ran.b);
1686
+ expect8(ran.b - afterWindow.b, "the second publish reached B again").toBe(1);
1687
+ });
1688
+ },
1689
+ SLOW5
1690
+ );
1691
+ const second = opts.second;
1692
+ const timing = opts.timing;
1693
+ if (second === void 0 || timing === void 0) return;
1694
+ it8(
1695
+ "a named handler that throws on another instance fails the publish on this one with handler_failed, not publish_timeout, well within ackTimeoutMs",
1696
+ async () => {
1697
+ const a = await make();
1698
+ const b = await second();
1699
+ const held = gate2();
1700
+ let holding;
1701
+ try {
1702
+ const group = "contract-remote";
1703
+ const hold = "contract.remote.hold";
1704
+ const target = "contract.remote.target";
1705
+ const ranOn = {};
1706
+ const note = (id, where) => (ranOn[id] ??= []).push(where);
1707
+ let aHolding = false;
1708
+ await a.subscribe(
1709
+ "contract.remote.*",
1710
+ async (e2) => {
1711
+ note(e2.id, "a");
1712
+ if (e2.type === hold) {
1713
+ aHolding = true;
1714
+ await held.wait;
1715
+ }
1716
+ },
1717
+ { name: group }
1718
+ );
1719
+ holding = settled(a.publish(event2(hold)));
1720
+ await until2(() => aHolding, "a to take the holding entry", timing.ackTimeoutMs);
1721
+ await b.subscribe(
1722
+ "contract.remote.*",
1723
+ async (e2) => {
1724
+ if (e2.type === hold) return;
1725
+ note(e2.id, "b");
1726
+ throw new Error("contract remote handler refused");
1727
+ },
1728
+ { name: group }
1729
+ );
1730
+ const e = event2(target);
1731
+ const outcome = await settled(a.publish(e));
1732
+ expect8(ranOn[e.id], "the failing handler ran on the second instance only").toEqual(["b"]);
1733
+ const f = failure(outcome);
1734
+ expect8(f.code, f.message).toBe("adapter.handler_failed");
1735
+ expect8(failedNames(f)).toEqual([group]);
1736
+ expect8(outcome.ms).toBeLessThan(timing.ackTimeoutMs / 2);
1737
+ } finally {
1738
+ held.open();
1739
+ await holding;
1740
+ await a.close();
1741
+ await b.close();
1742
+ }
1743
+ },
1744
+ SLOW5
1745
+ );
1746
+ it8(
1747
+ "a named handler that fails once on another instance, then passes on the second publish within ackTimeoutMs: the first publish rejects and the second resolves, and the handler ran twice",
1748
+ async () => {
1749
+ const a = await make();
1750
+ const b = await second();
1751
+ const held = gate2();
1752
+ let holding;
1753
+ try {
1754
+ const group = "contract-remote-once";
1755
+ const hold = "contract.remote-once.hold";
1756
+ const target = "contract.remote-once.target";
1757
+ const ranOn = {};
1758
+ const note = (id, where) => (ranOn[id] ??= []).push(where);
1759
+ let aHolding = false;
1760
+ await a.subscribe(
1761
+ "contract.remote-once.*",
1762
+ async (e2) => {
1763
+ note(e2.id, "a");
1764
+ if (e2.type === hold) {
1765
+ aHolding = true;
1766
+ await held.wait;
1767
+ }
1768
+ },
1769
+ { name: group }
1770
+ );
1771
+ holding = settled(a.publish(event2(hold)));
1772
+ await until2(() => aHolding, "a to take the holding entry", timing.ackTimeoutMs);
1773
+ let runs = 0;
1774
+ await b.subscribe(
1775
+ "contract.remote-once.*",
1776
+ async (e2) => {
1777
+ if (e2.type === hold) return;
1778
+ note(e2.id, "b");
1779
+ runs += 1;
1780
+ if (runs === 1) throw new Error("contract remote handler refused once");
1781
+ },
1782
+ { name: group }
1783
+ );
1784
+ const e = event2(target);
1785
+ const first = await settled(a.publish(e));
1786
+ const again = await settled(a.publish(e));
1787
+ expect8(ranOn[e.id], "the handler ran on the second instance, once per publish").toEqual([
1788
+ "b",
1789
+ "b"
1790
+ ]);
1791
+ expect8(failure(first).code).toBe("adapter.handler_failed");
1792
+ expect8(again.at - first.at).toBeLessThan(timing.ackTimeoutMs);
1793
+ expect8(again.state, String(again.error)).toBe("resolved");
1794
+ } finally {
1795
+ held.open();
1796
+ await holding;
1797
+ await a.close();
1798
+ await b.close();
1799
+ }
1800
+ },
1801
+ SLOW5
1802
+ );
1803
+ });
1804
+ }
1805
+
1489
1806
  // src/event-bus.ts
1490
- var SLOW5 = 3e4;
1807
+ var SLOW6 = 3e4;
1491
1808
  function makeEvent(type, payload = {}) {
1492
1809
  return {
1493
- id: `evt-${randomUUID2()}`,
1810
+ id: `evt-${randomUUID3()}`,
1494
1811
  type,
1495
1812
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1496
1813
  source: "contract-tests",
1497
- correlationId: `corr-${randomUUID2()}`,
1814
+ correlationId: `corr-${randomUUID3()}`,
1498
1815
  payload,
1499
1816
  metadata: { storeId: "test", environment: "development", version: "0.0.0" }
1500
1817
  };
1501
1818
  }
1502
- var delay2 = (ms) => new Promise((r) => setTimeout(r, ms));
1819
+ var delay3 = (ms) => new Promise((r) => setTimeout(r, ms));
1503
1820
  async function using3(buses, body) {
1504
1821
  try {
1505
1822
  await body();
@@ -1509,96 +1826,121 @@ async function using3(buses, body) {
1509
1826
  }
1510
1827
  function runEventBusContract(name, make, opts = {}) {
1511
1828
  describe8(`EventBusAdapter contract: ${name}`, () => {
1512
- it9("publish resolves after the matching handler has finished, and never calls a non-matching one", async () => {
1513
- const bus = await make();
1514
- await using3([bus], async () => {
1515
- const received = [];
1516
- const other = [];
1517
- await bus.subscribe("test.event.v1", async (e) => {
1518
- await delay2(50);
1519
- received.push(e.payload);
1829
+ it9(
1830
+ "publish resolves after the matching handler has finished, and never calls a non-matching one",
1831
+ async () => {
1832
+ const bus = await make();
1833
+ await using3([bus], async () => {
1834
+ const received = [];
1835
+ const other = [];
1836
+ await bus.subscribe("test.event.v1", async (e) => {
1837
+ await delay3(50);
1838
+ received.push(e.payload);
1839
+ });
1840
+ await bus.subscribe("test.other.v1", async (e) => void other.push(e));
1841
+ await bus.publish(makeEvent("test.event.v1", { x: 1 }));
1842
+ expect9(received).toEqual([{ x: 1 }]);
1843
+ expect9(other).toEqual([]);
1520
1844
  });
1521
- await bus.subscribe("test.other.v1", async (e) => void other.push(e));
1522
- await bus.publish(makeEvent("test.event.v1", { x: 1 }));
1523
- expect9(received).toEqual([{ x: 1 }]);
1524
- expect9(other).toEqual([]);
1525
- });
1526
- }, CASE_MS);
1527
- it9("a handler that throws leaves publish resolved and every other handler called", async () => {
1528
- const bus = await make();
1529
- await using3([bus], async () => {
1530
- const results = [];
1531
- await bus.subscribe("error.test.v1", async () => {
1532
- throw new Error("boom");
1845
+ },
1846
+ CASE_MS
1847
+ );
1848
+ it9(
1849
+ "a handler that throws leaves publish resolved and every other handler called",
1850
+ async () => {
1851
+ const bus = await make();
1852
+ await using3([bus], async () => {
1853
+ const results = [];
1854
+ await bus.subscribe("error.test.v1", async () => {
1855
+ throw new Error("boom");
1856
+ });
1857
+ await bus.subscribe("error.test.v1", async () => void results.push("ok"));
1858
+ await expect9(bus.publish(makeEvent("error.test.v1"))).resolves.toBeUndefined();
1859
+ expect9(results).toEqual(["ok"]);
1533
1860
  });
1534
- await bus.subscribe("error.test.v1", async () => void results.push("ok"));
1535
- await expect9(bus.publish(makeEvent("error.test.v1"))).resolves.toBeUndefined();
1536
- expect9(results).toEqual(["ok"]);
1537
- });
1538
- }, CASE_MS);
1539
- it9("replay reaches derived subscriptions only", async () => {
1540
- const bus = await make();
1541
- await using3([bus], async () => {
1542
- const derived = [];
1543
- const sideEffect = [];
1544
- await bus.subscribe("replay.test.v1", async (e) => void derived.push(e.id), {
1545
- name: "contract-projection",
1546
- derived: true
1861
+ },
1862
+ CASE_MS
1863
+ );
1864
+ it9(
1865
+ "replay reaches derived subscriptions only",
1866
+ async () => {
1867
+ const bus = await make();
1868
+ await using3([bus], async () => {
1869
+ const derived = [];
1870
+ const sideEffect = [];
1871
+ await bus.subscribe("replay.test.v1", async (e) => void derived.push(e.id), {
1872
+ name: "contract-projection",
1873
+ derived: true
1874
+ });
1875
+ await bus.subscribe("replay.test.v1", async (e) => void sideEffect.push(e.id), {
1876
+ name: "contract-side-effect"
1877
+ });
1878
+ const events = [makeEvent("replay.test.v1"), makeEvent("replay.test.v1")];
1879
+ await bus.replay(events);
1880
+ expect9(derived).toEqual(events.map((e) => e.id));
1881
+ expect9(sideEffect).toEqual([]);
1547
1882
  });
1548
- await bus.subscribe("replay.test.v1", async (e) => void sideEffect.push(e.id), {
1549
- name: "contract-side-effect"
1883
+ },
1884
+ CASE_MS
1885
+ );
1886
+ it9(
1887
+ "publishBatch reaches each subscription in batch order, through wildcards and pattern arrays",
1888
+ async () => {
1889
+ const bus = await make();
1890
+ await using3([bus], async () => {
1891
+ const wildcard = [];
1892
+ const listed = [];
1893
+ await bus.subscribe("batch.*", async (e) => void wildcard.push(e.id));
1894
+ await bus.subscribe(["batch.a.v1", "batch.c.v1"], async (e) => void listed.push(e.id), {
1895
+ name: "contract-listed"
1896
+ });
1897
+ const events = ["batch.a.v1", "batch.b.v1", "batch.c.v1", "batch.a.v1"].map(
1898
+ (t, seq) => makeEvent(t, { seq })
1899
+ );
1900
+ await bus.publishBatch(events);
1901
+ expect9(wildcard).toEqual(events.map((e) => e.id));
1902
+ expect9(listed).toEqual(events.filter((e) => e.type !== "batch.b.v1").map((e) => e.id));
1550
1903
  });
1551
- const events = [makeEvent("replay.test.v1"), makeEvent("replay.test.v1")];
1552
- await bus.replay(events);
1553
- expect9(derived).toEqual(events.map((e) => e.id));
1554
- expect9(sideEffect).toEqual([]);
1555
- });
1556
- }, CASE_MS);
1557
- it9("publishBatch reaches each subscription in batch order, through wildcards and pattern arrays", async () => {
1558
- const bus = await make();
1559
- await using3([bus], async () => {
1560
- const wildcard = [];
1561
- const listed = [];
1562
- await bus.subscribe("batch.*", async (e) => void wildcard.push(e.id));
1563
- await bus.subscribe(["batch.a.v1", "batch.c.v1"], async (e) => void listed.push(e.id), {
1564
- name: "contract-listed"
1904
+ },
1905
+ CASE_MS
1906
+ );
1907
+ it9(
1908
+ "an unsubscribed handler receives nothing more",
1909
+ async () => {
1910
+ const bus = await make();
1911
+ await using3([bus], async () => {
1912
+ const received = [];
1913
+ const sub = await bus.subscribe("unsub.event.v1", async (e) => void received.push(e.id));
1914
+ const before = makeEvent("unsub.event.v1");
1915
+ await bus.publish(before);
1916
+ await bus.unsubscribe(sub);
1917
+ await bus.publish(makeEvent("unsub.event.v1"));
1918
+ expect9(received).toEqual([before.id]);
1565
1919
  });
1566
- const events = ["batch.a.v1", "batch.b.v1", "batch.c.v1", "batch.a.v1"].map(
1567
- (t, seq) => makeEvent(t, { seq })
1568
- );
1569
- await bus.publishBatch(events);
1570
- expect9(wildcard).toEqual(events.map((e) => e.id));
1571
- expect9(listed).toEqual(events.filter((e) => e.type !== "batch.b.v1").map((e) => e.id));
1572
- });
1573
- }, CASE_MS);
1574
- it9("an unsubscribed handler receives nothing more", async () => {
1575
- const bus = await make();
1576
- await using3([bus], async () => {
1577
- const received = [];
1578
- const sub = await bus.subscribe("unsub.event.v1", async (e) => void received.push(e.id));
1579
- const before = makeEvent("unsub.event.v1");
1580
- await bus.publish(before);
1581
- await bus.unsubscribe(sub);
1582
- await bus.publish(makeEvent("unsub.event.v1"));
1583
- expect9(received).toEqual([before.id]);
1584
- });
1585
- }, CASE_MS);
1586
- it9("on one instance, a named and an unnamed subscription each handle every event once", async () => {
1587
- const bus = await make();
1588
- await using3([bus], async () => {
1589
- const named = [];
1590
- const unnamed = [];
1591
- await bus.subscribe("both.event.v1", async (e) => void named.push(e.id), {
1592
- name: "contract-named"
1920
+ },
1921
+ CASE_MS
1922
+ );
1923
+ it9(
1924
+ "on one instance, a named and an unnamed subscription each handle every event once",
1925
+ async () => {
1926
+ const bus = await make();
1927
+ await using3([bus], async () => {
1928
+ const named = [];
1929
+ const unnamed = [];
1930
+ await bus.subscribe("both.event.v1", async (e) => void named.push(e.id), {
1931
+ name: "contract-named"
1932
+ });
1933
+ await bus.subscribe("both.event.v1", async (e) => void unnamed.push(e.id));
1934
+ const events = [makeEvent("both.event.v1"), makeEvent("both.event.v1")];
1935
+ for (const e of events) await bus.publish(e);
1936
+ expect9(named).toEqual(events.map((e) => e.id));
1937
+ expect9(unnamed).toEqual(named);
1593
1938
  });
1594
- await bus.subscribe("both.event.v1", async (e) => void unnamed.push(e.id));
1595
- const events = [makeEvent("both.event.v1"), makeEvent("both.event.v1")];
1596
- for (const e of events) await bus.publish(e);
1597
- expect9(named).toEqual(events.map((e) => e.id));
1598
- expect9(unnamed).toEqual(named);
1599
- });
1600
- }, CASE_MS);
1939
+ },
1940
+ CASE_MS
1941
+ );
1601
1942
  eventBusDeliveryCases(make, opts);
1943
+ eventBusFailureCases(make, opts);
1602
1944
  healthAndDowntime(
1603
1945
  make,
1604
1946
  opts.unreachable,
@@ -1636,7 +1978,7 @@ function runEventBusContract(name, make, opts = {}) {
1636
1978
  expect9(onB.filter((id) => ids2.includes(id))).toEqual(ids2);
1637
1979
  });
1638
1980
  },
1639
- SLOW5
1981
+ SLOW6
1640
1982
  );
1641
1983
  });
1642
1984
  }
@@ -1685,7 +2027,7 @@ function runMediaStorageContract(name, make, options) {
1685
2027
  }
1686
2028
 
1687
2029
  // src/payments.ts
1688
- import { randomUUID as randomUUID3 } from "crypto";
2030
+ import { randomUUID as randomUUID4 } from "crypto";
1689
2031
  import { describe as describe10, expect as expect11, it as it11 } from "vitest";
1690
2032
  var TOTAL = 1999;
1691
2033
  async function rejectionCode(call) {
@@ -1703,7 +2045,7 @@ function runPaymentsContract(name, build, options = {}) {
1703
2045
  const intent = await adapter.createPaymentIntent({
1704
2046
  amountMinor: 1e3,
1705
2047
  currency: "GBP",
1706
- orderRef: `contract-${randomUUID3()}`
2048
+ orderRef: `contract-${randomUUID4()}`
1707
2049
  });
1708
2050
  expect11(intent.id).toBeTruthy();
1709
2051
  expect11(intent.clientSecret).toBeTruthy();
@@ -1738,11 +2080,11 @@ function runPaymentsContract(name, build, options = {}) {
1738
2080
  const intent = await adapter.createPaymentIntent({
1739
2081
  amountMinor: TOTAL,
1740
2082
  currency: "GBP",
1741
- orderRef: `contract-${randomUUID3()}`
2083
+ orderRef: `contract-${randomUUID4()}`
1742
2084
  });
1743
2085
  if (intent.status !== "succeeded") await adapter.capturePayment(intent.id);
1744
2086
  const half = Math.floor(TOTAL / 2);
1745
- const key = `contract-refund-${randomUUID3()}`;
2087
+ const key = `contract-refund-${randomUUID4()}`;
1746
2088
  const first = await adapter.refund(intent.id, half, "requested_by_customer", {
1747
2089
  idempotencyKey: key
1748
2090
  });
@@ -1788,11 +2130,11 @@ function runPaymentsContract(name, build, options = {}) {
1788
2130
  }
1789
2131
 
1790
2132
  // src/reviews.ts
1791
- import { randomUUID as randomUUID4 } from "crypto";
2133
+ import { randomUUID as randomUUID5 } from "crypto";
1792
2134
  import fc4 from "fast-check";
1793
2135
  import { describe as describe11, expect as expect12, it as it12 } from "vitest";
1794
2136
  async function seed(adapter, count) {
1795
- const productId = `contract-reviews-${randomUUID4()}`;
2137
+ const productId = `contract-reviews-${randomUUID5()}`;
1796
2138
  const seeded = [];
1797
2139
  for (let i = 0; i < count; i++) {
1798
2140
  const { review } = await adapter.submitReview({
@@ -1827,7 +2169,7 @@ function runReviewsContract(name, build, options = {}) {
1827
2169
  "submitReview returns the created review",
1828
2170
  async () => {
1829
2171
  const adapter = await build();
1830
- const productId = `contract-reviews-${randomUUID4()}`;
2172
+ const productId = `contract-reviews-${randomUUID5()}`;
1831
2173
  const { review } = await adapter.submitReview({
1832
2174
  productId,
1833
2175
  customerId: null,
@@ -1908,13 +2250,13 @@ function runReviewsContract(name, build, options = {}) {
1908
2250
  build,
1909
2251
  options.unreachable,
1910
2252
  "getProductReviews()",
1911
- (adapter) => adapter.getProductReviews(`contract-reviews-${randomUUID4()}`, { limit: 1 })
2253
+ (adapter) => adapter.getProductReviews(`contract-reviews-${randomUUID5()}`, { limit: 1 })
1912
2254
  );
1913
2255
  });
1914
2256
  }
1915
2257
 
1916
2258
  // src/search.ts
1917
- import { randomUUID as randomUUID5 } from "crypto";
2259
+ import { randomUUID as randomUUID6 } from "crypto";
1918
2260
  import fc5 from "fast-check";
1919
2261
  import { describe as describe12, expect as expect13, it as it13 } from "vitest";
1920
2262
 
@@ -1958,7 +2300,7 @@ function tierOf(p, query) {
1958
2300
 
1959
2301
  // src/search.ts
1960
2302
  function runToken() {
1961
- return `t${randomUUID5().replace(/-/g, "").slice(0, 15)}`;
2303
+ return `t${randomUUID6().replace(/-/g, "").slice(0, 15)}`;
1962
2304
  }
1963
2305
  var word = fc5.string({
1964
2306
  unit: fc5.constantFrom("a", "b", "c", "d", "1", "2"),
@@ -2029,7 +2371,7 @@ function expectExactWalk(pages, expected, limit, label) {
2029
2371
  expect13(page.items.length, `${label}: page ${i + 1} is empty`).toBeGreaterThan(0);
2030
2372
  }
2031
2373
  }
2032
- var SLOW6 = 18e4;
2374
+ var SLOW7 = 18e4;
2033
2375
  function runSearchContract(name, build, options = {}) {
2034
2376
  const numRuns = options.numRuns ?? 25;
2035
2377
  describe12(`SearchAdapter contract: ${name}`, () => {
@@ -2062,7 +2404,7 @@ function runSearchContract(name, build, options = {}) {
2062
2404
  "remove deletes documents from index",
2063
2405
  async () => {
2064
2406
  const adapter = await build();
2065
- const tag = `remove-${randomUUID5()}`;
2407
+ const tag = `remove-${randomUUID6()}`;
2066
2408
  await adapter.index([{ id: tag, type: "product", fields: { name: "To Remove" } }]);
2067
2409
  await adapter.remove([tag]);
2068
2410
  const result = await adapter.search("To Remove");
@@ -2161,7 +2503,7 @@ function runSearchContract(name, build, options = {}) {
2161
2503
  { numRuns }
2162
2504
  );
2163
2505
  },
2164
- SLOW6
2506
+ SLOW7
2165
2507
  );
2166
2508
  if (options.ranking) {
2167
2509
  it13(
@@ -2226,7 +2568,7 @@ function runSearchContract(name, build, options = {}) {
2226
2568
  { numRuns }
2227
2569
  );
2228
2570
  },
2229
- SLOW6
2571
+ SLOW7
2230
2572
  );
2231
2573
  }
2232
2574
  if (options.facets) {
@@ -2276,7 +2618,7 @@ function runSearchContract(name, build, options = {}) {
2276
2618
  { numRuns }
2277
2619
  );
2278
2620
  },
2279
- SLOW6
2621
+ SLOW7
2280
2622
  );
2281
2623
  }
2282
2624
  describe12("hostile input", () => {
@@ -2355,7 +2697,7 @@ function runSearchContract(name, build, options = {}) {
2355
2697
  }
2356
2698
  );
2357
2699
  },
2358
- SLOW6
2700
+ SLOW7
2359
2701
  );
2360
2702
  it13(
2361
2703
  "input with no terms returns no items, total 0 and no next page, over an index that holds products",
@@ -2395,7 +2737,7 @@ function runSearchContract(name, build, options = {}) {
2395
2737
  await adapter.remove(products.map((p) => p.id));
2396
2738
  }
2397
2739
  },
2398
- SLOW6
2740
+ SLOW7
2399
2741
  );
2400
2742
  it13(
2401
2743
  "a malformed cursor (undecodable, negative, non-integer) rejects, and a cursor at or past total, as after a removal between two pages, is an empty page with nextCursor null",
@@ -2449,7 +2791,7 @@ function runSearchContract(name, build, options = {}) {
2449
2791
  await adapter.remove(products.map((p) => p.id));
2450
2792
  }
2451
2793
  },
2452
- SLOW6
2794
+ SLOW7
2453
2795
  );
2454
2796
  });
2455
2797
  });
@@ -2542,8 +2884,72 @@ function adapterContext(slot, config, env = {}) {
2542
2884
  return { slot, config, logger: silent, env };
2543
2885
  }
2544
2886
 
2545
- // src/security.ts
2887
+ // src/webhook.ts
2546
2888
  import { describe as describe15, expect as expect16, it as it16 } from "vitest";
2889
+ import {
2890
+ webhookDeliveryProblem
2891
+ } from "@nymbalhq/types";
2892
+ function receiving(adapter) {
2893
+ expect16(typeof adapter.receiveWebhook, "the adapter has no receiveWebhook").toBe("function");
2894
+ return adapter;
2895
+ }
2896
+ async function receive(adapter, request) {
2897
+ return receiving(adapter).receiveWebhook(request.rawBody, request.headers);
2898
+ }
2899
+ function runWebhookContract(name, build, options) {
2900
+ describe15(`webhook contract: ${name}`, () => {
2901
+ it16(
2902
+ "idempotency: one delivery received twice gives one deliveryId and equal events",
2903
+ () => using(build, async (adapter) => {
2904
+ const first = await receive(adapter, options.delivery());
2905
+ const second = await receive(adapter, options.delivery());
2906
+ expect16(first.verified, "the signed delivery was not verified").toBe(true);
2907
+ expect16(typeof first.deliveryId === "string" && first.deliveryId !== "").toBe(true);
2908
+ expect16(second.deliveryId, "the same delivery got another deliveryId").toBe(
2909
+ first.deliveryId
2910
+ );
2911
+ expect16(second.events).toEqual(first.events);
2912
+ }),
2913
+ CASE_MS
2914
+ );
2915
+ const tampered = options.tampered;
2916
+ if (tampered !== void 0) {
2917
+ it16(
2918
+ "verification: a tampered delivery is not verified",
2919
+ () => using(build, async (adapter) => {
2920
+ const outcome = await receive(adapter, tampered()).then(
2921
+ (delivery) => ({ verified: delivery.verified, code: null }),
2922
+ (err) => ({
2923
+ verified: false,
2924
+ code: err?.code
2925
+ })
2926
+ );
2927
+ expect16(outcome.verified, "a tampered delivery was verified").toBe(false);
2928
+ if (outcome.code !== null) expect16(String(outcome.code)).toMatch(/webhook_invalid$/);
2929
+ }),
2930
+ CASE_MS
2931
+ );
2932
+ }
2933
+ it16(
2934
+ "namespace: every event is the adapter's own, declared and versioned",
2935
+ () => using(build, async (adapter) => {
2936
+ const delivery = await receive(adapter, options.delivery());
2937
+ expect16(delivery.verified, "the signed delivery was not verified").toBe(true);
2938
+ expect16(delivery.events.length, "the delivery carried no event to check").toBeGreaterThan(
2939
+ 0
2940
+ );
2941
+ expect16(
2942
+ webhookDeliveryProblem(adapter.providerName, adapter.producesEvents, delivery),
2943
+ "the kernel would refuse this delivery"
2944
+ ).toBeNull();
2945
+ }),
2946
+ CASE_MS
2947
+ );
2948
+ });
2949
+ }
2950
+
2951
+ // src/security.ts
2952
+ import { describe as describe16, expect as expect17, it as it17 } from "vitest";
2547
2953
  var testConfig = {
2548
2954
  rateLimit: {
2549
2955
  api: { requests: 100, window: "1m", action: "block" }
@@ -2557,24 +2963,24 @@ var testConfig = {
2557
2963
  }
2558
2964
  };
2559
2965
  function runSecurityContract(name, build, options = {}) {
2560
- describe15(`SecurityAdapter contract: ${name}`, () => {
2561
- it16(
2966
+ describe16(`SecurityAdapter contract: ${name}`, () => {
2967
+ it17(
2562
2968
  "validate accepts a valid config",
2563
2969
  async () => {
2564
2970
  const adapter = await build();
2565
2971
  const result = await adapter.validate(testConfig);
2566
- expect16(typeof result.valid).toBe("boolean");
2567
- expect16(Array.isArray(result.errors)).toBe(true);
2972
+ expect17(typeof result.valid).toBe("boolean");
2973
+ expect17(Array.isArray(result.errors)).toBe(true);
2568
2974
  },
2569
2975
  CASE_MS
2570
2976
  );
2571
- it16(
2977
+ it17(
2572
2978
  "validate of empty config reports errors",
2573
2979
  async () => {
2574
2980
  const adapter = await build();
2575
2981
  const result = await adapter.validate({});
2576
2982
  if (!result.valid) {
2577
- expect16(result.errors.length).toBeGreaterThan(0);
2983
+ expect17(result.errors.length).toBeGreaterThan(0);
2578
2984
  }
2579
2985
  },
2580
2986
  CASE_MS
@@ -2589,7 +2995,7 @@ function runSecurityContract(name, build, options = {}) {
2589
2995
  }
2590
2996
 
2591
2997
  // src/projection.ts
2592
- import { describe as describe16, it as it17, expect as expect17 } from "vitest";
2998
+ import { describe as describe17, it as it18, expect as expect18 } from "vitest";
2593
2999
  function makeEvent2(id) {
2594
3000
  return {
2595
3001
  id,
@@ -2602,26 +3008,26 @@ function makeEvent2(id) {
2602
3008
  };
2603
3009
  }
2604
3010
  function runProjectionContract(opts) {
2605
- describe16("Projection idempotency contract", () => {
2606
- it17("processing the same event twice applies the mutation only once", async () => {
3011
+ describe17("Projection idempotency contract", () => {
3012
+ it18("processing the same event twice applies the mutation only once", async () => {
2607
3013
  const ctx = await opts.build();
2608
- const event2 = makeEvent2("dedup-evt-001");
2609
- await ctx.handle(event2);
2610
- await ctx.handle(event2);
2611
- expect17(await ctx.applicationCount()).toBe(1);
3014
+ const event3 = makeEvent2("dedup-evt-001");
3015
+ await ctx.handle(event3);
3016
+ await ctx.handle(event3);
3017
+ expect18(await ctx.applicationCount()).toBe(1);
2612
3018
  });
2613
- it17("distinct events each apply their mutation", async () => {
3019
+ it18("distinct events each apply their mutation", async () => {
2614
3020
  const ctx = await opts.build();
2615
3021
  await ctx.handle(makeEvent2("distinct-evt-001"));
2616
3022
  await ctx.handle(makeEvent2("distinct-evt-002"));
2617
- expect17(await ctx.applicationCount()).toBe(2);
3023
+ expect18(await ctx.applicationCount()).toBe(2);
2618
3024
  });
2619
- it17("after reprocessing a batch the count equals unique event count", async () => {
3025
+ it18("after reprocessing a batch the count equals unique event count", async () => {
2620
3026
  const ctx = await opts.build();
2621
3027
  const events = [makeEvent2("batch-001"), makeEvent2("batch-002"), makeEvent2("batch-003")];
2622
3028
  for (const e of events) await ctx.handle(e);
2623
3029
  for (const e of events) await ctx.handle(e);
2624
- expect17(await ctx.applicationCount()).toBe(3);
3030
+ expect18(await ctx.applicationCount()).toBe(3);
2625
3031
  });
2626
3032
  });
2627
3033
  }
@@ -2640,13 +3046,16 @@ var suites = {
2640
3046
  analytics: runAnalyticsContract,
2641
3047
  shipping: runShippingContract,
2642
3048
  tax: runTaxContract,
2643
- ai: runAiContract,
2644
3049
  documentStore: runDocumentStoreContract,
2645
3050
  eventBus: runEventBusContract,
2646
- media: (name, build, options = {}) => runMediaStorageContract(name, build, {
2647
- fetchUrl: fetchOverHttp,
2648
- ...options
2649
- })
3051
+ media: (name, build, options = {}) => runMediaStorageContract(
3052
+ name,
3053
+ build,
3054
+ {
3055
+ fetchUrl: fetchOverHttp,
3056
+ ...options
3057
+ }
3058
+ )
2650
3059
  };
2651
3060
  var contractSuites = suites;
2652
3061
  export {
@@ -2655,7 +3064,6 @@ export {
2655
3064
  adapterContext,
2656
3065
  contractSuites,
2657
3066
  matchesQuery,
2658
- runAiContract,
2659
3067
  runAnalyticsContract,
2660
3068
  runDocumentStoreContract,
2661
3069
  runEmailContract,
@@ -2668,6 +3076,7 @@ export {
2668
3076
  runSecurityContract,
2669
3077
  runShippingContract,
2670
3078
  runTaxContract,
3079
+ runWebhookContract,
2671
3080
  searchTerms,
2672
3081
  toSearchDocument
2673
3082
  };