@entifix/testing-unit 0.8.0 → 0.10.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.
Files changed (47) hide show
  1. package/README.md +6 -0
  2. package/dist/contracts/entity-facet-counter.contract.d.ts +30 -0
  3. package/dist/contracts/entity-facet-counter.contract.d.ts.map +1 -0
  4. package/dist/contracts/entity-facet-counter.contract.js +194 -0
  5. package/dist/contracts/entity-facet-counter.contract.js.map +1 -0
  6. package/dist/contracts/entity-repository.contract.d.ts.map +1 -1
  7. package/dist/contracts/entity-repository.contract.js +43 -0
  8. package/dist/contracts/entity-repository.contract.js.map +1 -1
  9. package/dist/contracts/failure.contract.d.ts +13 -0
  10. package/dist/contracts/failure.contract.d.ts.map +1 -0
  11. package/dist/contracts/failure.contract.js +38 -0
  12. package/dist/contracts/failure.contract.js.map +1 -0
  13. package/dist/contracts/index.d.ts +2 -0
  14. package/dist/contracts/index.d.ts.map +1 -1
  15. package/dist/contracts/index.js +2 -0
  16. package/dist/contracts/index.js.map +1 -1
  17. package/dist/contracts/unit-of-work.contract.d.ts +3 -3
  18. package/dist/contracts/unit-of-work.contract.d.ts.map +1 -1
  19. package/dist/contracts/unit-of-work.contract.js +7 -3
  20. package/dist/contracts/unit-of-work.contract.js.map +1 -1
  21. package/dist/doubles/entity-facet-counter.d.ts +21 -0
  22. package/dist/doubles/entity-facet-counter.d.ts.map +1 -0
  23. package/dist/doubles/entity-facet-counter.js +28 -0
  24. package/dist/doubles/entity-facet-counter.js.map +1 -0
  25. package/dist/doubles/transaction-ports.d.ts +3 -3
  26. package/dist/doubles/transaction-ports.d.ts.map +1 -1
  27. package/dist/doubles/transaction-ports.js +4 -6
  28. package/dist/doubles/transaction-ports.js.map +1 -1
  29. package/dist/doubles/unit-of-work.d.ts.map +1 -1
  30. package/dist/doubles/unit-of-work.js +2 -2
  31. package/dist/doubles/unit-of-work.js.map +1 -1
  32. package/dist/drivers/fake-amqp.d.ts +2 -0
  33. package/dist/drivers/fake-amqp.d.ts.map +1 -1
  34. package/dist/drivers/fake-amqp.js +14 -2
  35. package/dist/drivers/fake-amqp.js.map +1 -1
  36. package/dist/drivers/fake-mongo.d.ts.map +1 -1
  37. package/dist/drivers/fake-mongo.js +83 -0
  38. package/dist/drivers/fake-mongo.js.map +1 -1
  39. package/dist/http/entity-handlers.d.ts +11 -4
  40. package/dist/http/entity-handlers.d.ts.map +1 -1
  41. package/dist/http/entity-handlers.js +30 -6
  42. package/dist/http/entity-handlers.js.map +1 -1
  43. package/dist/index.d.ts +1 -0
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +1 -0
  46. package/dist/index.js.map +1 -1
  47. package/package.json +7 -7
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/contracts/unit-of-work.contract.ts"],"sourcesContent":["import { type DomainEvent, EntifixConnError } from '@entifix/core';\nimport type { UnitOfWork } from '@entifix/transactions';\nimport { Effect, Option } from 'effect';\nimport { describe, expect, it } from 'vitest';\n\n/** What {@link describeUnitOfWorkContract} needs to observe a store. */\nexport interface UnitOfWorkContractHarness<H> {\n readonly unitOfWork: UnitOfWork<H>;\n /** Writes one record through the unit's handle. */\n write(handle: H, id: string): Effect.Effect<void, EntifixConnError>;\n /** Whether a record exists, read through the unit's handle. */\n readIn(handle: H, id: string): Effect.Effect<boolean, EntifixConnError>;\n /** Whether a record is durable, read outside any unit. */\n stored(id: string): Promise<boolean>;\n /** The ids of every event the outbox holds. */\n recorded(): Promise<readonly string[]>;\n}\n\nconst anEvent = (id: string): DomainEvent => ({\n name: 'contract.widget.changed',\n id,\n source: 'contract-slice',\n at: '2026-01-01T00:00:00.000Z',\n correlationId: id,\n data: {},\n});\n\nconst run = <A, E>(effect: Effect.Effect<A, E>) => Effect.runPromise(effect);\n\n/**\n * What every {@link UnitOfWork} must guarantee: the writes, the events and the\n * claim of one unit commit or vanish together; a taken claim turns the next\n * unit into a `duplicate` without running it; a business failure is an\n * `aborted` value and a store failure is an error; and what a unit wrote is\n * visible to itself before it commits.\n */\nexport const describeUnitOfWorkContract = <H>(\n name: string,\n makeHarness: () =>\n UnitOfWorkContractHarness<H> | Promise<UnitOfWorkContractHarness<H>>,\n): void => {\n describe(`UnitOfWork contract: ${name}`, () => {\n it('commits the writes and the events together', async () => {\n const harness = await makeHarness();\n\n const outcome = await run(\n harness.unitOfWork.run(unit =>\n harness.write(unit.handle, 'w-1').pipe(\n Effect.tap(() => Effect.sync(() => unit.emit(anEvent('e-1')))),\n Effect.as('done'),\n ),\n ),\n );\n\n expect(outcome).toEqual({ _tag: 'committed', value: 'done' });\n expect(await harness.stored('w-1')).toBe(true);\n expect(await harness.recorded()).toContain('e-1');\n });\n\n it('answers a business failure as aborted, and writes nothing', async () => {\n const harness = await makeHarness();\n\n const outcome = await run(\n harness.unitOfWork.run(unit =>\n harness.write(unit.handle, 'w-2').pipe(\n Effect.tap(() => Effect.sync(() => unit.emit(anEvent('e-2')))),\n Effect.andThen(Effect.fail('outOfStock' as const)),\n ),\n ),\n );\n\n expect(outcome).toEqual({ _tag: 'aborted', reason: 'outOfStock' });\n expect(await harness.stored('w-2')).toBe(false);\n expect(await harness.recorded()).not.toContain('e-2');\n });\n\n it('surfaces a store failure as an error, and writes nothing', async () => {\n const harness = await makeHarness();\n\n const error = await run(\n harness.unitOfWork\n .run(unit =>\n harness\n .write(unit.handle, 'w-3')\n .pipe(\n Effect.andThen(\n Effect.fail(new EntifixConnError('store went away')),\n ),\n ),\n )\n .pipe(Effect.flip),\n );\n\n expect(error).toBeInstanceOf(EntifixConnError);\n expect(await harness.stored('w-3')).toBe(false);\n });\n\n it('lets a unit read its own uncommitted writes', async () => {\n const harness = await makeHarness();\n\n const outcome = await run(\n harness.unitOfWork.run(unit =>\n harness\n .write(unit.handle, 'w-4')\n .pipe(Effect.andThen(harness.readIn(unit.handle, 'w-4'))),\n ),\n );\n\n expect(outcome).toEqual({ _tag: 'committed', value: true });\n });\n\n it('turns a second unit on a taken event claim into a duplicate that does not run', async () => {\n const harness = await makeHarness();\n const claim = {\n kind: 'event',\n consumer: 'contract.fold',\n eventId: 'm-1',\n } as const;\n let runs = 0;\n const body = (unit: { handle: H }) =>\n Effect.suspend(() => {\n runs += 1;\n return harness.write(unit.handle, `fold-${runs}`);\n });\n\n const first = await run(harness.unitOfWork.run(body, { claim }));\n const second = await run(harness.unitOfWork.run(body, { claim }));\n\n expect(first._tag).toBe('committed');\n expect(second).toEqual({ _tag: 'duplicate', replay: Option.none() });\n expect(await harness.stored('fold-2')).toBe(false);\n });\n\n it('keys an event claim on the consumer as well as the event', async () => {\n const harness = await makeHarness();\n const noop = () => Effect.void;\n\n const one = await run(\n harness.unitOfWork.run(noop, {\n claim: { kind: 'event', consumer: 'contract.a', eventId: 'm-2' },\n }),\n );\n const other = await run(\n harness.unitOfWork.run(noop, {\n claim: { kind: 'event', consumer: 'contract.b', eventId: 'm-2' },\n }),\n );\n\n expect(one._tag).toBe('committed');\n expect(other._tag).toBe('committed');\n });\n\n it('answers a duplicate command with what the first run remembered', async () => {\n const harness = await makeHarness();\n const claim = { kind: 'command', commandId: 'c-1' } as const;\n const options = {\n claim,\n replay: (value: { paymentId: string }) => value,\n };\n\n let runs = 0;\n const capture = (unit: { handle: H }) =>\n Effect.suspend(() => {\n runs += 1;\n const paymentId = `pay-${runs}`;\n return harness\n .write(unit.handle, paymentId)\n .pipe(Effect.as({ paymentId }));\n });\n\n await run(harness.unitOfWork.run(capture, options));\n const again = await run(harness.unitOfWork.run(capture, options));\n\n expect(again).toEqual({\n _tag: 'duplicate',\n replay: Option.some({ paymentId: 'pay-1' }),\n });\n // A redelivered capture must not charge again, so the body never reruns.\n expect(runs).toBe(1);\n expect(await run(harness.unitOfWork.replay('c-1'))).toEqual(\n Option.some({ paymentId: 'pay-1' }),\n );\n expect(await run(harness.unitOfWork.replay('c-unknown'))).toEqual(\n Option.none(),\n );\n });\n\n it('leaves a claim untaken when its unit aborts', async () => {\n const harness = await makeHarness();\n const claim = { kind: 'command', commandId: 'c-2' } as const;\n\n const refused = await run(\n harness.unitOfWork.run(() => Effect.fail('declined' as const), {\n claim,\n }),\n );\n const retried = await run(\n harness.unitOfWork.run(() => Effect.succeed('ok'), { claim }),\n );\n\n expect(refused._tag).toBe('aborted');\n expect(retried).toEqual({ _tag: 'committed', value: 'ok' });\n });\n\n it('treats an event id already in the outbox as a duplicate', async () => {\n const harness = await makeHarness();\n const announce = (id: string) =>\n harness.unitOfWork.run(unit =>\n harness\n .write(unit.handle, id)\n .pipe(\n Effect.tap(() => Effect.sync(() => unit.emit(anEvent('e-same')))),\n ),\n );\n\n await run(announce('w-5'));\n const again = await run(announce('w-6'));\n\n expect(again._tag).toBe('duplicate');\n expect(await harness.stored('w-6')).toBe(false);\n });\n\n it('runs afterCommit only on commit, and never lets it change the outcome', async () => {\n const harness = await makeHarness();\n const ran: string[] = [];\n const after = (label: string) => ({\n afterCommit: Effect.sync(() => ran.push(label)).pipe(\n Effect.andThen(Effect.fail('drain failed')),\n ),\n });\n\n const committed = await run(\n harness.unitOfWork.run(() => Effect.succeed(1), after('commit')),\n );\n await run(\n harness.unitOfWork.run(\n () => Effect.fail('no' as const),\n after('abort'),\n ),\n );\n\n expect(committed).toEqual({ _tag: 'committed', value: 1 });\n expect(ran).toEqual(['commit']);\n });\n });\n};\n"],"names":["EntifixConnError","Effect","Option","describe","expect","it","anEvent","id","name","source","at","correlationId","data","run","effect","runPromise","describeUnitOfWorkContract","makeHarness","harness","outcome","unitOfWork","unit","write","handle","pipe","tap","sync","emit","as","toEqual","_tag","value","stored","toBe","recorded","toContain","andThen","fail","reason","not","error","flip","toBeInstanceOf","readIn","claim","kind","consumer","eventId","runs","body","suspend","first","second","replay","none","noop","void","one","other","commandId","options","capture","paymentId","again","some","refused","retried","succeed","announce","ran","after","label","afterCommit","push","committed"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/contracts/unit-of-work.contract.ts"],"sourcesContent":["import { type DomainEvent, type StoreFault, Unavailable } from '@entifix/core';\nimport type { UnitOfWork } from '@entifix/transactions';\nimport { Effect, Option } from 'effect';\nimport { describe, expect, it } from 'vitest';\n\n/** What {@link describeUnitOfWorkContract} needs to observe a store. */\nexport interface UnitOfWorkContractHarness<H> {\n readonly unitOfWork: UnitOfWork<H>;\n /** Writes one record through the unit's handle. */\n write(handle: H, id: string): Effect.Effect<void, StoreFault>;\n /** Whether a record exists, read through the unit's handle. */\n readIn(handle: H, id: string): Effect.Effect<boolean, StoreFault>;\n /** Whether a record is durable, read outside any unit. */\n stored(id: string): Promise<boolean>;\n /** The ids of every event the outbox holds. */\n recorded(): Promise<readonly string[]>;\n}\n\nconst anEvent = (id: string): DomainEvent => ({\n name: 'contract.widget.changed',\n id,\n source: 'contract-slice',\n at: '2026-01-01T00:00:00.000Z',\n correlationId: id,\n data: {},\n});\n\nconst run = <A, E>(effect: Effect.Effect<A, E>) => Effect.runPromise(effect);\n\n/**\n * What every {@link UnitOfWork} must guarantee: the writes, the events and the\n * claim of one unit commit or vanish together; a taken claim turns the next\n * unit into a `duplicate` without running it; a business failure is an\n * `aborted` value and a store failure is an error; and what a unit wrote is\n * visible to itself before it commits.\n */\nexport const describeUnitOfWorkContract = <H>(\n name: string,\n makeHarness: () =>\n UnitOfWorkContractHarness<H> | Promise<UnitOfWorkContractHarness<H>>,\n): void => {\n describe(`UnitOfWork contract: ${name}`, () => {\n it('commits the writes and the events together', async () => {\n const harness = await makeHarness();\n\n const outcome = await run(\n harness.unitOfWork.run(unit =>\n harness.write(unit.handle, 'w-1').pipe(\n Effect.tap(() => Effect.sync(() => unit.emit(anEvent('e-1')))),\n Effect.as('done'),\n ),\n ),\n );\n\n expect(outcome).toEqual({ _tag: 'committed', value: 'done' });\n expect(await harness.stored('w-1')).toBe(true);\n expect(await harness.recorded()).toContain('e-1');\n });\n\n it('answers a business failure as aborted, and writes nothing', async () => {\n const harness = await makeHarness();\n\n const outcome = await run(\n harness.unitOfWork.run(unit =>\n harness.write(unit.handle, 'w-2').pipe(\n Effect.tap(() => Effect.sync(() => unit.emit(anEvent('e-2')))),\n Effect.andThen(Effect.fail('outOfStock' as const)),\n ),\n ),\n );\n\n expect(outcome).toEqual({ _tag: 'aborted', reason: 'outOfStock' });\n expect(await harness.stored('w-2')).toBe(false);\n expect(await harness.recorded()).not.toContain('e-2');\n });\n\n it('surfaces a store failure as an error, and writes nothing', async () => {\n const harness = await makeHarness();\n\n const error = await run(\n harness.unitOfWork\n .run(unit =>\n harness\n .write(unit.handle, 'w-3')\n .pipe(\n Effect.andThen(\n Effect.fail(new Unavailable({ dependency: 'store' })),\n ),\n ),\n )\n .pipe(Effect.flip),\n );\n\n expect(error).toMatchObject({ _tag: 'Unavailable' });\n expect(await harness.stored('w-3')).toBe(false);\n });\n\n it('lets a unit read its own uncommitted writes', async () => {\n const harness = await makeHarness();\n\n const outcome = await run(\n harness.unitOfWork.run(unit =>\n harness\n .write(unit.handle, 'w-4')\n .pipe(Effect.andThen(harness.readIn(unit.handle, 'w-4'))),\n ),\n );\n\n expect(outcome).toEqual({ _tag: 'committed', value: true });\n });\n\n it('turns a second unit on a taken event claim into a duplicate that does not run', async () => {\n const harness = await makeHarness();\n const claim = {\n kind: 'event',\n consumer: 'contract.fold',\n eventId: 'm-1',\n } as const;\n let runs = 0;\n const body = (unit: { handle: H }) =>\n Effect.suspend(() => {\n runs += 1;\n return harness.write(unit.handle, `fold-${runs}`);\n });\n\n const first = await run(harness.unitOfWork.run(body, { claim }));\n const second = await run(harness.unitOfWork.run(body, { claim }));\n\n expect(first._tag).toBe('committed');\n expect(second).toEqual({ _tag: 'duplicate', replay: Option.none() });\n expect(await harness.stored('fold-2')).toBe(false);\n });\n\n it('keys an event claim on the consumer as well as the event', async () => {\n const harness = await makeHarness();\n const noop = () => Effect.void;\n\n const one = await run(\n harness.unitOfWork.run(noop, {\n claim: { kind: 'event', consumer: 'contract.a', eventId: 'm-2' },\n }),\n );\n const other = await run(\n harness.unitOfWork.run(noop, {\n claim: { kind: 'event', consumer: 'contract.b', eventId: 'm-2' },\n }),\n );\n\n expect(one._tag).toBe('committed');\n expect(other._tag).toBe('committed');\n });\n\n it('answers a duplicate command with what the first run remembered', async () => {\n const harness = await makeHarness();\n const claim = { kind: 'command', commandId: 'c-1' } as const;\n const options = {\n claim,\n replay: (value: { paymentId: string }) => value,\n };\n\n let runs = 0;\n const capture = (unit: { handle: H }) =>\n Effect.suspend(() => {\n runs += 1;\n const paymentId = `pay-${runs}`;\n return harness\n .write(unit.handle, paymentId)\n .pipe(Effect.as({ paymentId }));\n });\n\n await run(harness.unitOfWork.run(capture, options));\n const again = await run(harness.unitOfWork.run(capture, options));\n\n expect(again).toEqual({\n _tag: 'duplicate',\n replay: Option.some({ paymentId: 'pay-1' }),\n });\n // A redelivered capture must not charge again, so the body never reruns.\n expect(runs).toBe(1);\n expect(await run(harness.unitOfWork.replay('c-1'))).toEqual(\n Option.some({ paymentId: 'pay-1' }),\n );\n expect(await run(harness.unitOfWork.replay('c-unknown'))).toEqual(\n Option.none(),\n );\n });\n\n it('leaves a claim untaken when its unit aborts', async () => {\n const harness = await makeHarness();\n const claim = { kind: 'command', commandId: 'c-2' } as const;\n\n const refused = await run(\n harness.unitOfWork.run(() => Effect.fail('declined' as const), {\n claim,\n }),\n );\n const retried = await run(\n harness.unitOfWork.run(() => Effect.succeed('ok'), { claim }),\n );\n\n expect(refused._tag).toBe('aborted');\n expect(retried).toEqual({ _tag: 'committed', value: 'ok' });\n });\n\n it('treats an event id already in the outbox as a duplicate', async () => {\n const harness = await makeHarness();\n const announce = (id: string) =>\n harness.unitOfWork.run(unit =>\n harness\n .write(unit.handle, id)\n .pipe(\n Effect.tap(() => Effect.sync(() => unit.emit(anEvent('e-same')))),\n ),\n );\n\n await run(announce('w-5'));\n const again = await run(announce('w-6'));\n\n expect(again._tag).toBe('duplicate');\n expect(await harness.stored('w-6')).toBe(false);\n });\n\n it('runs afterCommit only on commit, and never lets it change the outcome', async () => {\n const harness = await makeHarness();\n const ran: string[] = [];\n const after = (label: string) => ({\n afterCommit: Effect.sync(() => ran.push(label)).pipe(\n Effect.andThen(Effect.fail('drain failed')),\n ),\n });\n\n const committed = await run(\n harness.unitOfWork.run(() => Effect.succeed(1), after('commit')),\n );\n await run(\n harness.unitOfWork.run(\n () => Effect.fail('no' as const),\n after('abort'),\n ),\n );\n\n expect(committed).toEqual({ _tag: 'committed', value: 1 });\n expect(ran).toEqual(['commit']);\n });\n });\n};\n"],"names":["Unavailable","Effect","Option","describe","expect","it","anEvent","id","name","source","at","correlationId","data","run","effect","runPromise","describeUnitOfWorkContract","makeHarness","harness","outcome","unitOfWork","unit","write","handle","pipe","tap","sync","emit","as","toEqual","_tag","value","stored","toBe","recorded","toContain","andThen","fail","reason","not","error","dependency","flip","toMatchObject","readIn","claim","kind","consumer","eventId","runs","body","suspend","first","second","replay","none","noop","void","one","other","commandId","options","capture","paymentId","again","some","refused","retried","succeed","announce","ran","after","label","afterCommit","push","committed"],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import type { EntityFacetCounter } from '@entifix/business';
2
+ import { type EntifixError, type Entity } from '@entifix/core';
3
+ export interface InMemoryEntityFacetCounter extends EntityFacetCounter {
4
+ /**
5
+ * Makes the next count fail with `error`, so a caller's degraded path is
6
+ * reachable without reaching for a mocking library.
7
+ */
8
+ failNext(error: EntifixError): void;
9
+ }
10
+ /**
11
+ * An {@link EntityFacetCounter} over records held in memory, counted by the
12
+ * same rules as every other in-memory query.
13
+ *
14
+ * `source` is either the records themselves or anything holding them — an
15
+ * `InMemoryEntityRepository` — read on every count, so a spec that saves
16
+ * through the repository sees the counts move.
17
+ */
18
+ export declare const makeInMemoryFacetCounter: (source: readonly Entity[] | {
19
+ readonly items: readonly Entity[];
20
+ }) => InMemoryEntityFacetCounter;
21
+ //# sourceMappingURL=entity-facet-counter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entity-facet-counter.d.ts","sourceRoot":"","sources":["../../src/doubles/entity-facet-counter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAC5D,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,MAAM,EAEZ,MAAM,eAAe,CAAC;AAGvB,MAAM,WAAW,0BAA2B,SAAQ,kBAAkB;IACpE;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,CAAC;CACrC;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,wBAAwB,GACnC,QAAQ,SAAS,MAAM,EAAE,GAAG;IAAE,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,KAChE,0BAsBF,CAAC"}
@@ -0,0 +1,28 @@
1
+ import { countFacets } from '@entifix/core';
2
+ import { Effect } from 'effect';
3
+ /**
4
+ * An {@link EntityFacetCounter} over records held in memory, counted by the
5
+ * same rules as every other in-memory query.
6
+ *
7
+ * `source` is either the records themselves or anything holding them — an
8
+ * `InMemoryEntityRepository` — read on every count, so a spec that saves
9
+ * through the repository sees the counts move.
10
+ */ export const makeInMemoryFacetCounter = (source)=>{
11
+ let pendingFailure;
12
+ const records = ()=>'items' in source ? source.items : source;
13
+ return {
14
+ count: (queries)=>Effect.suspend(()=>{
15
+ if (pendingFailure !== undefined) {
16
+ const error = pendingFailure;
17
+ pendingFailure = undefined;
18
+ return Effect.fail(error);
19
+ }
20
+ return Effect.succeed(countFacets(records(), queries));
21
+ }),
22
+ failNext: (error)=>{
23
+ pendingFailure = error;
24
+ }
25
+ };
26
+ };
27
+
28
+ //# sourceMappingURL=entity-facet-counter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/doubles/entity-facet-counter.ts"],"sourcesContent":["import type { EntityFacetCounter } from '@entifix/business';\nimport {\n countFacets,\n type EntifixError,\n type Entity,\n type EntityFacetQuery,\n} from '@entifix/core';\nimport { Effect } from 'effect';\n\nexport interface InMemoryEntityFacetCounter extends EntityFacetCounter {\n /**\n * Makes the next count fail with `error`, so a caller's degraded path is\n * reachable without reaching for a mocking library.\n */\n failNext(error: EntifixError): void;\n}\n\n/**\n * An {@link EntityFacetCounter} over records held in memory, counted by the\n * same rules as every other in-memory query.\n *\n * `source` is either the records themselves or anything holding them — an\n * `InMemoryEntityRepository` — read on every count, so a spec that saves\n * through the repository sees the counts move.\n */\nexport const makeInMemoryFacetCounter = (\n source: readonly Entity[] | { readonly items: readonly Entity[] },\n): InMemoryEntityFacetCounter => {\n let pendingFailure: EntifixError | undefined;\n const records = () => ('items' in source ? source.items : source);\n\n return {\n count: <TEntity extends Entity>(\n queries: readonly EntityFacetQuery<TEntity>[],\n ) =>\n Effect.suspend(() => {\n if (pendingFailure !== undefined) {\n const error = pendingFailure;\n pendingFailure = undefined;\n return Effect.fail(error);\n }\n return Effect.succeed(\n countFacets(records() as readonly TEntity[], queries),\n );\n }),\n failNext: error => {\n pendingFailure = error;\n },\n };\n};\n"],"names":["countFacets","Effect","makeInMemoryFacetCounter","source","pendingFailure","records","items","count","queries","suspend","undefined","error","fail","succeed","failNext"],"mappings":"AACA,SACEA,WAAW,QAIN,gBAAgB;AACvB,SAASC,MAAM,QAAQ,SAAS;AAUhC;;;;;;;CAOC,GACD,OAAO,MAAMC,2BAA2B,CACtCC;IAEA,IAAIC;IACJ,MAAMC,UAAU,IAAO,WAAWF,SAASA,OAAOG,KAAK,GAAGH;IAE1D,OAAO;QACLI,OAAO,CACLC,UAEAP,OAAOQ,OAAO,CAAC;gBACb,IAAIL,mBAAmBM,WAAW;oBAChC,MAAMC,QAAQP;oBACdA,iBAAiBM;oBACjB,OAAOT,OAAOW,IAAI,CAACD;gBACrB;gBACA,OAAOV,OAAOY,OAAO,CACnBb,YAAYK,WAAiCG;YAEjD;QACFM,UAAUH,CAAAA;YACRP,iBAAiBO;QACnB;IACF;AACF,EAAE"}
@@ -1,4 +1,4 @@
1
- import { type DomainEvent, type EntifixError } from '@entifix/core';
1
+ import { type DomainEvent, type TransitionalFailure } from '@entifix/core';
2
2
  import type { EventBus, LockService, SagaInstance, SagaStore, SequenceService, TransactionInbox } from '@entifix/transactions';
3
3
  import { Effect } from 'effect';
4
4
  export interface InMemoryLockService extends LockService {
@@ -11,7 +11,7 @@ export interface InMemoryLockService extends LockService {
11
11
  }>;
12
12
  /**
13
13
  * Marks `key` as already taken by someone else, so the next `acquire` fails
14
- * with {@link EntifixLockError} — the 409 path of the transaction facade.
14
+ * with {@link Contention} — the 409 path of the transaction facade.
15
15
  */
16
16
  contendOn(key: string): void;
17
17
  }
@@ -40,7 +40,7 @@ export interface RecordingEventBus extends EventBus {
40
40
  * broker's job in production, so a double that ignored it would let a
41
41
  * consumer pass its tests while receiving traffic it never bound to.
42
42
  */
43
- deliver(event: DomainEvent): Effect.Effect<void, EntifixError>;
43
+ deliver(event: DomainEvent): Effect.Effect<void, TransitionalFailure>;
44
44
  /** Makes the next `publish` fail, for the broker-unavailable branch. */
45
45
  failNextPublish(): void;
46
46
  }
@@ -1 +1 @@
1
- {"version":3,"file":"transaction-ports.d.ts","sourceRoot":"","sources":["../../src/doubles/transaction-ports.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,WAAW,EAEhB,KAAK,YAAY,EAGlB,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EACV,QAAQ,EAER,WAAW,EACX,YAAY,EACZ,SAAS,EACT,eAAe,EACf,gBAAgB,EACjB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,MAAM,WAAW,mBAAoB,SAAQ,WAAW;IACtD,iDAAiD;IACjD,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC;IACxB,sDAAsD;IACtD,QAAQ,CAAC,GAAG,EAAE,aAAa,CAAC;QAAE,MAAM,EAAE,SAAS,GAAG,SAAS,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC5E;;;OAGG;IACH,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,QAAO,mBAsC1C,CAAC;AAEF,MAAM,WAAW,uBAAwB,SAAQ,eAAe;IAC9D,6DAA6D;IAC7D,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACnD;AAED;;;GAGG;AACH,eAAO,MAAM,2BAA2B,QAAO,uBAc9C,CAAC;AAEF,MAAM,WAAW,iBAAkB,SAAQ,QAAQ;IACjD,uCAAuC;IACvC,QAAQ,CAAC,SAAS,EAAE,WAAW,EAAE,CAAC;IAClC;;;;;OAKG;IACH,OAAO,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC;IAC/D,wEAAwE;IACxE,eAAe,IAAI,IAAI,CAAC;CACzB;AAED;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,QAAO,iBA4CxC,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB;IAI5B,0DAA0D;oBAC1C,MAAM,KAAG,gBAAgB;qBAS3B,SAAS;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,EAAE;CAOjE,CAAC;AAEF,MAAM,WAAW,iBAAkB,SAAQ,SAAS;IAClD,4EAA4E;IAC5E,QAAQ,CAAC,SAAS,EAAE,SAAS,YAAY,EAAE,CAAC;CAC7C;AAED;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,QAAO,iBAwExC,CAAC"}
1
+ {"version":3,"file":"transaction-ports.d.ts","sourceRoot":"","sources":["../../src/doubles/transaction-ports.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,WAAW,EAEhB,KAAK,mBAAmB,EAEzB,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EACV,QAAQ,EAER,WAAW,EACX,YAAY,EACZ,SAAS,EACT,eAAe,EACf,gBAAgB,EACjB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,MAAM,WAAW,mBAAoB,SAAQ,WAAW;IACtD,iDAAiD;IACjD,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC;IACxB,sDAAsD;IACtD,QAAQ,CAAC,GAAG,EAAE,aAAa,CAAC;QAAE,MAAM,EAAE,SAAS,GAAG,SAAS,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC5E;;;OAGG;IACH,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,QAAO,mBAkC1C,CAAC;AAEF,MAAM,WAAW,uBAAwB,SAAQ,eAAe;IAC9D,6DAA6D;IAC7D,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACnD;AAED;;;GAGG;AACH,eAAO,MAAM,2BAA2B,QAAO,uBAc9C,CAAC;AAEF,MAAM,WAAW,iBAAkB,SAAQ,QAAQ;IACjD,uCAAuC;IACvC,QAAQ,CAAC,SAAS,EAAE,WAAW,EAAE,CAAC;IAClC;;;;;OAKG;IACH,OAAO,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,mBAAmB,CAAC,CAAC;IACtE,wEAAwE;IACxE,eAAe,IAAI,IAAI,CAAC;CACzB;AAED;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,QAAO,iBAwCxC,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB;IAI5B,0DAA0D;oBAC1C,MAAM,KAAG,gBAAgB;qBAS3B,SAAS;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,EAAE;CAOjE,CAAC;AAEF,MAAM,WAAW,iBAAkB,SAAQ,SAAS;IAClD,4EAA4E;IAC5E,QAAQ,CAAC,SAAS,EAAE,SAAS,YAAY,EAAE,CAAC;CAC7C;AAED;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,QAAO,iBAwExC,CAAC"}
@@ -1,5 +1,5 @@
1
1
  import { _ as _extends } from "@swc/helpers/_/_extends";
2
- import { EntifixConnError, EntifixLockError, matchesEventPattern } from '@entifix/core';
2
+ import { Contention, matchesEventPattern, Unavailable } from '@entifix/core';
3
3
  import { Effect } from 'effect';
4
4
  /**
5
5
  * In-memory {@link LockService}. Acquisition is immediate and uncontended
@@ -14,9 +14,7 @@ import { Effect } from 'effect';
14
14
  return {
15
15
  acquire: (key)=>Effect.suspend(()=>{
16
16
  if (contended.has(key) || tokens.has(key)) {
17
- return Effect.fail(new EntifixLockError(`Could not acquire lock "${key}"`, undefined, {
18
- key
19
- }));
17
+ return Effect.fail(new Contention());
20
18
  }
21
19
  const token = `token-${counter += 1}`;
22
20
  tokens.set(key, token);
@@ -80,8 +78,8 @@ import { Effect } from 'effect';
80
78
  publish: (event)=>Effect.suspend(()=>{
81
79
  if (failNext) {
82
80
  failNext = false;
83
- return Effect.fail(new EntifixConnError('Event bus unavailable', undefined, {
84
- eventId: event.id
81
+ return Effect.fail(new Unavailable({
82
+ dependency: 'bus'
85
83
  }));
86
84
  }
87
85
  published.push(event);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/doubles/transaction-ports.ts"],"sourcesContent":["import {\n type DomainEvent,\n EntifixConnError,\n type EntifixError,\n EntifixLockError,\n matchesEventPattern,\n} from '@entifix/core';\nimport type {\n EventBus,\n LockHandle,\n LockService,\n SagaInstance,\n SagaStore,\n SequenceService,\n TransactionInbox,\n} from '@entifix/transactions';\nimport { Effect } from 'effect';\n\nexport interface InMemoryLockService extends LockService {\n /** Keys currently held, in acquisition order. */\n readonly held: string[];\n /** Every acquire/release in the order it happened. */\n readonly log: ReadonlyArray<{ action: 'acquire' | 'release'; key: string }>;\n /**\n * Marks `key` as already taken by someone else, so the next `acquire` fails\n * with {@link EntifixLockError} — the 409 path of the transaction facade.\n */\n contendOn(key: string): void;\n}\n\n/**\n * In-memory {@link LockService}. Acquisition is immediate and uncontended\n * unless {@link InMemoryLockService.contendOn} says otherwise, and `release`\n * only frees a lock whose token matches — the compare-and-delete rule the Redis\n * adapter enforces with a Lua script.\n */\nexport const makeInMemoryLockService = (): InMemoryLockService => {\n const tokens = new Map<string, string>();\n const contended = new Set<string>();\n const log: Array<{ action: 'acquire' | 'release'; key: string }> = [];\n let counter = 0;\n\n return {\n acquire: key =>\n Effect.suspend(() => {\n if (contended.has(key) || tokens.has(key)) {\n return Effect.fail(\n new EntifixLockError(`Could not acquire lock \"${key}\"`, undefined, {\n key,\n }),\n );\n }\n const token = `token-${(counter += 1)}`;\n tokens.set(key, token);\n log.push({ action: 'acquire', key });\n return Effect.succeed({ key, token });\n }),\n release: (handle: LockHandle) =>\n Effect.sync(() => {\n if (tokens.get(handle.key) === handle.token) {\n tokens.delete(handle.key);\n log.push({ action: 'release', key: handle.key });\n }\n }),\n get held() {\n return [...tokens.keys()];\n },\n get log() {\n return log;\n },\n contendOn: key => {\n contended.add(key);\n },\n };\n};\n\nexport interface InMemorySequenceService extends SequenceService {\n /** The current value of every sequence drawn from so far. */\n readonly values: Readonly<Record<string, number>>;\n}\n\n/**\n * In-memory {@link SequenceService} — a plain counter per name. Atomicity is\n * what the Redis `INCR` adapter buys; single-threaded JS gives it for free here.\n */\nexport const makeInMemorySequenceService = (): InMemorySequenceService => {\n const sequences = new Map<string, number>();\n\n return {\n next: name =>\n Effect.sync(() => {\n const next = (sequences.get(name) ?? 0) + 1;\n sequences.set(name, next);\n return next;\n }),\n get values() {\n return Object.fromEntries(sequences);\n },\n };\n};\n\nexport interface RecordingEventBus extends EventBus {\n /** Every event published, in order. */\n readonly published: DomainEvent[];\n /**\n * Pushes an event to the subscribers whose pattern matches it, as a topic\n * exchange would. Deliberately not \"to all subscribers\": routing is the\n * broker's job in production, so a double that ignored it would let a\n * consumer pass its tests while receiving traffic it never bound to.\n */\n deliver(event: DomainEvent): Effect.Effect<void, EntifixError>;\n /** Makes the next `publish` fail, for the broker-unavailable branch. */\n failNextPublish(): void;\n}\n\n/**\n * In-memory {@link EventBus} that records what was published rather than\n * asserting on it. Prefer reading `bus.published` in a test over a mock's call\n * assertions: the expectation then reads as state, not as a spy protocol.\n */\nexport const makeRecordingEventBus = (): RecordingEventBus => {\n const published: DomainEvent[] = [];\n const handlers: Array<{\n pattern: string;\n handle: (event: DomainEvent) => Effect.Effect<void, EntifixError>;\n }> = [];\n let failNext = false;\n\n return {\n publish: event =>\n Effect.suspend(() => {\n if (failNext) {\n failNext = false;\n return Effect.fail(\n new EntifixConnError('Event bus unavailable', undefined, {\n eventId: event.id,\n }),\n );\n }\n published.push(event);\n return Effect.void;\n }),\n subscribe: (subscription, handle) =>\n Effect.sync(() => {\n // Only the pattern is retained: queue durability and the delivery\n // ceiling are broker facts with no in-memory analogue, and faking them\n // would assert against the double rather than against a broker.\n handlers.push({ pattern: subscription.pattern, handle });\n }),\n deliver: event =>\n Effect.forEach(\n handlers.filter(({ pattern }) =>\n matchesEventPattern(pattern, event.name),\n ),\n ({ handle }) => handle(event),\n { discard: true },\n ),\n get published() {\n return published;\n },\n failNextPublish: () => {\n failNext = true;\n },\n };\n};\n\n/**\n * An in-memory {@link TransactionInbox}, one per consumer, over a shared claim\n * set.\n *\n * Sharing the set across the inboxes one factory hands out is what makes the\n * contract's \"one consumer cannot consume another's claim\" case a real\n * assertion, rather than one the double satisfies by only ever having seen a\n * single consumer.\n */\nexport const makeInMemoryInboxes = () => {\n const claimed = new Set<string>();\n\n return {\n /** An inbox for `consumer`, over the shared claim set. */\n for: (consumer: string): TransactionInbox => ({\n claim: eventId =>\n Effect.sync(() => {\n const key = JSON.stringify([consumer, eventId]);\n if (claimed.has(key)) return 'duplicate' as const;\n claimed.add(key);\n return 'claimed' as const;\n }),\n }),\n get claims(): readonly { consumer: string; eventId: string }[] {\n return [...claimed].map(key => {\n const [consumer, eventId] = JSON.parse(key) as [string, string];\n return { consumer, eventId };\n });\n },\n };\n};\n\nexport interface InMemorySagaStore extends SagaStore {\n /** Every instance, as stored — for a spec to assert on state, not calls. */\n readonly instances: readonly SagaInstance[];\n}\n\n/**\n * In-memory {@link SagaStore}, with the Mongo adapter's semantics: `start`\n * never overwrites, outcomes append, and a resume claim only takes an\n * unfinished instance whose `updatedAt` is past the deadline — counting the\n * attempt and re-stamping it, so the next sweep does not find it again.\n */\nexport const makeInMemorySagaStore = (): InMemorySagaStore => {\n const instances = new Map<string, SagaInstance>();\n const stamp = () => new Date().toISOString();\n const unfinished = (entry: SagaInstance) =>\n entry.state === 'RUNNING' || entry.state === 'COMPENSATING';\n const pastDeadline = (entry: SagaInstance, olderThanMs: number) =>\n entry.updatedAt < new Date(Date.now() - olderThanMs).toISOString();\n const update = (\n sagaId: string,\n change: (entry: SagaInstance) => SagaInstance,\n ) =>\n Effect.sync(() => {\n const current = instances.get(sagaId);\n if (current !== undefined) {\n instances.set(sagaId, { ...change(current), updatedAt: stamp() });\n }\n });\n\n return {\n start: started =>\n Effect.sync(() => {\n if (!instances.has(started.sagaId)) {\n instances.set(started.sagaId, { ...started, updatedAt: stamp() });\n }\n }),\n beginStep: (sagaId, stepIndex) =>\n update(sagaId, entry => ({ ...entry, stepIndex })),\n recordOutcome: (sagaId, outcome) =>\n update(sagaId, entry => ({\n ...entry,\n outcomes: [...entry.outcomes, outcome],\n })),\n settle: (sagaId, state, error) =>\n update(sagaId, entry => ({ ...entry, state, error })),\n markCompensated: (sagaId, stepId) =>\n update(sagaId, entry => ({\n ...entry,\n outcomes: entry.outcomes.map(outcome =>\n outcome.stepId === stepId\n ? { ...outcome, compensated: true }\n : outcome,\n ),\n })),\n get: sagaId => Effect.sync(() => instances.get(sagaId)),\n findStale: olderThanMs =>\n Effect.sync(() =>\n [...instances.values()].filter(\n entry => unfinished(entry) && pastDeadline(entry, olderThanMs),\n ),\n ),\n claimForResume: (sagaId, olderThanMs) =>\n Effect.sync(() => {\n const current = instances.get(sagaId);\n if (\n current === undefined ||\n !unfinished(current) ||\n !pastDeadline(current, olderThanMs)\n ) {\n return undefined;\n }\n const claimed = {\n ...current,\n resumeAttempts: current.resumeAttempts + 1,\n updatedAt: stamp(),\n };\n instances.set(sagaId, claimed);\n return claimed;\n }),\n get instances() {\n return [...instances.values()];\n },\n };\n};\n"],"names":["EntifixConnError","EntifixLockError","matchesEventPattern","Effect","makeInMemoryLockService","tokens","Map","contended","Set","log","counter","acquire","key","suspend","has","fail","undefined","token","set","push","action","succeed","release","handle","sync","get","delete","held","keys","contendOn","add","makeInMemorySequenceService","sequences","next","name","values","Object","fromEntries","makeRecordingEventBus","published","handlers","failNext","publish","event","eventId","id","void","subscribe","subscription","pattern","deliver","forEach","filter","discard","failNextPublish","makeInMemoryInboxes","claimed","for","consumer","claim","JSON","stringify","claims","map","parse","makeInMemorySagaStore","instances","stamp","Date","toISOString","unfinished","entry","state","pastDeadline","olderThanMs","updatedAt","now","update","sagaId","change","current","start","started","beginStep","stepIndex","recordOutcome","outcome","outcomes","settle","error","markCompensated","stepId","compensated","findStale","claimForResume","resumeAttempts"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/doubles/transaction-ports.ts"],"sourcesContent":["import {\n Contention,\n type DomainEvent,\n matchesEventPattern,\n type TransitionalFailure,\n Unavailable,\n} from '@entifix/core';\nimport type {\n EventBus,\n LockHandle,\n LockService,\n SagaInstance,\n SagaStore,\n SequenceService,\n TransactionInbox,\n} from '@entifix/transactions';\nimport { Effect } from 'effect';\n\nexport interface InMemoryLockService extends LockService {\n /** Keys currently held, in acquisition order. */\n readonly held: string[];\n /** Every acquire/release in the order it happened. */\n readonly log: ReadonlyArray<{ action: 'acquire' | 'release'; key: string }>;\n /**\n * Marks `key` as already taken by someone else, so the next `acquire` fails\n * with {@link Contention} — the 409 path of the transaction facade.\n */\n contendOn(key: string): void;\n}\n\n/**\n * In-memory {@link LockService}. Acquisition is immediate and uncontended\n * unless {@link InMemoryLockService.contendOn} says otherwise, and `release`\n * only frees a lock whose token matches — the compare-and-delete rule the Redis\n * adapter enforces with a Lua script.\n */\nexport const makeInMemoryLockService = (): InMemoryLockService => {\n const tokens = new Map<string, string>();\n const contended = new Set<string>();\n const log: Array<{ action: 'acquire' | 'release'; key: string }> = [];\n let counter = 0;\n\n return {\n acquire: key =>\n Effect.suspend(() => {\n if (contended.has(key) || tokens.has(key)) {\n return Effect.fail(new Contention());\n }\n const token = `token-${(counter += 1)}`;\n tokens.set(key, token);\n log.push({ action: 'acquire', key });\n return Effect.succeed({ key, token });\n }),\n release: (handle: LockHandle) =>\n Effect.sync(() => {\n if (tokens.get(handle.key) === handle.token) {\n tokens.delete(handle.key);\n log.push({ action: 'release', key: handle.key });\n }\n }),\n get held() {\n return [...tokens.keys()];\n },\n get log() {\n return log;\n },\n contendOn: key => {\n contended.add(key);\n },\n };\n};\n\nexport interface InMemorySequenceService extends SequenceService {\n /** The current value of every sequence drawn from so far. */\n readonly values: Readonly<Record<string, number>>;\n}\n\n/**\n * In-memory {@link SequenceService} — a plain counter per name. Atomicity is\n * what the Redis `INCR` adapter buys; single-threaded JS gives it for free here.\n */\nexport const makeInMemorySequenceService = (): InMemorySequenceService => {\n const sequences = new Map<string, number>();\n\n return {\n next: name =>\n Effect.sync(() => {\n const next = (sequences.get(name) ?? 0) + 1;\n sequences.set(name, next);\n return next;\n }),\n get values() {\n return Object.fromEntries(sequences);\n },\n };\n};\n\nexport interface RecordingEventBus extends EventBus {\n /** Every event published, in order. */\n readonly published: DomainEvent[];\n /**\n * Pushes an event to the subscribers whose pattern matches it, as a topic\n * exchange would. Deliberately not \"to all subscribers\": routing is the\n * broker's job in production, so a double that ignored it would let a\n * consumer pass its tests while receiving traffic it never bound to.\n */\n deliver(event: DomainEvent): Effect.Effect<void, TransitionalFailure>;\n /** Makes the next `publish` fail, for the broker-unavailable branch. */\n failNextPublish(): void;\n}\n\n/**\n * In-memory {@link EventBus} that records what was published rather than\n * asserting on it. Prefer reading `bus.published` in a test over a mock's call\n * assertions: the expectation then reads as state, not as a spy protocol.\n */\nexport const makeRecordingEventBus = (): RecordingEventBus => {\n const published: DomainEvent[] = [];\n const handlers: Array<{\n pattern: string;\n handle: (event: DomainEvent) => Effect.Effect<void, TransitionalFailure>;\n }> = [];\n let failNext = false;\n\n return {\n publish: event =>\n Effect.suspend(() => {\n if (failNext) {\n failNext = false;\n return Effect.fail(new Unavailable({ dependency: 'bus' }));\n }\n published.push(event);\n return Effect.void;\n }),\n subscribe: (subscription, handle) =>\n Effect.sync(() => {\n // Only the pattern is retained: queue durability and the delivery\n // ceiling are broker facts with no in-memory analogue, and faking them\n // would assert against the double rather than against a broker.\n handlers.push({ pattern: subscription.pattern, handle });\n }),\n deliver: event =>\n Effect.forEach(\n handlers.filter(({ pattern }) =>\n matchesEventPattern(pattern, event.name),\n ),\n ({ handle }) => handle(event),\n { discard: true },\n ),\n get published() {\n return published;\n },\n failNextPublish: () => {\n failNext = true;\n },\n };\n};\n\n/**\n * An in-memory {@link TransactionInbox}, one per consumer, over a shared claim\n * set.\n *\n * Sharing the set across the inboxes one factory hands out is what makes the\n * contract's \"one consumer cannot consume another's claim\" case a real\n * assertion, rather than one the double satisfies by only ever having seen a\n * single consumer.\n */\nexport const makeInMemoryInboxes = () => {\n const claimed = new Set<string>();\n\n return {\n /** An inbox for `consumer`, over the shared claim set. */\n for: (consumer: string): TransactionInbox => ({\n claim: eventId =>\n Effect.sync(() => {\n const key = JSON.stringify([consumer, eventId]);\n if (claimed.has(key)) return 'duplicate' as const;\n claimed.add(key);\n return 'claimed' as const;\n }),\n }),\n get claims(): readonly { consumer: string; eventId: string }[] {\n return [...claimed].map(key => {\n const [consumer, eventId] = JSON.parse(key) as [string, string];\n return { consumer, eventId };\n });\n },\n };\n};\n\nexport interface InMemorySagaStore extends SagaStore {\n /** Every instance, as stored — for a spec to assert on state, not calls. */\n readonly instances: readonly SagaInstance[];\n}\n\n/**\n * In-memory {@link SagaStore}, with the Mongo adapter's semantics: `start`\n * never overwrites, outcomes append, and a resume claim only takes an\n * unfinished instance whose `updatedAt` is past the deadline — counting the\n * attempt and re-stamping it, so the next sweep does not find it again.\n */\nexport const makeInMemorySagaStore = (): InMemorySagaStore => {\n const instances = new Map<string, SagaInstance>();\n const stamp = () => new Date().toISOString();\n const unfinished = (entry: SagaInstance) =>\n entry.state === 'RUNNING' || entry.state === 'COMPENSATING';\n const pastDeadline = (entry: SagaInstance, olderThanMs: number) =>\n entry.updatedAt < new Date(Date.now() - olderThanMs).toISOString();\n const update = (\n sagaId: string,\n change: (entry: SagaInstance) => SagaInstance,\n ) =>\n Effect.sync(() => {\n const current = instances.get(sagaId);\n if (current !== undefined) {\n instances.set(sagaId, { ...change(current), updatedAt: stamp() });\n }\n });\n\n return {\n start: started =>\n Effect.sync(() => {\n if (!instances.has(started.sagaId)) {\n instances.set(started.sagaId, { ...started, updatedAt: stamp() });\n }\n }),\n beginStep: (sagaId, stepIndex) =>\n update(sagaId, entry => ({ ...entry, stepIndex })),\n recordOutcome: (sagaId, outcome) =>\n update(sagaId, entry => ({\n ...entry,\n outcomes: [...entry.outcomes, outcome],\n })),\n settle: (sagaId, state, error) =>\n update(sagaId, entry => ({ ...entry, state, error })),\n markCompensated: (sagaId, stepId) =>\n update(sagaId, entry => ({\n ...entry,\n outcomes: entry.outcomes.map(outcome =>\n outcome.stepId === stepId\n ? { ...outcome, compensated: true }\n : outcome,\n ),\n })),\n get: sagaId => Effect.sync(() => instances.get(sagaId)),\n findStale: olderThanMs =>\n Effect.sync(() =>\n [...instances.values()].filter(\n entry => unfinished(entry) && pastDeadline(entry, olderThanMs),\n ),\n ),\n claimForResume: (sagaId, olderThanMs) =>\n Effect.sync(() => {\n const current = instances.get(sagaId);\n if (\n current === undefined ||\n !unfinished(current) ||\n !pastDeadline(current, olderThanMs)\n ) {\n return undefined;\n }\n const claimed = {\n ...current,\n resumeAttempts: current.resumeAttempts + 1,\n updatedAt: stamp(),\n };\n instances.set(sagaId, claimed);\n return claimed;\n }),\n get instances() {\n return [...instances.values()];\n },\n };\n};\n"],"names":["Contention","matchesEventPattern","Unavailable","Effect","makeInMemoryLockService","tokens","Map","contended","Set","log","counter","acquire","key","suspend","has","fail","token","set","push","action","succeed","release","handle","sync","get","delete","held","keys","contendOn","add","makeInMemorySequenceService","sequences","next","name","values","Object","fromEntries","makeRecordingEventBus","published","handlers","failNext","publish","event","dependency","void","subscribe","subscription","pattern","deliver","forEach","filter","discard","failNextPublish","makeInMemoryInboxes","claimed","for","consumer","claim","eventId","JSON","stringify","claims","map","parse","makeInMemorySagaStore","instances","stamp","Date","toISOString","unfinished","entry","state","pastDeadline","olderThanMs","updatedAt","now","update","sagaId","change","current","undefined","start","started","beginStep","stepIndex","recordOutcome","outcome","outcomes","settle","error","markCompensated","stepId","compensated","findStale","claimForResume","resumeAttempts"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"unit-of-work.d.ts","sourceRoot":"","sources":["../../src/doubles/unit-of-work.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,WAAW,EAAoB,MAAM,eAAe,CAAC;AACnE,OAAO,KAAK,EAAa,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAGnE;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACxC;AAED,MAAM,WAAW,kBAAmB,SAAQ,UAAU,CAAC,kBAAkB,CAAC;IACxE,6BAA6B;IAC7B,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/C,6DAA6D;IAC7D,QAAQ,CAAC,MAAM,EAAE,SAAS,WAAW,EAAE,CAAC;CACzC;AAOD;;;;;;;;;GASG;AACH,eAAO,MAAM,sBAAsB,GACjC,OAAM,QAAQ,CAAC,SAAS,CAAC,MAAM,EAAE,OAAO,CAAC,CAAM,KAC9C,kBA0EF,CAAC"}
1
+ {"version":3,"file":"unit-of-work.d.ts","sourceRoot":"","sources":["../../src/doubles/unit-of-work.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,WAAW,EAAgB,MAAM,eAAe,CAAC;AAC/D,OAAO,KAAK,EAAa,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAGnE;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACxC;AAED,MAAM,WAAW,kBAAmB,SAAQ,UAAU,CAAC,kBAAkB,CAAC;IACxE,6BAA6B;IAC7B,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/C,6DAA6D;IAC7D,QAAQ,CAAC,MAAM,EAAE,SAAS,WAAW,EAAE,CAAC;CACzC;AAOD;;;;;;;;;GASG;AACH,eAAO,MAAM,sBAAsB,GACjC,OAAM,QAAQ,CAAC,SAAS,CAAC,MAAM,EAAE,OAAO,CAAC,CAAM,KAC9C,kBA0EF,CAAC"}
@@ -1,4 +1,4 @@
1
- import { EntifixConnError } from '@entifix/core';
1
+ import { isStoreFault } from '@entifix/core';
2
2
  import { Effect, Option, Result } from 'effect';
3
3
  const claimKey = (claim)=>claim.kind === 'event' ? `event:${claim.consumer}:${claim.eventId}` : `command:${claim.commandId}`;
4
4
  /**
@@ -41,7 +41,7 @@ const claimKey = (claim)=>claim.kind === 'event' ? `event:${claim.consumer}:${cl
41
41
  emit: (event)=>void emitted.push(event)
42
42
  }));
43
43
  if (Result.isFailure(outcome)) {
44
- if (outcome.failure instanceof EntifixConnError) {
44
+ if (isStoreFault(outcome.failure)) {
45
45
  return yield* Effect.fail(outcome.failure);
46
46
  }
47
47
  return {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/doubles/unit-of-work.ts"],"sourcesContent":["import { type DomainEvent, EntifixConnError } from '@entifix/core';\nimport type { UnitClaim, UnitOfWork } from '@entifix/transactions';\nimport { Effect, Option, Result } from 'effect';\n\n/**\n * The handle an in-memory unit hands its body: a working copy of the records,\n * which becomes the store only if the unit commits.\n */\nexport interface InMemoryUnitHandle {\n readonly records: Map<string, unknown>;\n}\n\nexport interface InMemoryUnitOfWork extends UnitOfWork<InMemoryUnitHandle> {\n /** The committed records. */\n readonly records: ReadonlyMap<string, unknown>;\n /** Every event a committed unit emitted, in commit order. */\n readonly outbox: readonly DomainEvent[];\n}\n\nconst claimKey = (claim: UnitClaim): string =>\n claim.kind === 'event'\n ? `event:${claim.consumer}:${claim.eventId}`\n : `command:${claim.commandId}`;\n\n/**\n * A {@link UnitOfWork} over plain maps, for testing code that runs units\n * without a database.\n *\n * Atomic the way a real one is: the body writes to a copy, and a body that\n * fails — with a business reason or with a store error — leaves the committed\n * records, the outbox and the claims exactly as they were. Like the Mongo\n * driver fake it is atomicity and not isolation; nothing here runs\n * concurrently, so a race is a `live` test's to prove.\n */\nexport const makeInMemoryUnitOfWork = (\n seed: Iterable<readonly [string, unknown]> = [],\n): InMemoryUnitOfWork => {\n let records = new Map<string, unknown>(seed);\n const outbox: DomainEvent[] = [];\n const eventIds = new Set<string>();\n // A taken claim, and for a command what it is to be answered with.\n const claims = new Map<string, Option.Option<unknown>>();\n\n const unitOfWork: InMemoryUnitOfWork = {\n get records() {\n return records;\n },\n get outbox() {\n return outbox;\n },\n\n run: (body, options) =>\n Effect.gen(function* () {\n const key = options?.claim ? claimKey(options.claim) : undefined;\n const taken = key === undefined ? undefined : claims.get(key);\n if (taken !== undefined) {\n return { _tag: 'duplicate' as const, replay: taken };\n }\n\n const handle: InMemoryUnitHandle = { records: new Map(records) };\n const emitted: DomainEvent[] = [];\n const outcome = yield* Effect.result(\n body({ handle, emit: event => void emitted.push(event) }),\n );\n\n if (Result.isFailure(outcome)) {\n if (outcome.failure instanceof EntifixConnError) {\n return yield* Effect.fail(outcome.failure);\n }\n return {\n _tag: 'aborted' as const,\n reason: outcome.failure as never,\n };\n }\n\n // An event id already recorded is the outbox's own idempotency claim:\n // the work it announces has committed before.\n if (emitted.some(event => eventIds.has(event.id))) {\n return { _tag: 'duplicate' as const, replay: Option.none() };\n }\n\n records = handle.records;\n for (const event of emitted) {\n eventIds.add(event.id);\n outbox.push(event);\n }\n if (key !== undefined) {\n claims.set(\n key,\n options?.replay && options.claim?.kind === 'command'\n ? Option.some(options.replay(outcome.success))\n : Option.none(),\n );\n }\n\n if (options?.afterCommit) {\n yield* options.afterCommit.pipe(Effect.ignore);\n }\n return { _tag: 'committed' as const, value: outcome.success };\n }),\n\n replay: commandId =>\n Effect.sync(() =>\n Option.flatten(\n Option.fromUndefinedOr(claims.get(`command:${commandId}`)),\n ),\n ),\n };\n\n return unitOfWork;\n};\n"],"names":["EntifixConnError","Effect","Option","Result","claimKey","claim","kind","consumer","eventId","commandId","makeInMemoryUnitOfWork","seed","records","Map","outbox","eventIds","Set","claims","unitOfWork","run","body","options","gen","key","undefined","taken","get","_tag","replay","handle","emitted","outcome","result","emit","event","push","isFailure","failure","fail","reason","some","has","id","none","add","set","success","afterCommit","pipe","ignore","value","sync","flatten","fromUndefinedOr"],"mappings":"AAAA,SAA2BA,gBAAgB,QAAQ,gBAAgB;AAEnE,SAASC,MAAM,EAAEC,MAAM,EAAEC,MAAM,QAAQ,SAAS;AAiBhD,MAAMC,WAAW,CAACC,QAChBA,MAAMC,IAAI,KAAK,UACX,CAAC,MAAM,EAAED,MAAME,QAAQ,CAAC,CAAC,EAAEF,MAAMG,OAAO,EAAE,GAC1C,CAAC,QAAQ,EAAEH,MAAMI,SAAS,EAAE;AAElC;;;;;;;;;CASC,GACD,OAAO,MAAMC,yBAAyB,CACpCC,OAA6C,EAAE;IAE/C,IAAIC,UAAU,IAAIC,IAAqBF;IACvC,MAAMG,SAAwB,EAAE;IAChC,MAAMC,WAAW,IAAIC;IACrB,mEAAmE;IACnE,MAAMC,SAAS,IAAIJ;IAEnB,MAAMK,aAAiC;QACrC,IAAIN;YACF,OAAOA;QACT;QACA,IAAIE;YACF,OAAOA;QACT;QAEAK,KAAK,CAACC,MAAMC,UACVpB,OAAOqB,GAAG,CAAC;gBACT,MAAMC,MAAMF,CAAAA,2BAAAA,QAAShB,KAAK,IAAGD,SAASiB,QAAQhB,KAAK,IAAImB;gBACvD,MAAMC,QAAQF,QAAQC,YAAYA,YAAYP,OAAOS,GAAG,CAACH;gBACzD,IAAIE,UAAUD,WAAW;oBACvB,OAAO;wBAAEG,MAAM;wBAAsBC,QAAQH;oBAAM;gBACrD;gBAEA,MAAMI,SAA6B;oBAAEjB,SAAS,IAAIC,IAAID;gBAAS;gBAC/D,MAAMkB,UAAyB,EAAE;gBACjC,MAAMC,UAAU,OAAO9B,OAAO+B,MAAM,CAClCZ,KAAK;oBAAES;oBAAQI,MAAMC,CAAAA,QAAS,KAAKJ,QAAQK,IAAI,CAACD;gBAAO;gBAGzD,IAAI/B,OAAOiC,SAAS,CAACL,UAAU;oBAC7B,IAAIA,QAAQM,OAAO,YAAYrC,kBAAkB;wBAC/C,OAAO,OAAOC,OAAOqC,IAAI,CAACP,QAAQM,OAAO;oBAC3C;oBACA,OAAO;wBACLV,MAAM;wBACNY,QAAQR,QAAQM,OAAO;oBACzB;gBACF;gBAEA,sEAAsE;gBACtE,8CAA8C;gBAC9C,IAAIP,QAAQU,IAAI,CAACN,CAAAA,QAASnB,SAAS0B,GAAG,CAACP,MAAMQ,EAAE,IAAI;oBACjD,OAAO;wBAAEf,MAAM;wBAAsBC,QAAQ1B,OAAOyC,IAAI;oBAAG;gBAC7D;gBAEA/B,UAAUiB,OAAOjB,OAAO;gBACxB,KAAK,MAAMsB,SAASJ,QAAS;oBAC3Bf,SAAS6B,GAAG,CAACV,MAAMQ,EAAE;oBACrB5B,OAAOqB,IAAI,CAACD;gBACd;gBACA,IAAIX,QAAQC,WAAW;wBAGAH;oBAFrBJ,OAAO4B,GAAG,CACRtB,KACAF,CAAAA,2BAAAA,QAASO,MAAM,KAAIP,EAAAA,iBAAAA,QAAQhB,KAAK,qBAAbgB,eAAef,IAAI,MAAK,YACvCJ,OAAOsC,IAAI,CAACnB,QAAQO,MAAM,CAACG,QAAQe,OAAO,KAC1C5C,OAAOyC,IAAI;gBAEnB;gBAEA,IAAItB,2BAAAA,QAAS0B,WAAW,EAAE;oBACxB,OAAO1B,QAAQ0B,WAAW,CAACC,IAAI,CAAC/C,OAAOgD,MAAM;gBAC/C;gBACA,OAAO;oBAAEtB,MAAM;oBAAsBuB,OAAOnB,QAAQe,OAAO;gBAAC;YAC9D;QAEFlB,QAAQnB,CAAAA,YACNR,OAAOkD,IAAI,CAAC,IACVjD,OAAOkD,OAAO,CACZlD,OAAOmD,eAAe,CAACpC,OAAOS,GAAG,CAAC,CAAC,QAAQ,EAAEjB,WAAW;IAGhE;IAEA,OAAOS;AACT,EAAE"}
1
+ {"version":3,"sources":["../../src/doubles/unit-of-work.ts"],"sourcesContent":["import { type DomainEvent, isStoreFault } from '@entifix/core';\nimport type { UnitClaim, UnitOfWork } from '@entifix/transactions';\nimport { Effect, Option, Result } from 'effect';\n\n/**\n * The handle an in-memory unit hands its body: a working copy of the records,\n * which becomes the store only if the unit commits.\n */\nexport interface InMemoryUnitHandle {\n readonly records: Map<string, unknown>;\n}\n\nexport interface InMemoryUnitOfWork extends UnitOfWork<InMemoryUnitHandle> {\n /** The committed records. */\n readonly records: ReadonlyMap<string, unknown>;\n /** Every event a committed unit emitted, in commit order. */\n readonly outbox: readonly DomainEvent[];\n}\n\nconst claimKey = (claim: UnitClaim): string =>\n claim.kind === 'event'\n ? `event:${claim.consumer}:${claim.eventId}`\n : `command:${claim.commandId}`;\n\n/**\n * A {@link UnitOfWork} over plain maps, for testing code that runs units\n * without a database.\n *\n * Atomic the way a real one is: the body writes to a copy, and a body that\n * fails — with a business reason or with a store error — leaves the committed\n * records, the outbox and the claims exactly as they were. Like the Mongo\n * driver fake it is atomicity and not isolation; nothing here runs\n * concurrently, so a race is a `live` test's to prove.\n */\nexport const makeInMemoryUnitOfWork = (\n seed: Iterable<readonly [string, unknown]> = [],\n): InMemoryUnitOfWork => {\n let records = new Map<string, unknown>(seed);\n const outbox: DomainEvent[] = [];\n const eventIds = new Set<string>();\n // A taken claim, and for a command what it is to be answered with.\n const claims = new Map<string, Option.Option<unknown>>();\n\n const unitOfWork: InMemoryUnitOfWork = {\n get records() {\n return records;\n },\n get outbox() {\n return outbox;\n },\n\n run: (body, options) =>\n Effect.gen(function* () {\n const key = options?.claim ? claimKey(options.claim) : undefined;\n const taken = key === undefined ? undefined : claims.get(key);\n if (taken !== undefined) {\n return { _tag: 'duplicate' as const, replay: taken };\n }\n\n const handle: InMemoryUnitHandle = { records: new Map(records) };\n const emitted: DomainEvent[] = [];\n const outcome = yield* Effect.result(\n body({ handle, emit: event => void emitted.push(event) }),\n );\n\n if (Result.isFailure(outcome)) {\n if (isStoreFault(outcome.failure)) {\n return yield* Effect.fail(outcome.failure);\n }\n return {\n _tag: 'aborted' as const,\n reason: outcome.failure as never,\n };\n }\n\n // An event id already recorded is the outbox's own idempotency claim:\n // the work it announces has committed before.\n if (emitted.some(event => eventIds.has(event.id))) {\n return { _tag: 'duplicate' as const, replay: Option.none() };\n }\n\n records = handle.records;\n for (const event of emitted) {\n eventIds.add(event.id);\n outbox.push(event);\n }\n if (key !== undefined) {\n claims.set(\n key,\n options?.replay && options.claim?.kind === 'command'\n ? Option.some(options.replay(outcome.success))\n : Option.none(),\n );\n }\n\n if (options?.afterCommit) {\n yield* options.afterCommit.pipe(Effect.ignore);\n }\n return { _tag: 'committed' as const, value: outcome.success };\n }),\n\n replay: commandId =>\n Effect.sync(() =>\n Option.flatten(\n Option.fromUndefinedOr(claims.get(`command:${commandId}`)),\n ),\n ),\n };\n\n return unitOfWork;\n};\n"],"names":["isStoreFault","Effect","Option","Result","claimKey","claim","kind","consumer","eventId","commandId","makeInMemoryUnitOfWork","seed","records","Map","outbox","eventIds","Set","claims","unitOfWork","run","body","options","gen","key","undefined","taken","get","_tag","replay","handle","emitted","outcome","result","emit","event","push","isFailure","failure","fail","reason","some","has","id","none","add","set","success","afterCommit","pipe","ignore","value","sync","flatten","fromUndefinedOr"],"mappings":"AAAA,SAA2BA,YAAY,QAAQ,gBAAgB;AAE/D,SAASC,MAAM,EAAEC,MAAM,EAAEC,MAAM,QAAQ,SAAS;AAiBhD,MAAMC,WAAW,CAACC,QAChBA,MAAMC,IAAI,KAAK,UACX,CAAC,MAAM,EAAED,MAAME,QAAQ,CAAC,CAAC,EAAEF,MAAMG,OAAO,EAAE,GAC1C,CAAC,QAAQ,EAAEH,MAAMI,SAAS,EAAE;AAElC;;;;;;;;;CASC,GACD,OAAO,MAAMC,yBAAyB,CACpCC,OAA6C,EAAE;IAE/C,IAAIC,UAAU,IAAIC,IAAqBF;IACvC,MAAMG,SAAwB,EAAE;IAChC,MAAMC,WAAW,IAAIC;IACrB,mEAAmE;IACnE,MAAMC,SAAS,IAAIJ;IAEnB,MAAMK,aAAiC;QACrC,IAAIN;YACF,OAAOA;QACT;QACA,IAAIE;YACF,OAAOA;QACT;QAEAK,KAAK,CAACC,MAAMC,UACVpB,OAAOqB,GAAG,CAAC;gBACT,MAAMC,MAAMF,CAAAA,2BAAAA,QAAShB,KAAK,IAAGD,SAASiB,QAAQhB,KAAK,IAAImB;gBACvD,MAAMC,QAAQF,QAAQC,YAAYA,YAAYP,OAAOS,GAAG,CAACH;gBACzD,IAAIE,UAAUD,WAAW;oBACvB,OAAO;wBAAEG,MAAM;wBAAsBC,QAAQH;oBAAM;gBACrD;gBAEA,MAAMI,SAA6B;oBAAEjB,SAAS,IAAIC,IAAID;gBAAS;gBAC/D,MAAMkB,UAAyB,EAAE;gBACjC,MAAMC,UAAU,OAAO9B,OAAO+B,MAAM,CAClCZ,KAAK;oBAAES;oBAAQI,MAAMC,CAAAA,QAAS,KAAKJ,QAAQK,IAAI,CAACD;gBAAO;gBAGzD,IAAI/B,OAAOiC,SAAS,CAACL,UAAU;oBAC7B,IAAI/B,aAAa+B,QAAQM,OAAO,GAAG;wBACjC,OAAO,OAAOpC,OAAOqC,IAAI,CAACP,QAAQM,OAAO;oBAC3C;oBACA,OAAO;wBACLV,MAAM;wBACNY,QAAQR,QAAQM,OAAO;oBACzB;gBACF;gBAEA,sEAAsE;gBACtE,8CAA8C;gBAC9C,IAAIP,QAAQU,IAAI,CAACN,CAAAA,QAASnB,SAAS0B,GAAG,CAACP,MAAMQ,EAAE,IAAI;oBACjD,OAAO;wBAAEf,MAAM;wBAAsBC,QAAQ1B,OAAOyC,IAAI;oBAAG;gBAC7D;gBAEA/B,UAAUiB,OAAOjB,OAAO;gBACxB,KAAK,MAAMsB,SAASJ,QAAS;oBAC3Bf,SAAS6B,GAAG,CAACV,MAAMQ,EAAE;oBACrB5B,OAAOqB,IAAI,CAACD;gBACd;gBACA,IAAIX,QAAQC,WAAW;wBAGAH;oBAFrBJ,OAAO4B,GAAG,CACRtB,KACAF,CAAAA,2BAAAA,QAASO,MAAM,KAAIP,EAAAA,iBAAAA,QAAQhB,KAAK,qBAAbgB,eAAef,IAAI,MAAK,YACvCJ,OAAOsC,IAAI,CAACnB,QAAQO,MAAM,CAACG,QAAQe,OAAO,KAC1C5C,OAAOyC,IAAI;gBAEnB;gBAEA,IAAItB,2BAAAA,QAAS0B,WAAW,EAAE;oBACxB,OAAO1B,QAAQ0B,WAAW,CAACC,IAAI,CAAC/C,OAAOgD,MAAM;gBAC/C;gBACA,OAAO;oBAAEtB,MAAM;oBAAsBuB,OAAOnB,QAAQe,OAAO;gBAAC;YAC9D;QAEFlB,QAAQnB,CAAAA,YACNR,OAAOkD,IAAI,CAAC,IACVjD,OAAOkD,OAAO,CACZlD,OAAOmD,eAAe,CAACpC,OAAOS,GAAG,CAAC,CAAC,QAAQ,EAAEjB,WAAW;IAGhE;IAEA,OAAOS;AACT,EAAE"}
@@ -14,6 +14,8 @@ export interface FakeAmqpChannel {
14
14
  exchange: string;
15
15
  routingKey: string;
16
16
  body: unknown;
17
+ /** The headers it was published with — a quarantine's reason rides here. */
18
+ headers: Record<string, unknown> | undefined;
17
19
  }>;
18
20
  /**
19
21
  * Every queue binding the adapter asked for. Recorded because the exchange is
@@ -1 +1 @@
1
- {"version":3,"file":"fake-amqp.d.ts","sourceRoot":"","sources":["../../src/drivers/fake-amqp.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AAEH,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,eAAe;IAC9B,mDAAmD;IACnD,QAAQ,CAAC,SAAS,EAAE,aAAa,CAAC;QAChC,QAAQ,EAAE,MAAM,CAAC;QACjB,UAAU,EAAE,MAAM,CAAC;QACnB,IAAI,EAAE,OAAO,CAAC;KACf,CAAC,CAAC;IACH;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC;QAC/B,KAAK,EAAE,MAAM,CAAC;QACd,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC,CAAC;IACH;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,EAAE,aAAa,CAAC;QAChC,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;KAC9C,CAAC,CAAC;IACH;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;QAC7B,KAAK,EAAE,MAAM,CAAC;QACd,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;KAC9C,CAAC,CAAC;IACH,6DAA6D;IAC7D,QAAQ,CAAC,KAAK,EAAE,eAAe,EAAE,CAAC;IAClC;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;QAC7B,OAAO,EAAE,eAAe,CAAC;QACzB,OAAO,EAAE,OAAO,CAAC;QACjB,OAAO,EAAE,OAAO,CAAC;KAClB,CAAC,CAAC;IACH,iEAAiE;IACjE,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3C;;;;;;;OAOG;IACH,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC;;;;OAIG;IACH,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3D,yDAAyD;IACzD,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3C,8EAA8E;IAC9E,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,qEAAqE;IACrE,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IAC/B,iEAAiE;IACjE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AASD,eAAO,MAAM,mBAAmB,QAAO,eA0KtC,CAAC"}
1
+ {"version":3,"file":"fake-amqp.d.ts","sourceRoot":"","sources":["../../src/drivers/fake-amqp.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AAEH,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,eAAe;IAC9B,mDAAmD;IACnD,QAAQ,CAAC,SAAS,EAAE,aAAa,CAAC;QAChC,QAAQ,EAAE,MAAM,CAAC;QACjB,UAAU,EAAE,MAAM,CAAC;QACnB,IAAI,EAAE,OAAO,CAAC;QACd,4EAA4E;QAC5E,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;KAC9C,CAAC,CAAC;IACH;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC;QAC/B,KAAK,EAAE,MAAM,CAAC;QACd,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC,CAAC;IACH;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,EAAE,aAAa,CAAC;QAChC,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;KAC9C,CAAC,CAAC;IACH;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;QAC7B,KAAK,EAAE,MAAM,CAAC;QACd,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;KAC9C,CAAC,CAAC;IACH,6DAA6D;IAC7D,QAAQ,CAAC,KAAK,EAAE,eAAe,EAAE,CAAC;IAClC;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;QAC7B,OAAO,EAAE,eAAe,CAAC;QACzB,OAAO,EAAE,OAAO,CAAC;QACjB,OAAO,EAAE,OAAO,CAAC;KAClB,CAAC,CAAC;IACH,iEAAiE;IACjE,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3C;;;;;;;OAOG;IACH,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC;;;;OAIG;IACH,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3D,yDAAyD;IACzD,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3C,8EAA8E;IAC9E,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,qEAAqE;IACrE,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IAC/B,iEAAiE;IACjE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAsBD,eAAO,MAAM,mBAAmB,QAAO,eAiLtC,CAAC"}
@@ -4,6 +4,17 @@ import { matchesEventPattern } from '@entifix/core';
4
4
  const event = body == null ? void 0 : (_body_meta = body.meta) == null ? void 0 : _body_meta.event;
5
5
  return typeof (event == null ? void 0 : event.name) === 'string' ? event.name : '';
6
6
  };
7
+ /**
8
+ * The body as JSON, or as the text it is when it is not — a poison message
9
+ * republished to quarantine is exactly the bytes that did not parse.
10
+ */ const decode = (content)=>{
11
+ const text = content.toString();
12
+ try {
13
+ return JSON.parse(text);
14
+ } catch (unused) {
15
+ return text;
16
+ }
17
+ };
7
18
  export const makeFakeAmqpChannel = ()=>{
8
19
  const published = [];
9
20
  const bindings = [];
@@ -21,12 +32,13 @@ export const makeFakeAmqpChannel = ()=>{
21
32
  if (failure !== undefined) throw failure;
22
33
  };
23
34
  const channel = {
24
- publish: (exchange, routingKey, content)=>{
35
+ publish: (exchange, routingKey, content, options)=>{
25
36
  guard();
26
37
  published.push({
27
38
  exchange,
28
39
  routingKey,
29
- body: JSON.parse(content.toString())
40
+ body: decode(content),
41
+ headers: options == null ? void 0 : options.headers
30
42
  });
31
43
  return true;
32
44
  },
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/drivers/fake-amqp.ts"],"sourcesContent":["import { matchesEventPattern } from '@entifix/core';\n\n/**\n * A fake of the amqplib channel, not of the {@link EventBus} port.\n *\n * `makeAmqpEventBus` runs on top of it, so the envelope framing, the\n * `prefetch(1)` the saga tracker depends on, and the ack/nack policy are\n * all exercised for real.\n */\n\nexport interface FakeAmqpMessage {\n content: Buffer;\n}\n\nexport interface FakeAmqpChannel {\n /** Everything published, decoded from the wire. */\n readonly published: ReadonlyArray<{\n exchange: string;\n routingKey: string;\n body: unknown;\n }>;\n /**\n * Every queue binding the adapter asked for. Recorded because the exchange is\n * a topic now: the pattern a subscriber binds is the whole of its routing, so\n * a fake that swallowed it would let a wrong binding pass every test.\n */\n readonly bindings: ReadonlyArray<{\n queue: string;\n exchange: string;\n pattern: string;\n }>;\n /**\n * Every exchange the adapter declared, with its type. Recorded because the\n * dead-letter exchange is `direct` while the event exchange is `topic`, and a\n * broker will not retype an existing exchange — declaring the wrong type is a\n * failure that only appears against a real broker.\n */\n readonly exchanges: ReadonlyArray<{\n exchange: string;\n type: string;\n options: Record<string, unknown> | undefined;\n }>;\n /**\n * Every queue the adapter declared, with the arguments it declared it with.\n * The arguments *are* the delivery policy — `x-queue-type`,\n * `x-delivery-limit`, `x-dead-letter-exchange` — so a fake that swallowed\n * them would let a queue with no retry ceiling and no dead-letter path pass\n * every test, which is the exact defect a quarantine path exists to close.\n */\n readonly queues: ReadonlyArray<{\n queue: string;\n options: Record<string, unknown> | undefined;\n }>;\n /** Messages acked, so the success policy can be asserted. */\n readonly acked: FakeAmqpMessage[];\n /**\n * Messages nacked, **with their flags**. `requeue` is the whole distinction\n * between a transient failure the broker should retry and a poison message it\n * should quarantine at once, so recording only that a nack happened is how\n * \"dead-letters a failed message\" stayed a passing test for a bus that\n * discarded it.\n */\n readonly nacked: ReadonlyArray<{\n message: FakeAmqpMessage;\n allUpTo: boolean;\n requeue: boolean;\n }>;\n /** The prefetch the adapter asked for — 1, or the fold races. */\n readonly prefetchCount: number | undefined;\n /**\n * The consumer tags cancelled, in order.\n *\n * Recorded rather than merely accepted because a graceful shutdown's first\n * phase *is* the cancel: a connector that dropped the tag on the\n * floor would pass every test while leaving the broker delivering into a\n * process that is closing its connection.\n */\n readonly cancelled: readonly string[];\n /**\n * Pushes a message to the consumers whose binding matches it, as the broker\n * would. The routing key defaults to the envelope's own `meta.event.name`,\n * which is what the adapter publishes with.\n */\n deliver(body: unknown, routingKey?: string): Promise<void>;\n /** Pushes a raw payload, for malformed-message paths. */\n deliverRaw(payload: string): Promise<void>;\n /** Simulates the broker cancelling the consumer (amqplib delivers `null`). */\n deliverCancellation(): Promise<void>;\n /** Makes every subsequent channel call throw/reject with `error`. */\n failWith(error: unknown): void;\n /** The object to pass where an amqplib `Channel` is expected. */\n readonly channel: unknown;\n}\n\n/** The routing key a body would have been published with. */\nconst routingKeyOf = (body: unknown): string => {\n const event = (body as { meta?: { event?: { name?: unknown } } })?.meta\n ?.event;\n return typeof event?.name === 'string' ? event.name : '';\n};\n\nexport const makeFakeAmqpChannel = (): FakeAmqpChannel => {\n const published: Array<{\n exchange: string;\n routingKey: string;\n body: unknown;\n }> = [];\n const bindings: Array<{ queue: string; exchange: string; pattern: string }> =\n [];\n const exchanges: Array<{\n exchange: string;\n type: string;\n options: Record<string, unknown> | undefined;\n }> = [];\n const queues: Array<{\n queue: string;\n options: Record<string, unknown> | undefined;\n }> = [];\n const acked: FakeAmqpMessage[] = [];\n const nacked: Array<{\n message: FakeAmqpMessage;\n allUpTo: boolean;\n requeue: boolean;\n }> = [];\n let consumer: ((message: FakeAmqpMessage | null) => void) | undefined;\n const cancelled: string[] = [];\n let consumerTags = 0;\n let boundQueue: string | undefined;\n let prefetchCount: number | undefined;\n let failure: unknown;\n\n const guard = () => {\n if (failure !== undefined) throw failure;\n };\n\n const channel = {\n publish: (exchange: string, routingKey: string, content: Buffer) => {\n guard();\n published.push({\n exchange,\n routingKey,\n body: JSON.parse(content.toString()) as unknown,\n });\n return true;\n },\n prefetch: async (count: number) => {\n guard();\n prefetchCount = count;\n },\n assertExchange: async (\n exchange: string,\n type: string,\n options?: Record<string, unknown>,\n ) => {\n guard();\n exchanges.push({ exchange, type, options });\n return { exchange };\n },\n assertQueue: async (queue: string, options?: Record<string, unknown>) => {\n guard();\n const name = queue === '' ? 'amq.gen-fake' : queue;\n queues.push({ queue: name, options });\n return { queue: name };\n },\n bindQueue: async (queue: string, exchange: string, pattern: string) => {\n guard();\n bindings.push({ queue, exchange, pattern });\n },\n consume: async (\n queue: string,\n handler: (message: FakeAmqpMessage | null) => void,\n ) => {\n guard();\n consumer = handler;\n boundQueue = queue;\n // A fresh tag per `consume`, so a reconnect's re-bind is distinguishable\n // from the binding it replaced — the connector cancels what it currently\n // holds, and a constant tag would hide it cancelling a stale one.\n consumerTags += 1;\n return { consumerTag: `fake-consumer-${consumerTags}` };\n },\n cancel: async (consumerTag: string) => {\n guard();\n cancelled.push(consumerTag);\n consumer = undefined;\n },\n ack: (message: FakeAmqpMessage) => {\n acked.push(message);\n },\n nack: (message: FakeAmqpMessage, allUpTo: boolean, requeue: boolean) => {\n nacked.push({ message, allUpTo, requeue });\n },\n close: async () => {\n guard();\n },\n };\n\n /**\n * The adapter acks/nacks from inside a floating promise chain, so a delivery\n * is only settled once the microtask queue drains.\n */\n const settle = () => new Promise<void>(resolve => setTimeout(resolve, 0));\n\n /**\n * Whether the consumer's queue is bound to a pattern matching `routingKey`.\n * The broker decides this in production, so the fake has to as well — without\n * it every subscriber sees every message and the topic exchange is untested.\n */\n const routes = (routingKey: string) =>\n bindings.some(\n binding =>\n binding.queue === boundQueue &&\n matchesEventPattern(binding.pattern, routingKey),\n );\n\n /**\n * `routingKey === undefined` means \"the broker already routed this\" — used by\n * the malformed-payload path, where the message reached the queue and it is\n * the *content* that is wrong.\n */\n const push = async (payload: string, routingKey: string | undefined) => {\n if (consumer === undefined) {\n throw new Error('fake amqp: nothing subscribed yet');\n }\n if (routingKey !== undefined && !routes(routingKey)) {\n return;\n }\n consumer({ content: Buffer.from(payload) });\n await settle();\n };\n\n return {\n get published() {\n return published;\n },\n get bindings() {\n return bindings;\n },\n get exchanges() {\n return exchanges;\n },\n get queues() {\n return queues;\n },\n get acked() {\n return acked;\n },\n get nacked() {\n return nacked;\n },\n get prefetchCount() {\n return prefetchCount;\n },\n get cancelled() {\n return cancelled;\n },\n deliver: (body, routingKey) =>\n push(JSON.stringify(body), routingKey ?? routingKeyOf(body)),\n deliverRaw: payload => push(payload, undefined),\n deliverCancellation: async () => {\n if (consumer === undefined) {\n throw new Error('fake amqp: nothing subscribed yet');\n }\n consumer(null);\n await settle();\n },\n failWith: error => {\n failure = error;\n },\n channel,\n };\n};\n"],"names":["matchesEventPattern","routingKeyOf","body","event","meta","name","makeFakeAmqpChannel","published","bindings","exchanges","queues","acked","nacked","consumer","cancelled","consumerTags","boundQueue","prefetchCount","failure","guard","undefined","channel","publish","exchange","routingKey","content","push","JSON","parse","toString","prefetch","count","assertExchange","type","options","assertQueue","queue","bindQueue","pattern","consume","handler","consumerTag","cancel","ack","message","nack","allUpTo","requeue","close","settle","Promise","resolve","setTimeout","routes","some","binding","payload","Error","Buffer","from","deliver","stringify","deliverRaw","deliverCancellation","failWith","error"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/drivers/fake-amqp.ts"],"sourcesContent":["import { matchesEventPattern } from '@entifix/core';\n\n/**\n * A fake of the amqplib channel, not of the {@link EventBus} port.\n *\n * `makeAmqpEventBus` runs on top of it, so the envelope framing, the\n * `prefetch(1)` the saga tracker depends on, and the ack/nack policy are\n * all exercised for real.\n */\n\nexport interface FakeAmqpMessage {\n content: Buffer;\n}\n\nexport interface FakeAmqpChannel {\n /** Everything published, decoded from the wire. */\n readonly published: ReadonlyArray<{\n exchange: string;\n routingKey: string;\n body: unknown;\n /** The headers it was published with — a quarantine's reason rides here. */\n headers: Record<string, unknown> | undefined;\n }>;\n /**\n * Every queue binding the adapter asked for. Recorded because the exchange is\n * a topic now: the pattern a subscriber binds is the whole of its routing, so\n * a fake that swallowed it would let a wrong binding pass every test.\n */\n readonly bindings: ReadonlyArray<{\n queue: string;\n exchange: string;\n pattern: string;\n }>;\n /**\n * Every exchange the adapter declared, with its type. Recorded because the\n * dead-letter exchange is `direct` while the event exchange is `topic`, and a\n * broker will not retype an existing exchange — declaring the wrong type is a\n * failure that only appears against a real broker.\n */\n readonly exchanges: ReadonlyArray<{\n exchange: string;\n type: string;\n options: Record<string, unknown> | undefined;\n }>;\n /**\n * Every queue the adapter declared, with the arguments it declared it with.\n * The arguments *are* the delivery policy — `x-queue-type`,\n * `x-delivery-limit`, `x-dead-letter-exchange` — so a fake that swallowed\n * them would let a queue with no retry ceiling and no dead-letter path pass\n * every test, which is the exact defect a quarantine path exists to close.\n */\n readonly queues: ReadonlyArray<{\n queue: string;\n options: Record<string, unknown> | undefined;\n }>;\n /** Messages acked, so the success policy can be asserted. */\n readonly acked: FakeAmqpMessage[];\n /**\n * Messages nacked, **with their flags**. `requeue` is the whole distinction\n * between a transient failure the broker should retry and a poison message it\n * should quarantine at once, so recording only that a nack happened is how\n * \"dead-letters a failed message\" stayed a passing test for a bus that\n * discarded it.\n */\n readonly nacked: ReadonlyArray<{\n message: FakeAmqpMessage;\n allUpTo: boolean;\n requeue: boolean;\n }>;\n /** The prefetch the adapter asked for — 1, or the fold races. */\n readonly prefetchCount: number | undefined;\n /**\n * The consumer tags cancelled, in order.\n *\n * Recorded rather than merely accepted because a graceful shutdown's first\n * phase *is* the cancel: a connector that dropped the tag on the\n * floor would pass every test while leaving the broker delivering into a\n * process that is closing its connection.\n */\n readonly cancelled: readonly string[];\n /**\n * Pushes a message to the consumers whose binding matches it, as the broker\n * would. The routing key defaults to the envelope's own `meta.event.name`,\n * which is what the adapter publishes with.\n */\n deliver(body: unknown, routingKey?: string): Promise<void>;\n /** Pushes a raw payload, for malformed-message paths. */\n deliverRaw(payload: string): Promise<void>;\n /** Simulates the broker cancelling the consumer (amqplib delivers `null`). */\n deliverCancellation(): Promise<void>;\n /** Makes every subsequent channel call throw/reject with `error`. */\n failWith(error: unknown): void;\n /** The object to pass where an amqplib `Channel` is expected. */\n readonly channel: unknown;\n}\n\n/** The routing key a body would have been published with. */\nconst routingKeyOf = (body: unknown): string => {\n const event = (body as { meta?: { event?: { name?: unknown } } })?.meta\n ?.event;\n return typeof event?.name === 'string' ? event.name : '';\n};\n\n/**\n * The body as JSON, or as the text it is when it is not — a poison message\n * republished to quarantine is exactly the bytes that did not parse.\n */\nconst decode = (content: Buffer): unknown => {\n const text = content.toString();\n try {\n return JSON.parse(text) as unknown;\n } catch {\n return text;\n }\n};\n\nexport const makeFakeAmqpChannel = (): FakeAmqpChannel => {\n const published: Array<{\n exchange: string;\n routingKey: string;\n body: unknown;\n headers: Record<string, unknown> | undefined;\n }> = [];\n const bindings: Array<{ queue: string; exchange: string; pattern: string }> =\n [];\n const exchanges: Array<{\n exchange: string;\n type: string;\n options: Record<string, unknown> | undefined;\n }> = [];\n const queues: Array<{\n queue: string;\n options: Record<string, unknown> | undefined;\n }> = [];\n const acked: FakeAmqpMessage[] = [];\n const nacked: Array<{\n message: FakeAmqpMessage;\n allUpTo: boolean;\n requeue: boolean;\n }> = [];\n let consumer: ((message: FakeAmqpMessage | null) => void) | undefined;\n const cancelled: string[] = [];\n let consumerTags = 0;\n let boundQueue: string | undefined;\n let prefetchCount: number | undefined;\n let failure: unknown;\n\n const guard = () => {\n if (failure !== undefined) throw failure;\n };\n\n const channel = {\n publish: (\n exchange: string,\n routingKey: string,\n content: Buffer,\n options?: { headers?: Record<string, unknown> },\n ) => {\n guard();\n published.push({\n exchange,\n routingKey,\n body: decode(content),\n headers: options?.headers,\n });\n return true;\n },\n prefetch: async (count: number) => {\n guard();\n prefetchCount = count;\n },\n assertExchange: async (\n exchange: string,\n type: string,\n options?: Record<string, unknown>,\n ) => {\n guard();\n exchanges.push({ exchange, type, options });\n return { exchange };\n },\n assertQueue: async (queue: string, options?: Record<string, unknown>) => {\n guard();\n const name = queue === '' ? 'amq.gen-fake' : queue;\n queues.push({ queue: name, options });\n return { queue: name };\n },\n bindQueue: async (queue: string, exchange: string, pattern: string) => {\n guard();\n bindings.push({ queue, exchange, pattern });\n },\n consume: async (\n queue: string,\n handler: (message: FakeAmqpMessage | null) => void,\n ) => {\n guard();\n consumer = handler;\n boundQueue = queue;\n // A fresh tag per `consume`, so a reconnect's re-bind is distinguishable\n // from the binding it replaced — the connector cancels what it currently\n // holds, and a constant tag would hide it cancelling a stale one.\n consumerTags += 1;\n return { consumerTag: `fake-consumer-${consumerTags}` };\n },\n cancel: async (consumerTag: string) => {\n guard();\n cancelled.push(consumerTag);\n consumer = undefined;\n },\n ack: (message: FakeAmqpMessage) => {\n acked.push(message);\n },\n nack: (message: FakeAmqpMessage, allUpTo: boolean, requeue: boolean) => {\n nacked.push({ message, allUpTo, requeue });\n },\n close: async () => {\n guard();\n },\n };\n\n /**\n * The adapter acks/nacks from inside a floating promise chain, so a delivery\n * is only settled once the microtask queue drains.\n */\n const settle = () => new Promise<void>(resolve => setTimeout(resolve, 0));\n\n /**\n * Whether the consumer's queue is bound to a pattern matching `routingKey`.\n * The broker decides this in production, so the fake has to as well — without\n * it every subscriber sees every message and the topic exchange is untested.\n */\n const routes = (routingKey: string) =>\n bindings.some(\n binding =>\n binding.queue === boundQueue &&\n matchesEventPattern(binding.pattern, routingKey),\n );\n\n /**\n * `routingKey === undefined` means \"the broker already routed this\" — used by\n * the malformed-payload path, where the message reached the queue and it is\n * the *content* that is wrong.\n */\n const push = async (payload: string, routingKey: string | undefined) => {\n if (consumer === undefined) {\n throw new Error('fake amqp: nothing subscribed yet');\n }\n if (routingKey !== undefined && !routes(routingKey)) {\n return;\n }\n consumer({ content: Buffer.from(payload) });\n await settle();\n };\n\n return {\n get published() {\n return published;\n },\n get bindings() {\n return bindings;\n },\n get exchanges() {\n return exchanges;\n },\n get queues() {\n return queues;\n },\n get acked() {\n return acked;\n },\n get nacked() {\n return nacked;\n },\n get prefetchCount() {\n return prefetchCount;\n },\n get cancelled() {\n return cancelled;\n },\n deliver: (body, routingKey) =>\n push(JSON.stringify(body), routingKey ?? routingKeyOf(body)),\n deliverRaw: payload => push(payload, undefined),\n deliverCancellation: async () => {\n if (consumer === undefined) {\n throw new Error('fake amqp: nothing subscribed yet');\n }\n consumer(null);\n await settle();\n },\n failWith: error => {\n failure = error;\n },\n channel,\n };\n};\n"],"names":["matchesEventPattern","routingKeyOf","body","event","meta","name","decode","content","text","toString","JSON","parse","makeFakeAmqpChannel","published","bindings","exchanges","queues","acked","nacked","consumer","cancelled","consumerTags","boundQueue","prefetchCount","failure","guard","undefined","channel","publish","exchange","routingKey","options","push","headers","prefetch","count","assertExchange","type","assertQueue","queue","bindQueue","pattern","consume","handler","consumerTag","cancel","ack","message","nack","allUpTo","requeue","close","settle","Promise","resolve","setTimeout","routes","some","binding","payload","Error","Buffer","from","deliver","stringify","deliverRaw","deliverCancellation","failWith","error"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"fake-mongo.d.ts","sourceRoot":"","sources":["../../src/drivers/fake-mongo.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,KAAK,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAuMxC,MAAM,WAAW,mBAAmB;IAClC,uEAAuE;IACvE,QAAQ,CAAC,SAAS,EAAE,QAAQ,EAAE,CAAC;CAChC;AAED,MAAM,WAAW,WAAW;IAC1B,wCAAwC;IACxC,IAAI,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAC;IACtD,6CAA6C;IAC7C,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,QAAQ,EAAE,CAAC;IACrC;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IAC/B;;;OAGG;IACH,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IAChD;;;OAGG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IAClD,4EAA4E;IAC5E,QAAQ,CAAC,UAAU,EAAE,aAAa,CAAC;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACvE;;;;OAIG;IACH,YAAY,IAAI;QACd,eAAe,CAAC,IAAI,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;QAChE;;;;;WAKG;QACH,gBAAgB,IAAI,IAAI,CAAC;QACzB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;QACnC,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;QAClC,aAAa,IAAI,OAAO,CAAC;QACzB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;QAC5B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;KAC5B,CAAC;IACF,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;CACtB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,GAC1B,OAAM,MAAM,CAAC,MAAM,EAAE,QAAQ,EAAE,CAAM,KACpC,WA6lBF,CAAC"}
1
+ {"version":3,"file":"fake-mongo.d.ts","sourceRoot":"","sources":["../../src/drivers/fake-mongo.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,KAAK,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AA0RxC,MAAM,WAAW,mBAAmB;IAClC,uEAAuE;IACvE,QAAQ,CAAC,SAAS,EAAE,QAAQ,EAAE,CAAC;CAChC;AAED,MAAM,WAAW,WAAW;IAC1B,wCAAwC;IACxC,IAAI,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAC;IACtD,6CAA6C;IAC7C,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,QAAQ,EAAE,CAAC;IACrC;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IAC/B;;;OAGG;IACH,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IAChD;;;OAGG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IAClD,4EAA4E;IAC5E,QAAQ,CAAC,UAAU,EAAE,aAAa,CAAC;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACvE;;;;OAIG;IACH,YAAY,IAAI;QACd,eAAe,CAAC,IAAI,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;QAChE;;;;;WAKG;QACH,gBAAgB,IAAI,IAAI,CAAC;QACzB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;QACnC,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;QAClC,aAAa,IAAI,OAAO,CAAC;QACzB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;QAC5B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;KAC5B,CAAC;IACF,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;CACtB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,GAC1B,OAAM,MAAM,CAAC,MAAM,EAAE,QAAQ,EAAE,CAAM,KACpC,WA0mBF,CAAC"}
@@ -9,6 +9,7 @@
9
9
  * Only the surface the adapter uses is implemented; anything else is a
10
10
  * deliberate `TypeError` rather than a silent no-op.
11
11
  */ /** The subset of a Mongo query document the adapter produces. */ import { _ as _extends } from "@swc/helpers/_/_extends";
12
+ import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
12
13
  const isPlainObject = (value)=>typeof value === 'object' && value !== null && !Array.isArray(value);
13
14
  const compare = (left, right)=>{
14
15
  if (left == null && right == null) return 0;
@@ -168,6 +169,80 @@ const matches = (doc, query)=>Object.entries(query).every(([field, condition])=>
168
169
  // has a line for `a`, not include it because some other line differs.
169
170
  return isNegativeCondition(condition) ? candidates.every((value)=>matchesCondition(value, condition)) : candidates.some((value)=>matchesCondition(value, condition));
170
171
  });
172
+ /** A `$field` reference to a top-level field, the only one the stages take. */ const fieldOf = (reference, stage)=>{
173
+ if (typeof reference !== 'string' || !reference.startsWith('$') || reference.includes('.')) {
174
+ throw new TypeError(`fake mongo: ${stage} takes a top-level "$field", got ${JSON.stringify(reference)}`);
175
+ }
176
+ return reference.slice(1);
177
+ };
178
+ /**
179
+ * `$unwind` without `preserveNullAndEmptyArrays`: an array becomes one
180
+ * document per element, a missing, `null` or empty one drops the document, and
181
+ * any other value passes through as it is.
182
+ */ const unwind = (docs, spec)=>{
183
+ const options = isPlainObject(spec) ? spec : {
184
+ path: spec
185
+ };
186
+ if (options['preserveNullAndEmptyArrays'] === true) {
187
+ throw new TypeError('fake mongo: $unwind cannot preserve nulls');
188
+ }
189
+ const field = fieldOf(options['path'], '$unwind');
190
+ return docs.flatMap((doc)=>{
191
+ const value = doc[field];
192
+ if (value == null) return [];
193
+ if (!Array.isArray(value)) return [
194
+ doc
195
+ ];
196
+ return value.map((element)=>_extends({}, doc, {
197
+ [field]: element
198
+ }));
199
+ });
200
+ };
201
+ /** `$group` by one field, counting with `{ $sum: 1 }`; a missing key is `null`. */ const group = (docs, spec)=>{
202
+ const _ref = isPlainObject(spec) ? spec : {}, { _id: key } = _ref, accumulators = _object_without_properties_loose(_ref, [
203
+ "_id"
204
+ ]);
205
+ const field = fieldOf(key, '$group');
206
+ const entries = Object.entries(accumulators);
207
+ if (entries.length !== 1 || !isPlainObject(entries[0][1]) || entries[0][1]['$sum'] !== 1) {
208
+ throw new TypeError('fake mongo: $group supports one accumulator, { $sum: 1 }');
209
+ }
210
+ const [counted] = entries[0];
211
+ const tally = new Map();
212
+ for (const doc of docs){
213
+ var _doc_field, _tally_get;
214
+ const value = (_doc_field = doc[field]) != null ? _doc_field : null;
215
+ tally.set(value, ((_tally_get = tally.get(value)) != null ? _tally_get : 0) + 1);
216
+ }
217
+ return Array.from(tally, ([id, count])=>({
218
+ _id: id,
219
+ [counted]: count
220
+ }));
221
+ };
222
+ const runPipeline = (docs, pipeline)=>pipeline.reduce((current, stage)=>{
223
+ const entries = Object.entries(stage);
224
+ if (entries.length !== 1) {
225
+ throw new TypeError('fake mongo: a stage has exactly one operator');
226
+ }
227
+ const [operator, spec] = entries[0];
228
+ switch(operator){
229
+ case '$match':
230
+ return current.filter((doc)=>matches(doc, spec));
231
+ case '$unwind':
232
+ return unwind(current, spec);
233
+ case '$group':
234
+ return group(current, spec);
235
+ case '$facet':
236
+ return [
237
+ Object.fromEntries(Object.entries(spec).map(([output, branch])=>[
238
+ output,
239
+ runPipeline(current, branch)
240
+ ]))
241
+ ];
242
+ default:
243
+ throw new TypeError(`fake mongo: unsupported stage "${operator}"`);
244
+ }
245
+ }, docs);
171
246
  /**
172
247
  * The error a real server raises when a write violates a unique index.
173
248
  *
@@ -298,6 +373,14 @@ const matches = (doc, query)=>Object.entries(query).every(([field, condition])=>
298
373
  };
299
374
  return cursor;
300
375
  },
376
+ /**
377
+ * The four stages a facet count sends — `$facet`, `$match`, `$unwind` and
378
+ * a `$group` counting with `{ $sum: 1 }` — and nothing else: an unknown
379
+ * stage, or a stage in a shape not listed here, is a `TypeError` rather
380
+ * than a pass-through, for the reason `$expr` above gives.
381
+ */ aggregate: (pipeline)=>({
382
+ toArray: ()=>record(name, 'aggregate', ()=>runPipeline(documentsOf(name), pipeline))
383
+ }),
301
384
  findOne: (query, options)=>record(name, 'findOne', ()=>{
302
385
  const found = documentsOf(name).find((doc)=>matches(doc, query));
303
386
  return found === undefined ? null : project(found, options);