@entifix/mongo 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 (65) hide show
  1. package/README.md +4 -4
  2. package/dist/adapters/filter-translator.d.ts +1 -1
  3. package/dist/adapters/filter-translator.d.ts.map +1 -1
  4. package/dist/adapters/filter-translator.js +6 -5
  5. package/dist/adapters/filter-translator.js.map +1 -1
  6. package/dist/adapters/make-mongo-facet-counter.d.ts +18 -0
  7. package/dist/adapters/make-mongo-facet-counter.d.ts.map +1 -0
  8. package/dist/adapters/make-mongo-facet-counter.js +76 -0
  9. package/dist/adapters/make-mongo-facet-counter.js.map +1 -0
  10. package/dist/adapters/make-mongo-link-resolver.d.ts.map +1 -1
  11. package/dist/adapters/make-mongo-link-resolver.js +2 -4
  12. package/dist/adapters/make-mongo-link-resolver.js.map +1 -1
  13. package/dist/adapters/make-mongo-repository.d.ts +5 -3
  14. package/dist/adapters/make-mongo-repository.d.ts.map +1 -1
  15. package/dist/adapters/make-mongo-repository.js +13 -20
  16. package/dist/adapters/make-mongo-repository.js.map +1 -1
  17. package/dist/adapters/mongo-facet-counter-factory.d.ts +18 -0
  18. package/dist/adapters/mongo-facet-counter-factory.d.ts.map +1 -0
  19. package/dist/adapters/mongo-facet-counter-factory.js +29 -0
  20. package/dist/adapters/mongo-facet-counter-factory.js.map +1 -0
  21. package/dist/adapters/mongo-repository-factory.d.ts.map +1 -1
  22. package/dist/adapters/mongo-repository-factory.js +2 -4
  23. package/dist/adapters/mongo-repository-factory.js.map +1 -1
  24. package/dist/errors/translate-mongo-error.d.ts +30 -0
  25. package/dist/errors/translate-mongo-error.d.ts.map +1 -0
  26. package/dist/errors/translate-mongo-error.js +74 -0
  27. package/dist/errors/translate-mongo-error.js.map +1 -0
  28. package/dist/inbox/store.d.ts +1 -2
  29. package/dist/inbox/store.d.ts.map +1 -1
  30. package/dist/inbox/store.js +3 -8
  31. package/dist/inbox/store.js.map +1 -1
  32. package/dist/index.d.ts +2 -0
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +2 -0
  35. package/dist/index.js.map +1 -1
  36. package/dist/mongo-database/mongo-database.d.ts +3 -3
  37. package/dist/mongo-database/mongo-database.d.ts.map +1 -1
  38. package/dist/mongo-database/mongo-database.js +2 -2
  39. package/dist/mongo-database/mongo-database.js.map +1 -1
  40. package/dist/mongo-database/mongo-tenant-directory.d.ts.map +1 -1
  41. package/dist/mongo-database/mongo-tenant-directory.js +2 -4
  42. package/dist/mongo-database/mongo-tenant-directory.js.map +1 -1
  43. package/dist/mongo-database/mongo-tenant-resolver.d.ts.map +1 -1
  44. package/dist/mongo-database/mongo-tenant-resolver.js +18 -7
  45. package/dist/mongo-database/mongo-tenant-resolver.js.map +1 -1
  46. package/dist/outbox/relay.d.ts +50 -9
  47. package/dist/outbox/relay.d.ts.map +1 -1
  48. package/dist/outbox/relay.js +67 -26
  49. package/dist/outbox/relay.js.map +1 -1
  50. package/dist/outbox/store.d.ts +3 -3
  51. package/dist/outbox/store.d.ts.map +1 -1
  52. package/dist/outbox/store.js +8 -22
  53. package/dist/outbox/store.js.map +1 -1
  54. package/dist/saga/store.d.ts +3 -4
  55. package/dist/saga/store.d.ts.map +1 -1
  56. package/dist/saga/store.js +10 -15
  57. package/dist/saga/store.js.map +1 -1
  58. package/dist/unit-of-work/command-inbox.d.ts +2 -3
  59. package/dist/unit-of-work/command-inbox.d.ts.map +1 -1
  60. package/dist/unit-of-work/command-inbox.js +3 -7
  61. package/dist/unit-of-work/command-inbox.js.map +1 -1
  62. package/dist/unit-of-work/mongo-unit-of-work.d.ts.map +1 -1
  63. package/dist/unit-of-work/mongo-unit-of-work.js +30 -26
  64. package/dist/unit-of-work/mongo-unit-of-work.js.map +1 -1
  65. package/package.json +8 -8
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/saga/store.ts"],"sourcesContent":["import { EntifixConnError } from '@entifix/core';\nimport {\n type SagaInstance,\n type SagaState,\n type SagaStepOutcome,\n type SagaStore,\n SagaStoreTag,\n} from '@entifix/transactions';\nimport { Effect, Layer } from 'effect';\nimport type { Db } from 'mongodb';\n\nimport {\n MongoClientTag,\n MongoDatabaseTag,\n} from '../mongo-database/mongo-database.js';\n\n/**\n * Saga instances: one document per flow, the durable record a coordinator\n * resumes from, so where to resume is derived from the outcomes rather than\n * stored beside them.\n */\nexport const SAGA_COLLECTION = 'saga_instances';\n\n/** The two states a sweep treats as unfinished. */\nconst UNFINISHED: readonly SagaState[] = ['RUNNING', 'COMPENSATING'];\n\nconst failed = (message: string, sagaId: string) => (error: unknown) =>\n new EntifixConnError(message, error, { sagaId });\n\nconst sagaCollection = (db: Db) => db.collection<SagaInstance>(SAGA_COLLECTION);\n\nconst deadline = (olderThanMs: number) =>\n new Date(Date.now() - olderThanMs).toISOString();\n\n/**\n * A unique index on `sagaId`, so a resumed coordinator can never create a\n * second instance for one flow, and `{ state, updatedAt }` for the sweep —\n * without it every pass is a scan of every saga ever run, on a schedule.\n */\nexport const ensureSagaIndexes = (db: Db) =>\n Effect.tryPromise({\n try: async () => {\n await sagaCollection(db).createIndex({ sagaId: 1 }, { unique: true });\n await sagaCollection(db).createIndex({ state: 1, updatedAt: 1 });\n },\n catch: error =>\n new EntifixConnError('Failed to ensure saga indexes', error, {\n database: db.databaseName,\n }),\n });\n\n/** {@link SagaStore} over one Mongo database. */\nexport const makeMongoSagaStore = (db: Db): SagaStore => {\n const collection = sagaCollection(db);\n const stamp = () => new Date().toISOString();\n\n return {\n start: instance =>\n Effect.tryPromise({\n try: async () => {\n // `$setOnInsert` with an upsert rather than `insertOne`: a coordinator\n // restarted mid-flight re-enters with an id it already wrote, and\n // refusing that would turn a recoverable resume into a hard failure.\n await collection.updateOne(\n { sagaId: instance.sagaId },\n { $setOnInsert: { ...instance, updatedAt: stamp() } },\n { upsert: true },\n );\n },\n catch: failed('Failed to start the saga instance', instance.sagaId),\n }),\n\n beginStep: (sagaId, stepIndex) =>\n Effect.tryPromise({\n try: async () => {\n await collection.updateOne(\n { sagaId },\n { $set: { stepIndex, updatedAt: stamp() } },\n );\n },\n catch: failed('Failed to record the step transition', sagaId),\n }),\n\n recordOutcome: (sagaId, outcome: SagaStepOutcome) =>\n Effect.tryPromise({\n try: async () => {\n await collection.updateOne(\n { sagaId },\n // `$push`, never a read-modify-write of the array: two outcomes\n // recorded from one process would otherwise lose one another, and a\n // lost outcome is a compensation that cannot address what it undoes.\n { $push: { outcomes: outcome }, $set: { updatedAt: stamp() } },\n );\n },\n catch: failed('Failed to record the step outcome', sagaId),\n }),\n\n settle: (sagaId, state, error) =>\n Effect.tryPromise({\n try: async () => {\n await collection.updateOne(\n { sagaId },\n { $set: { state, error, updatedAt: stamp() } },\n );\n },\n catch: failed('Failed to settle the saga instance', sagaId),\n }),\n\n /**\n * Take ownership of a stale instance, or answer `undefined`.\n *\n * ⚠️ **One conditional write, not a read and then a write.** The filter\n * repeats `findStale`'s predicate, so two sweepers racing the same instance\n * produce one winner: the loser's filter no longer matches because the\n * winner re-stamped `updatedAt`. That stamp is also what stops the next tick\n * finding the instance this sweep is still working on.\n */\n claimForResume: (sagaId, olderThanMs) =>\n Effect.tryPromise({\n try: async () => {\n const claimed = await collection.findOneAndUpdate(\n {\n sagaId,\n state: { $in: [...UNFINISHED] },\n updatedAt: { $lt: deadline(olderThanMs) },\n },\n { $inc: { resumeAttempts: 1 }, $set: { updatedAt: stamp() } },\n { returnDocument: 'after', projection: { _id: 0 } },\n );\n return (claimed ?? undefined) as SagaInstance | undefined;\n },\n catch: failed('Failed to claim the saga instance', sagaId),\n }),\n\n /**\n * Flag one step's calls as given back, addressing the outcome by its step\n * id: outcomes are `$push`ed, so an index is a fact about arrival order and\n * a resumed walk must not depend on one.\n */\n markCompensated: (sagaId, stepId) =>\n Effect.tryPromise({\n try: async () => {\n await collection.updateOne(\n { sagaId },\n {\n $set: {\n 'outcomes.$[entry].compensated': true,\n updatedAt: stamp(),\n },\n },\n { arrayFilters: [{ 'entry.stepId': stepId }] },\n );\n },\n catch: failed('Failed to mark the step compensated', sagaId),\n }),\n\n get: sagaId =>\n Effect.tryPromise({\n try: async () =>\n ((await collection.findOne({ sagaId }, { projection: { _id: 0 } })) ??\n undefined) as SagaInstance | undefined,\n catch: failed('Failed to read the saga instance', sagaId),\n }),\n\n /**\n * Instances stuck mid-flight past a deadline. ⚠️ `COMPENSATING` counts: a\n * coordinator that died while unwinding is the state that leaves holds in\n * place, and the one an operator most needs to find.\n */\n findStale: olderThanMs =>\n Effect.tryPromise({\n try: async () =>\n (await collection\n .find(\n {\n state: { $in: [...UNFINISHED] },\n updatedAt: { $lt: deadline(olderThanMs) },\n },\n { projection: { _id: 0 } },\n )\n .toArray()) as SagaInstance[],\n catch: failed('Failed to find stale saga instances', ''),\n }),\n };\n};\n\n/**\n * Provides {@link SagaStoreTag} from the service's own database, creating the\n * indexes first.\n */\nexport const MongoSagaStoreLayer = Layer.effect(\n SagaStoreTag,\n Effect.gen(function* () {\n const db = yield* MongoDatabaseTag;\n yield* ensureSagaIndexes(db);\n return makeMongoSagaStore(db);\n }),\n);\n\n/**\n * Provides {@link SagaStoreTag} from a database the host **names**, resolved on\n * the shared client — for a process whose saga store is one of several stores\n * it holds, rather than the one database {@link MongoDatabaseTag} carries.\n *\n * Same pool, same indexes, same store; only where the handle comes from\n * differs.\n */\nexport const mongoSagaStoreLayer = (options: { readonly database: string }) =>\n Layer.effect(\n SagaStoreTag,\n Effect.gen(function* () {\n const client = yield* MongoClientTag;\n const db = client.db(options.database);\n yield* ensureSagaIndexes(db);\n return makeMongoSagaStore(db);\n }),\n );\n"],"names":["EntifixConnError","SagaStoreTag","Effect","Layer","MongoClientTag","MongoDatabaseTag","SAGA_COLLECTION","UNFINISHED","failed","message","sagaId","error","sagaCollection","db","collection","deadline","olderThanMs","Date","now","toISOString","ensureSagaIndexes","tryPromise","try","createIndex","unique","state","updatedAt","catch","database","databaseName","makeMongoSagaStore","stamp","start","instance","updateOne","$setOnInsert","upsert","beginStep","stepIndex","$set","recordOutcome","outcome","$push","outcomes","settle","claimForResume","claimed","findOneAndUpdate","$in","$lt","$inc","resumeAttempts","returnDocument","projection","_id","undefined","markCompensated","stepId","arrayFilters","get","findOne","findStale","find","toArray","MongoSagaStoreLayer","effect","gen","mongoSagaStoreLayer","options","client"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/saga/store.ts"],"sourcesContent":["import {\n type SagaInstance,\n type SagaState,\n type SagaStepOutcome,\n type SagaStore,\n SagaStoreTag,\n} from '@entifix/transactions';\nimport { Effect, Layer } from 'effect';\nimport type { Db } from 'mongodb';\n\nimport { translateMongoError } from '../errors/translate-mongo-error.js';\nimport {\n MongoClientTag,\n MongoDatabaseTag,\n} from '../mongo-database/mongo-database.js';\n\n/**\n * Saga instances: one document per flow, the durable record a coordinator\n * resumes from, so where to resume is derived from the outcomes rather than\n * stored beside them.\n */\nexport const SAGA_COLLECTION = 'saga_instances';\n\n/** The two states a sweep treats as unfinished. */\nconst UNFINISHED: readonly SagaState[] = ['RUNNING', 'COMPENSATING'];\n\nconst sagaCollection = (db: Db) => db.collection<SagaInstance>(SAGA_COLLECTION);\n\nconst deadline = (olderThanMs: number) =>\n new Date(Date.now() - olderThanMs).toISOString();\n\n/**\n * A unique index on `sagaId`, so a resumed coordinator can never create a\n * second instance for one flow, and `{ state, updatedAt }` for the sweep —\n * without it every pass is a scan of every saga ever run, on a schedule.\n */\nexport const ensureSagaIndexes = (db: Db) =>\n Effect.tryPromise({\n try: async () => {\n await sagaCollection(db).createIndex({ sagaId: 1 }, { unique: true });\n await sagaCollection(db).createIndex({ state: 1, updatedAt: 1 });\n },\n catch: error => translateMongoError(error),\n });\n\n/** {@link SagaStore} over one Mongo database. */\nexport const makeMongoSagaStore = (db: Db): SagaStore => {\n const collection = sagaCollection(db);\n const stamp = () => new Date().toISOString();\n\n return {\n start: instance =>\n Effect.tryPromise({\n try: async () => {\n // `$setOnInsert` with an upsert rather than `insertOne`: a coordinator\n // restarted mid-flight re-enters with an id it already wrote, and\n // refusing that would turn a recoverable resume into a hard failure.\n await collection.updateOne(\n { sagaId: instance.sagaId },\n { $setOnInsert: { ...instance, updatedAt: stamp() } },\n { upsert: true },\n );\n },\n catch: error => translateMongoError(error),\n }),\n\n beginStep: (sagaId, stepIndex) =>\n Effect.tryPromise({\n try: async () => {\n await collection.updateOne(\n { sagaId },\n { $set: { stepIndex, updatedAt: stamp() } },\n );\n },\n catch: error => translateMongoError(error),\n }),\n\n recordOutcome: (sagaId, outcome: SagaStepOutcome) =>\n Effect.tryPromise({\n try: async () => {\n await collection.updateOne(\n { sagaId },\n // `$push`, never a read-modify-write of the array: two outcomes\n // recorded from one process would otherwise lose one another, and a\n // lost outcome is a compensation that cannot address what it undoes.\n { $push: { outcomes: outcome }, $set: { updatedAt: stamp() } },\n );\n },\n catch: error => translateMongoError(error),\n }),\n\n settle: (sagaId, state, error) =>\n Effect.tryPromise({\n try: async () => {\n await collection.updateOne(\n { sagaId },\n { $set: { state, error, updatedAt: stamp() } },\n );\n },\n catch: error => translateMongoError(error),\n }),\n\n /**\n * Take ownership of a stale instance, or answer `undefined`.\n *\n * ⚠️ **One conditional write, not a read and then a write.** The filter\n * repeats `findStale`'s predicate, so two sweepers racing the same instance\n * produce one winner: the loser's filter no longer matches because the\n * winner re-stamped `updatedAt`. That stamp is also what stops the next tick\n * finding the instance this sweep is still working on.\n */\n claimForResume: (sagaId, olderThanMs) =>\n Effect.tryPromise({\n try: async () => {\n const claimed = await collection.findOneAndUpdate(\n {\n sagaId,\n state: { $in: [...UNFINISHED] },\n updatedAt: { $lt: deadline(olderThanMs) },\n },\n { $inc: { resumeAttempts: 1 }, $set: { updatedAt: stamp() } },\n { returnDocument: 'after', projection: { _id: 0 } },\n );\n return (claimed ?? undefined) as SagaInstance | undefined;\n },\n catch: error => translateMongoError(error),\n }),\n\n /**\n * Flag one step's calls as given back, addressing the outcome by its step\n * id: outcomes are `$push`ed, so an index is a fact about arrival order and\n * a resumed walk must not depend on one.\n */\n markCompensated: (sagaId, stepId) =>\n Effect.tryPromise({\n try: async () => {\n await collection.updateOne(\n { sagaId },\n {\n $set: {\n 'outcomes.$[entry].compensated': true,\n updatedAt: stamp(),\n },\n },\n { arrayFilters: [{ 'entry.stepId': stepId }] },\n );\n },\n catch: error => translateMongoError(error),\n }),\n\n get: sagaId =>\n Effect.tryPromise({\n try: async () =>\n ((await collection.findOne({ sagaId }, { projection: { _id: 0 } })) ??\n undefined) as SagaInstance | undefined,\n catch: error => translateMongoError(error),\n }),\n\n /**\n * Instances stuck mid-flight past a deadline. ⚠️ `COMPENSATING` counts: a\n * coordinator that died while unwinding is the state that leaves holds in\n * place, and the one an operator most needs to find.\n */\n findStale: olderThanMs =>\n Effect.tryPromise({\n try: async () =>\n (await collection\n .find(\n {\n state: { $in: [...UNFINISHED] },\n updatedAt: { $lt: deadline(olderThanMs) },\n },\n { projection: { _id: 0 } },\n )\n .toArray()) as SagaInstance[],\n catch: error => translateMongoError(error),\n }),\n };\n};\n\n/**\n * Provides {@link SagaStoreTag} from the service's own database, creating the\n * indexes first.\n */\nexport const MongoSagaStoreLayer = Layer.effect(\n SagaStoreTag,\n Effect.gen(function* () {\n const db = yield* MongoDatabaseTag;\n yield* ensureSagaIndexes(db);\n return makeMongoSagaStore(db);\n }),\n);\n\n/**\n * Provides {@link SagaStoreTag} from a database the host **names**, resolved on\n * the shared client — for a process whose saga store is one of several stores\n * it holds, rather than the one database {@link MongoDatabaseTag} carries.\n *\n * Same pool, same indexes, same store; only where the handle comes from\n * differs.\n */\nexport const mongoSagaStoreLayer = (options: { readonly database: string }) =>\n Layer.effect(\n SagaStoreTag,\n Effect.gen(function* () {\n const client = yield* MongoClientTag;\n const db = client.db(options.database);\n yield* ensureSagaIndexes(db);\n return makeMongoSagaStore(db);\n }),\n );\n"],"names":["SagaStoreTag","Effect","Layer","translateMongoError","MongoClientTag","MongoDatabaseTag","SAGA_COLLECTION","UNFINISHED","sagaCollection","db","collection","deadline","olderThanMs","Date","now","toISOString","ensureSagaIndexes","tryPromise","try","createIndex","sagaId","unique","state","updatedAt","catch","error","makeMongoSagaStore","stamp","start","instance","updateOne","$setOnInsert","upsert","beginStep","stepIndex","$set","recordOutcome","outcome","$push","outcomes","settle","claimForResume","claimed","findOneAndUpdate","$in","$lt","$inc","resumeAttempts","returnDocument","projection","_id","undefined","markCompensated","stepId","arrayFilters","get","findOne","findStale","find","toArray","MongoSagaStoreLayer","effect","gen","mongoSagaStoreLayer","options","client","database"],"mappings":"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"}
@@ -1,4 +1,3 @@
1
- import { EntifixConnError } from '@entifix/core';
2
1
  import { Effect, Option } from 'effect';
3
2
  import type { ClientSession, Db } from 'mongodb';
4
3
  /**
@@ -20,7 +19,7 @@ export interface CommandClaim {
20
19
  readonly replay?: unknown;
21
20
  }
22
21
  /** The unique index the claim rides. */
23
- export declare const ensureCommandInboxIndexes: (db: Db) => Effect.Effect<void, EntifixConnError, never>;
22
+ export declare const ensureCommandInboxIndexes: (db: Db) => Effect.Effect<void, import("@entifix/core").StoreFault, never>;
24
23
  /** The claim document a unit inserts inside its session. */
25
24
  export declare const commandClaimDocument: (commandId: string) => CommandClaim;
26
25
  /**
@@ -29,5 +28,5 @@ export declare const commandClaimDocument: (commandId: string) => CommandClaim;
29
28
  */
30
29
  export declare const rememberCommandReplay: (db: Db, session: ClientSession, commandId: string, replay: unknown) => Promise<import("mongodb").UpdateResult<CommandClaim>>;
31
30
  /** What the run that took `commandId` asked to be remembered. */
32
- export declare const readCommandReplay: (db: Db, commandId: string) => Effect.Effect<Option.None<{} | null> | Option.Some<{} | null>, EntifixConnError, never>;
31
+ export declare const readCommandReplay: (db: Db, commandId: string) => Effect.Effect<Option.None<{} | null> | Option.Some<{} | null>, import("@entifix/core").StoreFault, never>;
33
32
  //# sourceMappingURL=command-inbox.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"command-inbox.d.ts","sourceRoot":"","sources":["../../src/unit-of-work/command-inbox.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACxC,OAAO,KAAK,EAAE,aAAa,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAEjD;;;;;;;;;GASG;AACH,eAAO,MAAM,wBAAwB,kBAAkB,CAAC;AAExD,2CAA2C;AAC3C,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,+EAA+E;IAC/E,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,wCAAwC;AACxC,eAAO,MAAM,yBAAyB,GAAI,IAAI,EAAE,iDAW5C,CAAC;AAEL,4DAA4D;AAC5D,eAAO,MAAM,oBAAoB,GAAI,WAAW,MAAM,KAAG,YAGvD,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,qBAAqB,GAChC,IAAI,EAAE,EACN,SAAS,aAAa,EACtB,WAAW,MAAM,EACjB,QAAQ,OAAO,0DAI+C,CAAC;AAEjE,iEAAiE;AACjE,eAAO,MAAM,iBAAiB,GAAI,IAAI,EAAE,EAAE,WAAW,MAAM,4FAgBxD,CAAC"}
1
+ {"version":3,"file":"command-inbox.d.ts","sourceRoot":"","sources":["../../src/unit-of-work/command-inbox.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACxC,OAAO,KAAK,EAAE,aAAa,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAIjD;;;;;;;;;GASG;AACH,eAAO,MAAM,wBAAwB,kBAAkB,CAAC;AAExD,2CAA2C;AAC3C,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,+EAA+E;IAC/E,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,wCAAwC;AACxC,eAAO,MAAM,yBAAyB,GAAI,IAAI,EAAE,mEAQ5C,CAAC;AAEL,4DAA4D;AAC5D,eAAO,MAAM,oBAAoB,GAAI,WAAW,MAAM,KAAG,YAGvD,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,qBAAqB,GAChC,IAAI,EAAE,EACN,SAAS,aAAa,EACtB,WAAW,MAAM,EACjB,QAAQ,OAAO,0DAI+C,CAAC;AAEjE,iEAAiE;AACjE,eAAO,MAAM,iBAAiB,GAAI,IAAI,EAAE,EAAE,WAAW,MAAM,8GAaxD,CAAC"}
@@ -1,5 +1,5 @@
1
- import { EntifixConnError } from '@entifix/core';
2
1
  import { Effect, Option } from 'effect';
2
+ import { translateMongoFault } from '../errors/translate-mongo-error.js';
3
3
  /**
4
4
  * Where a participant records the command ids it has acted on — in the
5
5
  * database its own writes go to, so the claim and the write are one fact.
@@ -19,9 +19,7 @@ import { Effect, Option } from 'effect';
19
19
  name: 'commandId_1'
20
20
  });
21
21
  },
22
- catch: (error)=>new EntifixConnError('Failed to ensure command inbox indexes', error, {
23
- database: db.databaseName
24
- })
22
+ catch: (error)=>translateMongoFault(error)
25
23
  });
26
24
  /** The claim document a unit inserts inside its session. */ export const commandClaimDocument = (commandId)=>({
27
25
  commandId,
@@ -47,9 +45,7 @@ import { Effect, Option } from 'effect';
47
45
  _id: 0
48
46
  }
49
47
  }),
50
- catch: (error)=>new EntifixConnError('Failed to read a command claim', error, {
51
- commandId
52
- })
48
+ catch: (error)=>translateMongoFault(error)
53
49
  }).pipe(Effect.map((claim)=>claim === null || claim.replay === undefined ? Option.none() : Option.some(claim.replay)));
54
50
 
55
51
  //# sourceMappingURL=command-inbox.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/unit-of-work/command-inbox.ts"],"sourcesContent":["import { EntifixConnError } from '@entifix/core';\nimport { Effect, Option } from 'effect';\nimport type { ClientSession, Db } from 'mongodb';\n\n/**\n * Where a participant records the command ids it has acted on — in the\n * database its own writes go to, so the claim and the write are one fact.\n *\n * The command-side twin of the inbox. A bus consumer dedupes on\n * `(consumer, eventId)`; a participant answering a dispatched command dedupes\n * on the command id alone, because a command has exactly one addressee. And\n * unlike the inbox it remembers an answer: a redelivered capture must be told\n * what the first delivery decided, not merely that there was one.\n */\nexport const COMMAND_INBOX_COLLECTION = 'command_inbox';\n\n/** The stored shape of a command claim. */\nexport interface CommandClaim {\n readonly commandId: string;\n readonly claimedAt: string;\n /** What a duplicate is answered with, when the claiming unit asked for one. */\n readonly replay?: unknown;\n}\n\n/** The unique index the claim rides. */\nexport const ensureCommandInboxIndexes = (db: Db) =>\n Effect.tryPromise({\n try: async () => {\n await db\n .collection<CommandClaim>(COMMAND_INBOX_COLLECTION)\n .createIndex({ commandId: 1 }, { unique: true, name: 'commandId_1' });\n },\n catch: error =>\n new EntifixConnError('Failed to ensure command inbox indexes', error, {\n database: db.databaseName,\n }),\n });\n\n/** The claim document a unit inserts inside its session. */\nexport const commandClaimDocument = (commandId: string): CommandClaim => ({\n commandId,\n claimedAt: new Date().toISOString(),\n});\n\n/**\n * Stores what a duplicate of `commandId` is answered with, inside the session\n * that took the claim.\n */\nexport const rememberCommandReplay = (\n db: Db,\n session: ClientSession,\n commandId: string,\n replay: unknown,\n) =>\n db\n .collection<CommandClaim>(COMMAND_INBOX_COLLECTION)\n .updateOne({ commandId }, { $set: { replay } }, { session });\n\n/** What the run that took `commandId` asked to be remembered. */\nexport const readCommandReplay = (db: Db, commandId: string) =>\n Effect.tryPromise({\n try: () =>\n db\n .collection<CommandClaim>(COMMAND_INBOX_COLLECTION)\n .findOne({ commandId }, { projection: { _id: 0 } }),\n catch: error =>\n new EntifixConnError('Failed to read a command claim', error, {\n commandId,\n }),\n }).pipe(\n Effect.map(claim =>\n claim === null || claim.replay === undefined\n ? Option.none()\n : Option.some(claim.replay),\n ),\n );\n"],"names":["EntifixConnError","Effect","Option","COMMAND_INBOX_COLLECTION","ensureCommandInboxIndexes","db","tryPromise","try","collection","createIndex","commandId","unique","name","catch","error","database","databaseName","commandClaimDocument","claimedAt","Date","toISOString","rememberCommandReplay","session","replay","updateOne","$set","readCommandReplay","findOne","projection","_id","pipe","map","claim","undefined","none","some"],"mappings":"AAAA,SAASA,gBAAgB,QAAQ,gBAAgB;AACjD,SAASC,MAAM,EAAEC,MAAM,QAAQ,SAAS;AAGxC;;;;;;;;;CASC,GACD,OAAO,MAAMC,2BAA2B,gBAAgB;AAUxD,sCAAsC,GACtC,OAAO,MAAMC,4BAA4B,CAACC,KACxCJ,OAAOK,UAAU,CAAC;QAChBC,KAAK;YACH,MAAMF,GACHG,UAAU,CAAeL,0BACzBM,WAAW,CAAC;gBAAEC,WAAW;YAAE,GAAG;gBAAEC,QAAQ;gBAAMC,MAAM;YAAc;QACvE;QACAC,OAAOC,CAAAA,QACL,IAAId,iBAAiB,0CAA0Cc,OAAO;gBACpEC,UAAUV,GAAGW,YAAY;YAC3B;IACJ,GAAG;AAEL,0DAA0D,GAC1D,OAAO,MAAMC,uBAAuB,CAACP,YAAqC,CAAA;QACxEA;QACAQ,WAAW,IAAIC,OAAOC,WAAW;IACnC,CAAA,EAAG;AAEH;;;CAGC,GACD,OAAO,MAAMC,wBAAwB,CACnChB,IACAiB,SACAZ,WACAa,SAEAlB,GACGG,UAAU,CAAeL,0BACzBqB,SAAS,CAAC;QAAEd;IAAU,GAAG;QAAEe,MAAM;YAAEF;QAAO;IAAE,GAAG;QAAED;IAAQ,GAAG;AAEjE,+DAA+D,GAC/D,OAAO,MAAMI,oBAAoB,CAACrB,IAAQK,YACxCT,OAAOK,UAAU,CAAC;QAChBC,KAAK,IACHF,GACGG,UAAU,CAAeL,0BACzBwB,OAAO,CAAC;gBAAEjB;YAAU,GAAG;gBAAEkB,YAAY;oBAAEC,KAAK;gBAAE;YAAE;QACrDhB,OAAOC,CAAAA,QACL,IAAId,iBAAiB,kCAAkCc,OAAO;gBAC5DJ;YACF;IACJ,GAAGoB,IAAI,CACL7B,OAAO8B,GAAG,CAACC,CAAAA,QACTA,UAAU,QAAQA,MAAMT,MAAM,KAAKU,YAC/B/B,OAAOgC,IAAI,KACXhC,OAAOiC,IAAI,CAACH,MAAMT,MAAM,IAE9B"}
1
+ {"version":3,"sources":["../../src/unit-of-work/command-inbox.ts"],"sourcesContent":["import { Effect, Option } from 'effect';\nimport type { ClientSession, Db } from 'mongodb';\n\nimport { translateMongoFault } from '../errors/translate-mongo-error.js';\n\n/**\n * Where a participant records the command ids it has acted on — in the\n * database its own writes go to, so the claim and the write are one fact.\n *\n * The command-side twin of the inbox. A bus consumer dedupes on\n * `(consumer, eventId)`; a participant answering a dispatched command dedupes\n * on the command id alone, because a command has exactly one addressee. And\n * unlike the inbox it remembers an answer: a redelivered capture must be told\n * what the first delivery decided, not merely that there was one.\n */\nexport const COMMAND_INBOX_COLLECTION = 'command_inbox';\n\n/** The stored shape of a command claim. */\nexport interface CommandClaim {\n readonly commandId: string;\n readonly claimedAt: string;\n /** What a duplicate is answered with, when the claiming unit asked for one. */\n readonly replay?: unknown;\n}\n\n/** The unique index the claim rides. */\nexport const ensureCommandInboxIndexes = (db: Db) =>\n Effect.tryPromise({\n try: async () => {\n await db\n .collection<CommandClaim>(COMMAND_INBOX_COLLECTION)\n .createIndex({ commandId: 1 }, { unique: true, name: 'commandId_1' });\n },\n catch: error => translateMongoFault(error),\n });\n\n/** The claim document a unit inserts inside its session. */\nexport const commandClaimDocument = (commandId: string): CommandClaim => ({\n commandId,\n claimedAt: new Date().toISOString(),\n});\n\n/**\n * Stores what a duplicate of `commandId` is answered with, inside the session\n * that took the claim.\n */\nexport const rememberCommandReplay = (\n db: Db,\n session: ClientSession,\n commandId: string,\n replay: unknown,\n) =>\n db\n .collection<CommandClaim>(COMMAND_INBOX_COLLECTION)\n .updateOne({ commandId }, { $set: { replay } }, { session });\n\n/** What the run that took `commandId` asked to be remembered. */\nexport const readCommandReplay = (db: Db, commandId: string) =>\n Effect.tryPromise({\n try: () =>\n db\n .collection<CommandClaim>(COMMAND_INBOX_COLLECTION)\n .findOne({ commandId }, { projection: { _id: 0 } }),\n catch: error => translateMongoFault(error),\n }).pipe(\n Effect.map(claim =>\n claim === null || claim.replay === undefined\n ? Option.none()\n : Option.some(claim.replay),\n ),\n );\n"],"names":["Effect","Option","translateMongoFault","COMMAND_INBOX_COLLECTION","ensureCommandInboxIndexes","db","tryPromise","try","collection","createIndex","commandId","unique","name","catch","error","commandClaimDocument","claimedAt","Date","toISOString","rememberCommandReplay","session","replay","updateOne","$set","readCommandReplay","findOne","projection","_id","pipe","map","claim","undefined","none","some"],"mappings":"AAAA,SAASA,MAAM,EAAEC,MAAM,QAAQ,SAAS;AAGxC,SAASC,mBAAmB,QAAQ,qCAAqC;AAEzE;;;;;;;;;CASC,GACD,OAAO,MAAMC,2BAA2B,gBAAgB;AAUxD,sCAAsC,GACtC,OAAO,MAAMC,4BAA4B,CAACC,KACxCL,OAAOM,UAAU,CAAC;QAChBC,KAAK;YACH,MAAMF,GACHG,UAAU,CAAeL,0BACzBM,WAAW,CAAC;gBAAEC,WAAW;YAAE,GAAG;gBAAEC,QAAQ;gBAAMC,MAAM;YAAc;QACvE;QACAC,OAAOC,CAAAA,QAASZ,oBAAoBY;IACtC,GAAG;AAEL,0DAA0D,GAC1D,OAAO,MAAMC,uBAAuB,CAACL,YAAqC,CAAA;QACxEA;QACAM,WAAW,IAAIC,OAAOC,WAAW;IACnC,CAAA,EAAG;AAEH;;;CAGC,GACD,OAAO,MAAMC,wBAAwB,CACnCd,IACAe,SACAV,WACAW,SAEAhB,GACGG,UAAU,CAAeL,0BACzBmB,SAAS,CAAC;QAAEZ;IAAU,GAAG;QAAEa,MAAM;YAAEF;QAAO;IAAE,GAAG;QAAED;IAAQ,GAAG;AAEjE,+DAA+D,GAC/D,OAAO,MAAMI,oBAAoB,CAACnB,IAAQK,YACxCV,OAAOM,UAAU,CAAC;QAChBC,KAAK,IACHF,GACGG,UAAU,CAAeL,0BACzBsB,OAAO,CAAC;gBAAEf;YAAU,GAAG;gBAAEgB,YAAY;oBAAEC,KAAK;gBAAE;YAAE;QACrDd,OAAOC,CAAAA,QAASZ,oBAAoBY;IACtC,GAAGc,IAAI,CACL5B,OAAO6B,GAAG,CAACC,CAAAA,QACTA,UAAU,QAAQA,MAAMT,MAAM,KAAKU,YAC/B9B,OAAO+B,IAAI,KACX/B,OAAOgC,IAAI,CAACH,MAAMT,MAAM,IAE9B"}
@@ -1 +1 @@
1
- {"version":3,"file":"mongo-unit-of-work.d.ts","sourceRoot":"","sources":["../../src/unit-of-work/mongo-unit-of-work.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAEV,UAAU,EAGX,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,OAAO,EAAU,KAAK,EAA4B,MAAM,QAAQ,CAAC;AAC1E,OAAO,KAAK,EAAE,aAAa,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAQjD,OAAO,EACL,cAAc,EACd,gBAAgB,EACjB,MAAM,qCAAqC,CAAC;AAc7C;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,EAAE,EAAE,EAAE,CAAC;IAChB,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;CACjC;AAED,6DAA6D;AAC7D,MAAM,WAAW,kBAAkB;IACjC,YAAY,IAAI,aAAa,CAAC;CAC/B;AAsHD;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,GAAI,QAAQ,kBAAkB;sBAuBpC,EAAE,KAAG,UAAU,CAAC,SAAS,CAAC;CAgIpD,CAAC;AAEF,8CAA8C;AAC9C,eAAO,MAAM,mBAAmB,GAAI,SAAS;IAC3C,QAAQ,CAAC,MAAM,EAAE,kBAAkB,CAAC;IACpC,QAAQ,CAAC,EAAE,EAAE,EAAE,CAAC;CACjB,KAAG,UAAU,CAAC,SAAS,CACsC,CAAC;;AAE/D,wDAAwD;AACxD,qBAAa,kBAAmB,SAAQ,uBAGf;CAAG;AAE5B;;;GAGG;AACH,eAAO,MAAM,oBAAoB,2EAOhC,CAAC"}
1
+ {"version":3,"file":"mongo-unit-of-work.d.ts","sourceRoot":"","sources":["../../src/unit-of-work/mongo-unit-of-work.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAEV,UAAU,EAGX,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,OAAO,EAAU,KAAK,EAA4B,MAAM,QAAQ,CAAC;AAC1E,OAAO,KAAK,EAAE,aAAa,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AASjD,OAAO,EACL,cAAc,EACd,gBAAgB,EACjB,MAAM,qCAAqC,CAAC;AAc7C;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,EAAE,EAAE,EAAE,CAAC;IAChB,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;CACjC;AAED,6DAA6D;AAC7D,MAAM,WAAW,kBAAkB;IACjC,YAAY,IAAI,aAAa,CAAC;CAC/B;AA4GD;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,GAAI,QAAQ,kBAAkB;sBAuBpC,EAAE,KAAG,UAAU,CAAC,SAAS,CAAC;CAqHpD,CAAC;AAEF,8CAA8C;AAC9C,eAAO,MAAM,mBAAmB,GAAI,SAAS;IAC3C,QAAQ,CAAC,MAAM,EAAE,kBAAkB,CAAC;IACpC,QAAQ,CAAC,EAAE,EAAE,EAAE,CAAC;CACjB,KAAG,UAAU,CAAC,SAAS,CACsC,CAAC;;AAE/D,wDAAwD;AACxD,qBAAa,kBAAmB,SAAQ,uBAGf;CAAG;AAE5B;;;GAGG;AACH,eAAO,MAAM,oBAAoB,2EAOhC,CAAC"}
@@ -1,5 +1,6 @@
1
- import { EntifixConnError } from '@entifix/core';
1
+ import { Contention, isFailure, isStoreFault } from '@entifix/core';
2
2
  import { Context, Effect, Layer, Option, Result, Schedule } from 'effect';
3
+ import { translateMongoFault } from '../errors/translate-mongo-error.js';
3
4
  import { ensureInboxIndexes, INBOX_COLLECTION, inboxDocument, isDuplicateKey } from '../inbox/store.js';
4
5
  import { MongoClientTag, MongoDatabaseTag } from '../mongo-database/mongo-database.js';
5
6
  import { ensureOutboxIndexes, OUTBOX_COLLECTION, outboxDocument } from '../outbox/store.js';
@@ -32,12 +33,13 @@ const hasErrorLabel = (error, label)=>{
32
33
  }
33
34
  return false;
34
35
  };
35
- const isTransient = (error)=>error instanceof EntifixConnError && labelledInChain(error, 'TransientTransactionError');
36
- const driver = (message, run, details)=>Effect.tryPromise({
36
+ /** A transient conflict, which reruns the whole unit. */ const isTransient = (error)=>isFailure(error) && error._tag === 'Contention';
37
+ const fault = (error)=>translateMongoFault(error);
38
+ const driver = (run)=>Effect.tryPromise({
37
39
  try: run,
38
- catch: (error)=>new EntifixConnError(message, error, details)
40
+ catch: fault
39
41
  });
40
- /** A duplicate-key rejection answered as `duplicate` rather than as an error. */ const insertOrDuplicate = (message, run, details)=>Effect.tryPromise({
42
+ /** A duplicate-key rejection answered as `duplicate` rather than as an error. */ const insertOrDuplicate = (run)=>Effect.tryPromise({
41
43
  try: async ()=>{
42
44
  try {
43
45
  await run();
@@ -47,26 +49,28 @@ const driver = (message, run, details)=>Effect.tryPromise({
47
49
  throw error;
48
50
  }
49
51
  },
50
- catch: (error)=>new EntifixConnError(message, error, details)
52
+ catch: fault
51
53
  });
52
- const takeClaim = (db, session, claim)=>claim.kind === 'event' ? insertOrDuplicate('Failed to claim an event', ()=>db.collection(INBOX_COLLECTION).insertOne(inboxDocument(claim.consumer, claim.eventId), {
54
+ const takeClaim = (db, session, claim)=>claim.kind === 'event' ? insertOrDuplicate(()=>db.collection(INBOX_COLLECTION).insertOne(inboxDocument(claim.consumer, claim.eventId), {
53
55
  session
54
- }), {
55
- consumer: claim.consumer,
56
- eventId: claim.eventId
57
- }) : insertOrDuplicate('Failed to claim a command', ()=>db.collection(COMMAND_INBOX_COLLECTION).insertOne(commandClaimDocument(claim.commandId), {
56
+ })) : insertOrDuplicate(()=>db.collection(COMMAND_INBOX_COLLECTION).insertOne(commandClaimDocument(claim.commandId), {
58
57
  session
59
- }), {
60
- commandId: claim.commandId
61
- });
58
+ }));
62
59
  /**
63
60
  * Commits, retrying a commit whose result the server could not report. Such a
64
61
  * commit may or may not have applied, and Mongo makes retrying it safe.
65
- */ const commit = (session)=>driver('Failed to commit a unit of work', ()=>session.commitTransaction()).pipe(Effect.retry({
62
+ */ /**
63
+ * Retried on the **driver's** error, before translation: the label that says
64
+ * the commit's result is unknown lives on it, and a translated failure no
65
+ * longer carries it.
66
+ */ const commit = (session)=>Effect.tryPromise({
67
+ try: ()=>session.commitTransaction(),
68
+ catch: (error)=>error
69
+ }).pipe(Effect.retry({
66
70
  times: ATTEMPTS - 1,
67
71
  while: (error)=>labelledInChain(error, 'UnknownTransactionCommitResult')
68
- }));
69
- /** Rolls back, and never fails: the unit is already ending for another reason. */ const abort = (session)=>driver('Failed to abort a unit of work', ()=>session.abortTransaction()).pipe(Effect.ignore);
72
+ }), Effect.mapError(fault));
73
+ /** Rolls back, and never fails: the unit is already ending for another reason. */ const abort = (session)=>driver(()=>session.abortTransaction()).pipe(Effect.ignore);
70
74
  /**
71
75
  * A {@link UnitOfWork} per database, sharing one client — the shape a
72
76
  * per-organization store needs, where the database is resolved inside the
@@ -77,7 +81,7 @@ const takeClaim = (db, session, claim)=>claim.kind === 'event' ? insertOrDuplica
77
81
  const key = db.databaseName;
78
82
  const done = ensured.get(key);
79
83
  if (done !== undefined) {
80
- return driver('Failed to ensure unit-of-work indexes', ()=>done);
84
+ return driver(()=>done);
81
85
  }
82
86
  const pending = Effect.runPromise(Effect.all([
83
87
  ensureOutboxIndexes(db),
@@ -87,13 +91,13 @@ const takeClaim = (db, session, claim)=>claim.kind === 'event' ? insertOrDuplica
87
91
  ensured.set(key, pending);
88
92
  // A failed ensure must be retried by the next unit, not remembered.
89
93
  pending.catch(()=>ensured.delete(key));
90
- return driver('Failed to ensure unit-of-work indexes', ()=>pending);
94
+ return driver(()=>pending);
91
95
  });
92
96
  const forDatabase = (db)=>{
93
97
  const attempt = (body, options)=>Effect.acquireUseRelease(Effect.sync(()=>client.startSession()), (session)=>Effect.gen(function*() {
94
98
  yield* Effect.try({
95
99
  try: ()=>session.startTransaction(),
96
- catch: (error)=>new EntifixConnError('Failed to start a unit of work', error)
100
+ catch: fault
97
101
  });
98
102
  const work = Effect.gen(function*() {
99
103
  const claim = options == null ? void 0 : options.claim;
@@ -111,7 +115,7 @@ const takeClaim = (db, session, claim)=>claim.kind === 'event' ? insertOrDuplica
111
115
  emit: (event)=>void emitted.push(event)
112
116
  });
113
117
  if (emitted.length > 0) {
114
- const written = yield* insertOrDuplicate('Failed to record a unit of work’s events', ()=>db.collection(OUTBOX_COLLECTION).insertMany(emitted.map(outboxDocument), {
118
+ const written = yield* insertOrDuplicate(()=>db.collection(OUTBOX_COLLECTION).insertMany(emitted.map(outboxDocument), {
115
119
  session
116
120
  }));
117
121
  // An event id already recorded is the outbox's own
@@ -124,7 +128,7 @@ const takeClaim = (db, session, claim)=>claim.kind === 'event' ? insertOrDuplica
124
128
  }
125
129
  if ((claim == null ? void 0 : claim.kind) === 'command' && (options == null ? void 0 : options.replay)) {
126
130
  const replay = options.replay(value);
127
- yield* driver('Failed to remember a command’s answer', ()=>rememberCommandReplay(db, session, claim.commandId, replay));
131
+ yield* driver(()=>rememberCommandReplay(db, session, claim.commandId, replay));
128
132
  }
129
133
  return {
130
134
  kind: 'commit',
@@ -135,13 +139,13 @@ const takeClaim = (db, session, claim)=>claim.kind === 'event' ? insertOrDuplica
135
139
  if (Result.isFailure(decided)) {
136
140
  yield* abort(session);
137
141
  const failure = decided.failure;
138
- if (failure instanceof EntifixConnError) {
142
+ if (isStoreFault(failure)) {
139
143
  return yield* Effect.fail(failure);
140
144
  }
141
- // A conflict the body surfaced without wrapping is still the
145
+ // A conflict the body surfaced without translating is still the
142
146
  // store's, not a business decision: it is retried, not aborted.
143
147
  if (labelledInChain(failure, 'TransientTransactionError')) {
144
- return yield* Effect.fail(new EntifixConnError('A unit of work hit a transient conflict', failure));
148
+ return yield* Effect.fail(new Contention());
145
149
  }
146
150
  return {
147
151
  _tag: 'aborted',
@@ -162,7 +166,7 @@ const takeClaim = (db, session, claim)=>claim.kind === 'event' ? insertOrDuplica
162
166
  _tag: 'committed',
163
167
  value: decided.success.value
164
168
  };
165
- }), (session)=>driver('Failed to end a session', ()=>session.endSession()).pipe(Effect.ignore));
169
+ }), (session)=>driver(()=>session.endSession()).pipe(Effect.ignore));
166
170
  return {
167
171
  run: (body, options)=>Effect.gen(function*() {
168
172
  yield* ensureIndexes(db);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/unit-of-work/mongo-unit-of-work.ts"],"sourcesContent":["import { type DomainEvent, EntifixConnError } from '@entifix/core';\nimport type {\n UnitClaim,\n UnitOfWork,\n UnitOutcome,\n UnitRunOptions,\n} from '@entifix/transactions';\nimport { Context, Effect, Layer, Option, Result, Schedule } from 'effect';\nimport type { ClientSession, Db } from 'mongodb';\n\nimport {\n ensureInboxIndexes,\n INBOX_COLLECTION,\n inboxDocument,\n isDuplicateKey,\n} from '../inbox/store.js';\nimport {\n MongoClientTag,\n MongoDatabaseTag,\n} from '../mongo-database/mongo-database.js';\nimport {\n ensureOutboxIndexes,\n OUTBOX_COLLECTION,\n outboxDocument,\n} from '../outbox/store.js';\nimport {\n COMMAND_INBOX_COLLECTION,\n commandClaimDocument,\n ensureCommandInboxIndexes,\n readCommandReplay,\n rememberCommandReplay,\n} from './command-inbox.js';\n\n/**\n * What a Mongo unit's body writes through: the database and the session bound\n * to the unit's transaction. Every write must pass `{ session }`, or it is not\n * part of the unit.\n */\nexport interface MongoUnit {\n readonly db: Db;\n readonly session: ClientSession;\n}\n\n/** What starts a session — a `MongoClient`, structurally. */\nexport interface MongoSessionSource {\n startSession(): ClientSession;\n}\n\n/**\n * How many times a unit runs before a transient conflict is reported as the\n * failure it is, and how long it waits between runs.\n *\n * Measured against a real replica set: eight units incrementing one document\n * at once conflict on every attempt but one, so a short fixed retry count\n * starves most of them. A jittered exponential back-off spreads the reruns so\n * each eventually runs alone.\n */\nconst ATTEMPTS = 10;\nconst BACK_OFF = Schedule.exponential('5 millis', 1.5).pipe(Schedule.jittered);\n\nconst hasErrorLabel = (error: unknown, label: string): boolean => {\n const candidate = error as {\n hasErrorLabel?: (label: string) => boolean;\n errorLabels?: readonly string[];\n } | null;\n return (\n candidate?.hasErrorLabel?.(label) ??\n candidate?.errorLabels?.includes(label) ??\n false\n );\n};\n\n/**\n * Whether a server label is on `error` or anywhere in its cause chain — a\n * driver error reaches here wrapped once by the adapter and possibly again by\n * the body that called the driver.\n */\nconst labelledInChain = (error: unknown, label: string): boolean => {\n let current: unknown = error;\n for (let depth = 0; current != null && depth < 5; depth += 1) {\n if (hasErrorLabel(current, label)) return true;\n current = (current as { cause?: unknown }).cause;\n }\n return false;\n};\n\nconst isTransient = (error: unknown): boolean =>\n error instanceof EntifixConnError &&\n labelledInChain(error, 'TransientTransactionError');\n\nconst driver = <A>(\n message: string,\n run: () => Promise<A>,\n details?: Record<string, unknown>,\n) =>\n Effect.tryPromise({\n try: run,\n catch: error => new EntifixConnError(message, error, details),\n });\n\n/** A duplicate-key rejection answered as `duplicate` rather than as an error. */\nconst insertOrDuplicate = (\n message: string,\n run: () => Promise<unknown>,\n details?: Record<string, unknown>,\n) =>\n Effect.tryPromise({\n try: async () => {\n try {\n await run();\n return 'inserted' as const;\n } catch (error) {\n if (isDuplicateKey(error)) return 'duplicate' as const;\n throw error;\n }\n },\n catch: error => new EntifixConnError(message, error, details),\n });\n\nconst takeClaim = (db: Db, session: ClientSession, claim: UnitClaim) =>\n claim.kind === 'event'\n ? insertOrDuplicate(\n 'Failed to claim an event',\n () =>\n db\n .collection(INBOX_COLLECTION)\n .insertOne(inboxDocument(claim.consumer, claim.eventId), {\n session,\n }),\n { consumer: claim.consumer, eventId: claim.eventId },\n )\n : insertOrDuplicate(\n 'Failed to claim a command',\n () =>\n db\n .collection(COMMAND_INBOX_COLLECTION)\n .insertOne(commandClaimDocument(claim.commandId), { session }),\n { commandId: claim.commandId },\n );\n\n/**\n * Commits, retrying a commit whose result the server could not report. Such a\n * commit may or may not have applied, and Mongo makes retrying it safe.\n */\nconst commit = (session: ClientSession) =>\n driver('Failed to commit a unit of work', () =>\n session.commitTransaction(),\n ).pipe(\n Effect.retry({\n times: ATTEMPTS - 1,\n while: error => labelledInChain(error, 'UnknownTransactionCommitResult'),\n }),\n );\n\n/** Rolls back, and never fails: the unit is already ending for another reason. */\nconst abort = (session: ClientSession) =>\n driver('Failed to abort a unit of work', () =>\n session.abortTransaction(),\n ).pipe(Effect.ignore);\n\ntype Decision<A> =\n | { readonly kind: 'commit'; readonly value: A }\n | { readonly kind: 'duplicate' };\n\n/**\n * A {@link UnitOfWork} per database, sharing one client — the shape a\n * per-organization store needs, where the database is resolved inside the\n * request. Each database's indexes are ensured once, before its first unit.\n */\nexport const makeMongoUnitsOfWork = (client: MongoSessionSource) => {\n const ensured = new Map<string, Promise<void>>();\n\n const ensureIndexes = (db: Db) =>\n Effect.suspend(() => {\n const key = db.databaseName;\n const done = ensured.get(key);\n if (done !== undefined) {\n return driver('Failed to ensure unit-of-work indexes', () => done);\n }\n const pending = Effect.runPromise(\n Effect.all([\n ensureOutboxIndexes(db),\n ensureInboxIndexes(db),\n ensureCommandInboxIndexes(db),\n ]),\n ).then(() => undefined);\n ensured.set(key, pending);\n // A failed ensure must be retried by the next unit, not remembered.\n pending.catch(() => ensured.delete(key));\n return driver('Failed to ensure unit-of-work indexes', () => pending);\n });\n\n const forDatabase = (db: Db): UnitOfWork<MongoUnit> => {\n const attempt = <A, E, R>(\n body: (unit: {\n readonly handle: MongoUnit;\n emit(event: DomainEvent): void;\n }) => Effect.Effect<A, E, R>,\n options: UnitRunOptions<A, R> | undefined,\n ) =>\n Effect.acquireUseRelease(\n Effect.sync(() => client.startSession()),\n session =>\n Effect.gen(function* () {\n yield* Effect.try({\n try: () => session.startTransaction(),\n catch: error =>\n new EntifixConnError('Failed to start a unit of work', error),\n });\n\n const work = Effect.gen(function* () {\n const claim = options?.claim;\n if (\n claim &&\n (yield* takeClaim(db, session, claim)) === 'duplicate'\n ) {\n return { kind: 'duplicate' } as Decision<A>;\n }\n\n const emitted: DomainEvent[] = [];\n const value = yield* body({\n handle: { db, session },\n emit: event => void emitted.push(event),\n });\n\n if (emitted.length > 0) {\n const written = yield* insertOrDuplicate(\n 'Failed to record a unit of work’s events',\n () =>\n db\n .collection(OUTBOX_COLLECTION)\n .insertMany(emitted.map(outboxDocument), { session }),\n );\n // An event id already recorded is the outbox's own\n // idempotency claim: what it announces committed before.\n if (written === 'duplicate') {\n return { kind: 'duplicate' } as Decision<A>;\n }\n }\n\n if (claim?.kind === 'command' && options?.replay) {\n const replay = options.replay(value);\n yield* driver('Failed to remember a command’s answer', () =>\n rememberCommandReplay(db, session, claim.commandId, replay),\n );\n }\n return { kind: 'commit', value } as Decision<A>;\n });\n\n const decided = yield* Effect.result(work);\n\n if (Result.isFailure(decided)) {\n yield* abort(session);\n const failure = decided.failure;\n if (failure instanceof EntifixConnError) {\n return yield* Effect.fail(failure);\n }\n // A conflict the body surfaced without wrapping is still the\n // store's, not a business decision: it is retried, not aborted.\n if (labelledInChain(failure, 'TransientTransactionError')) {\n return yield* Effect.fail(\n new EntifixConnError(\n 'A unit of work hit a transient conflict',\n failure,\n ),\n );\n }\n return {\n _tag: 'aborted',\n reason: failure as Exclude<E, EntifixConnError>,\n } as UnitOutcome<A, Exclude<E, EntifixConnError>>;\n }\n\n if (decided.success.kind === 'duplicate') {\n yield* abort(session);\n const replay =\n options?.claim?.kind === 'command'\n ? yield* readCommandReplay(db, options.claim.commandId)\n : Option.none();\n return { _tag: 'duplicate', replay } as UnitOutcome<\n A,\n Exclude<E, EntifixConnError>\n >;\n }\n\n yield* commit(session);\n return {\n _tag: 'committed',\n value: decided.success.value,\n } as UnitOutcome<A, Exclude<E, EntifixConnError>>;\n }),\n session =>\n driver('Failed to end a session', () => session.endSession()).pipe(\n Effect.ignore,\n ),\n );\n\n return {\n run: (body, options) =>\n Effect.gen(function* () {\n yield* ensureIndexes(db);\n const outcome = yield* attempt(body, options).pipe(\n // The whole unit runs again, body included: that is why the body\n // must be retry-safe.\n Effect.retry({\n times: ATTEMPTS - 1,\n while: isTransient,\n schedule: BACK_OFF,\n }),\n );\n if (outcome._tag === 'committed' && options?.afterCommit) {\n yield* options.afterCommit.pipe(Effect.ignore);\n }\n return outcome;\n }),\n replay: commandId => readCommandReplay(db, commandId),\n };\n };\n\n return { forDatabase };\n};\n\n/** A {@link UnitOfWork} over one database. */\nexport const makeMongoUnitOfWork = (options: {\n readonly client: MongoSessionSource;\n readonly db: Db;\n}): UnitOfWork<MongoUnit> =>\n makeMongoUnitsOfWork(options.client).forDatabase(options.db);\n\n/** The unit of work over the service's own database. */\nexport class MongoUnitOfWorkTag extends Context.Service<\n MongoUnitOfWorkTag,\n UnitOfWork<MongoUnit>\n>()('MongoUnitOfWorkTag') {}\n\n/**\n * Provides {@link MongoUnitOfWorkTag} from the client and the named database\n * {@link MongoDatabaseLayer} opened.\n */\nexport const MongoUnitOfWorkLayer = Layer.effect(\n MongoUnitOfWorkTag,\n Effect.gen(function* () {\n const client = yield* MongoClientTag;\n const db = yield* MongoDatabaseTag;\n return makeMongoUnitOfWork({ client, db });\n }),\n);\n"],"names":["EntifixConnError","Context","Effect","Layer","Option","Result","Schedule","ensureInboxIndexes","INBOX_COLLECTION","inboxDocument","isDuplicateKey","MongoClientTag","MongoDatabaseTag","ensureOutboxIndexes","OUTBOX_COLLECTION","outboxDocument","COMMAND_INBOX_COLLECTION","commandClaimDocument","ensureCommandInboxIndexes","readCommandReplay","rememberCommandReplay","ATTEMPTS","BACK_OFF","exponential","pipe","jittered","hasErrorLabel","error","label","candidate","errorLabels","includes","labelledInChain","current","depth","cause","isTransient","driver","message","run","details","tryPromise","try","catch","insertOrDuplicate","takeClaim","db","session","claim","kind","collection","insertOne","consumer","eventId","commandId","commit","commitTransaction","retry","times","while","abort","abortTransaction","ignore","makeMongoUnitsOfWork","client","ensured","Map","ensureIndexes","suspend","key","databaseName","done","get","undefined","pending","runPromise","all","then","set","delete","forDatabase","attempt","body","options","acquireUseRelease","sync","startSession","gen","startTransaction","work","emitted","value","handle","emit","event","push","length","written","insertMany","map","replay","decided","result","isFailure","failure","fail","_tag","reason","success","none","endSession","outcome","schedule","afterCommit","makeMongoUnitOfWork","MongoUnitOfWorkTag","Service","MongoUnitOfWorkLayer","effect"],"mappings":"AAAA,SAA2BA,gBAAgB,QAAQ,gBAAgB;AAOnE,SAASC,OAAO,EAAEC,MAAM,EAAEC,KAAK,EAAEC,MAAM,EAAEC,MAAM,EAAEC,QAAQ,QAAQ,SAAS;AAG1E,SACEC,kBAAkB,EAClBC,gBAAgB,EAChBC,aAAa,EACbC,cAAc,QACT,oBAAoB;AAC3B,SACEC,cAAc,EACdC,gBAAgB,QACX,sCAAsC;AAC7C,SACEC,mBAAmB,EACnBC,iBAAiB,EACjBC,cAAc,QACT,qBAAqB;AAC5B,SACEC,wBAAwB,EACxBC,oBAAoB,EACpBC,yBAAyB,EACzBC,iBAAiB,EACjBC,qBAAqB,QAChB,qBAAqB;AAiB5B;;;;;;;;CAQC,GACD,MAAMC,WAAW;AACjB,MAAMC,WAAWhB,SAASiB,WAAW,CAAC,YAAY,KAAKC,IAAI,CAAClB,SAASmB,QAAQ;AAE7E,MAAMC,gBAAgB,CAACC,OAAgBC;QAMnCC;QAAAA,0BACAA;IANF,MAAMA,YAAYF;IAIlB,QACEE,gBAAAA,8BAAAA,2BAAAA,UAAWH,aAAa,qBAAxBG,8BAAAA,WAA2BD,0BAC3BC,8BAAAA,yBAAAA,UAAWC,WAAW,qBAAtBD,uBAAwBE,QAAQ,CAACH,kBADjCC,OAEA;AAEJ;AAEA;;;;CAIC,GACD,MAAMG,kBAAkB,CAACL,OAAgBC;IACvC,IAAIK,UAAmBN;IACvB,IAAK,IAAIO,QAAQ,GAAGD,WAAW,QAAQC,QAAQ,GAAGA,SAAS,EAAG;QAC5D,IAAIR,cAAcO,SAASL,QAAQ,OAAO;QAC1CK,UAAU,AAACA,QAAgCE,KAAK;IAClD;IACA,OAAO;AACT;AAEA,MAAMC,cAAc,CAACT,QACnBA,iBAAiB3B,oBACjBgC,gBAAgBL,OAAO;AAEzB,MAAMU,SAAS,CACbC,SACAC,KACAC,UAEAtC,OAAOuC,UAAU,CAAC;QAChBC,KAAKH;QACLI,OAAOhB,CAAAA,QAAS,IAAI3B,iBAAiBsC,SAASX,OAAOa;IACvD;AAEF,+EAA+E,GAC/E,MAAMI,oBAAoB,CACxBN,SACAC,KACAC,UAEAtC,OAAOuC,UAAU,CAAC;QAChBC,KAAK;YACH,IAAI;gBACF,MAAMH;gBACN,OAAO;YACT,EAAE,OAAOZ,OAAO;gBACd,IAAIjB,eAAeiB,QAAQ,OAAO;gBAClC,MAAMA;YACR;QACF;QACAgB,OAAOhB,CAAAA,QAAS,IAAI3B,iBAAiBsC,SAASX,OAAOa;IACvD;AAEF,MAAMK,YAAY,CAACC,IAAQC,SAAwBC,QACjDA,MAAMC,IAAI,KAAK,UACXL,kBACE,4BACA,IACEE,GACGI,UAAU,CAAC1C,kBACX2C,SAAS,CAAC1C,cAAcuC,MAAMI,QAAQ,EAAEJ,MAAMK,OAAO,GAAG;YACvDN;QACF,IACJ;QAAEK,UAAUJ,MAAMI,QAAQ;QAAEC,SAASL,MAAMK,OAAO;IAAC,KAErDT,kBACE,6BACA,IACEE,GACGI,UAAU,CAAClC,0BACXmC,SAAS,CAAClC,qBAAqB+B,MAAMM,SAAS,GAAG;YAAEP;QAAQ,IAChE;QAAEO,WAAWN,MAAMM,SAAS;IAAC;AAGrC;;;CAGC,GACD,MAAMC,SAAS,CAACR,UACdV,OAAO,mCAAmC,IACxCU,QAAQS,iBAAiB,IACzBhC,IAAI,CACJtB,OAAOuD,KAAK,CAAC;QACXC,OAAOrC,WAAW;QAClBsC,OAAOhC,CAAAA,QAASK,gBAAgBL,OAAO;IACzC;AAGJ,gFAAgF,GAChF,MAAMiC,QAAQ,CAACb,UACbV,OAAO,kCAAkC,IACvCU,QAAQc,gBAAgB,IACxBrC,IAAI,CAACtB,OAAO4D,MAAM;AAMtB;;;;CAIC,GACD,OAAO,MAAMC,uBAAuB,CAACC;IACnC,MAAMC,UAAU,IAAIC;IAEpB,MAAMC,gBAAgB,CAACrB,KACrB5C,OAAOkE,OAAO,CAAC;YACb,MAAMC,MAAMvB,GAAGwB,YAAY;YAC3B,MAAMC,OAAON,QAAQO,GAAG,CAACH;YACzB,IAAIE,SAASE,WAAW;gBACtB,OAAOpC,OAAO,yCAAyC,IAAMkC;YAC/D;YACA,MAAMG,UAAUxE,OAAOyE,UAAU,CAC/BzE,OAAO0E,GAAG,CAAC;gBACT/D,oBAAoBiC;gBACpBvC,mBAAmBuC;gBACnB5B,0BAA0B4B;aAC3B,GACD+B,IAAI,CAAC,IAAMJ;YACbR,QAAQa,GAAG,CAACT,KAAKK;YACjB,oEAAoE;YACpEA,QAAQ/B,KAAK,CAAC,IAAMsB,QAAQc,MAAM,CAACV;YACnC,OAAOhC,OAAO,yCAAyC,IAAMqC;QAC/D;IAEF,MAAMM,cAAc,CAAClC;QACnB,MAAMmC,UAAU,CACdC,MAIAC,UAEAjF,OAAOkF,iBAAiB,CACtBlF,OAAOmF,IAAI,CAAC,IAAMrB,OAAOsB,YAAY,KACrCvC,CAAAA,UACE7C,OAAOqF,GAAG,CAAC;oBACT,OAAOrF,OAAOwC,GAAG,CAAC;wBAChBA,KAAK,IAAMK,QAAQyC,gBAAgB;wBACnC7C,OAAOhB,CAAAA,QACL,IAAI3B,iBAAiB,kCAAkC2B;oBAC3D;oBAEA,MAAM8D,OAAOvF,OAAOqF,GAAG,CAAC;wBACtB,MAAMvC,QAAQmC,2BAAAA,QAASnC,KAAK;wBAC5B,IACEA,SACA,AAAC,CAAA,OAAOH,UAAUC,IAAIC,SAASC,MAAK,MAAO,aAC3C;4BACA,OAAO;gCAAEC,MAAM;4BAAY;wBAC7B;wBAEA,MAAMyC,UAAyB,EAAE;wBACjC,MAAMC,QAAQ,OAAOT,KAAK;4BACxBU,QAAQ;gCAAE9C;gCAAIC;4BAAQ;4BACtB8C,MAAMC,CAAAA,QAAS,KAAKJ,QAAQK,IAAI,CAACD;wBACnC;wBAEA,IAAIJ,QAAQM,MAAM,GAAG,GAAG;4BACtB,MAAMC,UAAU,OAAOrD,kBACrB,4CACA,IACEE,GACGI,UAAU,CAACpC,mBACXoF,UAAU,CAACR,QAAQS,GAAG,CAACpF,iBAAiB;oCAAEgC;gCAAQ;4BAEzD,mDAAmD;4BACnD,yDAAyD;4BACzD,IAAIkD,YAAY,aAAa;gCAC3B,OAAO;oCAAEhD,MAAM;gCAAY;4BAC7B;wBACF;wBAEA,IAAID,CAAAA,yBAAAA,MAAOC,IAAI,MAAK,cAAakC,2BAAAA,QAASiB,MAAM,GAAE;4BAChD,MAAMA,SAASjB,QAAQiB,MAAM,CAACT;4BAC9B,OAAOtD,OAAO,yCAAyC,IACrDjB,sBAAsB0B,IAAIC,SAASC,MAAMM,SAAS,EAAE8C;wBAExD;wBACA,OAAO;4BAAEnD,MAAM;4BAAU0C;wBAAM;oBACjC;oBAEA,MAAMU,UAAU,OAAOnG,OAAOoG,MAAM,CAACb;oBAErC,IAAIpF,OAAOkG,SAAS,CAACF,UAAU;wBAC7B,OAAOzC,MAAMb;wBACb,MAAMyD,UAAUH,QAAQG,OAAO;wBAC/B,IAAIA,mBAAmBxG,kBAAkB;4BACvC,OAAO,OAAOE,OAAOuG,IAAI,CAACD;wBAC5B;wBACA,6DAA6D;wBAC7D,gEAAgE;wBAChE,IAAIxE,gBAAgBwE,SAAS,8BAA8B;4BACzD,OAAO,OAAOtG,OAAOuG,IAAI,CACvB,IAAIzG,iBACF,2CACAwG;wBAGN;wBACA,OAAO;4BACLE,MAAM;4BACNC,QAAQH;wBACV;oBACF;oBAEA,IAAIH,QAAQO,OAAO,CAAC3D,IAAI,KAAK,aAAa;4BAGtCkC;wBAFF,OAAOvB,MAAMb;wBACb,MAAMqD,SACJjB,CAAAA,4BAAAA,iBAAAA,QAASnC,KAAK,qBAAdmC,eAAgBlC,IAAI,MAAK,YACrB,OAAO9B,kBAAkB2B,IAAIqC,QAAQnC,KAAK,CAACM,SAAS,IACpDlD,OAAOyG,IAAI;wBACjB,OAAO;4BAAEH,MAAM;4BAAaN;wBAAO;oBAIrC;oBAEA,OAAO7C,OAAOR;oBACd,OAAO;wBACL2D,MAAM;wBACNf,OAAOU,QAAQO,OAAO,CAACjB,KAAK;oBAC9B;gBACF,IACF5C,CAAAA,UACEV,OAAO,2BAA2B,IAAMU,QAAQ+D,UAAU,IAAItF,IAAI,CAChEtB,OAAO4D,MAAM;QAIrB,OAAO;YACLvB,KAAK,CAAC2C,MAAMC,UACVjF,OAAOqF,GAAG,CAAC;oBACT,OAAOpB,cAAcrB;oBACrB,MAAMiE,UAAU,OAAO9B,QAAQC,MAAMC,SAAS3D,IAAI,CAChD,iEAAiE;oBACjE,sBAAsB;oBACtBtB,OAAOuD,KAAK,CAAC;wBACXC,OAAOrC,WAAW;wBAClBsC,OAAOvB;wBACP4E,UAAU1F;oBACZ;oBAEF,IAAIyF,QAAQL,IAAI,KAAK,gBAAevB,2BAAAA,QAAS8B,WAAW,GAAE;wBACxD,OAAO9B,QAAQ8B,WAAW,CAACzF,IAAI,CAACtB,OAAO4D,MAAM;oBAC/C;oBACA,OAAOiD;gBACT;YACFX,QAAQ9C,CAAAA,YAAanC,kBAAkB2B,IAAIQ;QAC7C;IACF;IAEA,OAAO;QAAE0B;IAAY;AACvB,EAAE;AAEF,4CAA4C,GAC5C,OAAO,MAAMkC,sBAAsB,CAAC/B,UAIlCpB,qBAAqBoB,QAAQnB,MAAM,EAAEgB,WAAW,CAACG,QAAQrC,EAAE,EAAE;AAE/D,sDAAsD,GACtD,OAAO,MAAMqE,2BAA2BlH,QAAQmH,OAAO,GAGnD;AAAuB;AAE3B;;;CAGC,GACD,OAAO,MAAMC,uBAAuBlH,MAAMmH,MAAM,CAC9CH,oBACAjH,OAAOqF,GAAG,CAAC;IACT,MAAMvB,SAAS,OAAOrD;IACtB,MAAMmC,KAAK,OAAOlC;IAClB,OAAOsG,oBAAoB;QAAElD;QAAQlB;IAAG;AAC1C,IACA"}
1
+ {"version":3,"sources":["../../src/unit-of-work/mongo-unit-of-work.ts"],"sourcesContent":["import {\n Contention,\n type DomainEvent,\n isFailure,\n isStoreFault,\n type StoreFault,\n} from '@entifix/core';\nimport type {\n UnitClaim,\n UnitOfWork,\n UnitOutcome,\n UnitRunOptions,\n} from '@entifix/transactions';\nimport { Context, Effect, Layer, Option, Result, Schedule } from 'effect';\nimport type { ClientSession, Db } from 'mongodb';\n\nimport { translateMongoFault } from '../errors/translate-mongo-error.js';\nimport {\n ensureInboxIndexes,\n INBOX_COLLECTION,\n inboxDocument,\n isDuplicateKey,\n} from '../inbox/store.js';\nimport {\n MongoClientTag,\n MongoDatabaseTag,\n} from '../mongo-database/mongo-database.js';\nimport {\n ensureOutboxIndexes,\n OUTBOX_COLLECTION,\n outboxDocument,\n} from '../outbox/store.js';\nimport {\n COMMAND_INBOX_COLLECTION,\n commandClaimDocument,\n ensureCommandInboxIndexes,\n readCommandReplay,\n rememberCommandReplay,\n} from './command-inbox.js';\n\n/**\n * What a Mongo unit's body writes through: the database and the session bound\n * to the unit's transaction. Every write must pass `{ session }`, or it is not\n * part of the unit.\n */\nexport interface MongoUnit {\n readonly db: Db;\n readonly session: ClientSession;\n}\n\n/** What starts a session — a `MongoClient`, structurally. */\nexport interface MongoSessionSource {\n startSession(): ClientSession;\n}\n\n/**\n * How many times a unit runs before a transient conflict is reported as the\n * failure it is, and how long it waits between runs.\n *\n * Measured against a real replica set: eight units incrementing one document\n * at once conflict on every attempt but one, so a short fixed retry count\n * starves most of them. A jittered exponential back-off spreads the reruns so\n * each eventually runs alone.\n */\nconst ATTEMPTS = 10;\nconst BACK_OFF = Schedule.exponential('5 millis', 1.5).pipe(Schedule.jittered);\n\nconst hasErrorLabel = (error: unknown, label: string): boolean => {\n const candidate = error as {\n hasErrorLabel?: (label: string) => boolean;\n errorLabels?: readonly string[];\n } | null;\n return (\n candidate?.hasErrorLabel?.(label) ??\n candidate?.errorLabels?.includes(label) ??\n false\n );\n};\n\n/**\n * Whether a server label is on `error` or anywhere in its cause chain — a\n * driver error reaches here wrapped once by the adapter and possibly again by\n * the body that called the driver.\n */\nconst labelledInChain = (error: unknown, label: string): boolean => {\n let current: unknown = error;\n for (let depth = 0; current != null && depth < 5; depth += 1) {\n if (hasErrorLabel(current, label)) return true;\n current = (current as { cause?: unknown }).cause;\n }\n return false;\n};\n\n/** A transient conflict, which reruns the whole unit. */\nconst isTransient = (error: unknown): boolean =>\n isFailure(error) && error._tag === 'Contention';\n\nconst fault = (error: unknown): StoreFault => translateMongoFault(error);\n\nconst driver = <A>(run: () => Promise<A>) =>\n Effect.tryPromise({ try: run, catch: fault });\n\n/** A duplicate-key rejection answered as `duplicate` rather than as an error. */\nconst insertOrDuplicate = (run: () => Promise<unknown>) =>\n Effect.tryPromise({\n try: async () => {\n try {\n await run();\n return 'inserted' as const;\n } catch (error) {\n if (isDuplicateKey(error)) return 'duplicate' as const;\n throw error;\n }\n },\n catch: fault,\n });\n\nconst takeClaim = (db: Db, session: ClientSession, claim: UnitClaim) =>\n claim.kind === 'event'\n ? insertOrDuplicate(() =>\n db\n .collection(INBOX_COLLECTION)\n .insertOne(inboxDocument(claim.consumer, claim.eventId), {\n session,\n }),\n )\n : insertOrDuplicate(() =>\n db\n .collection(COMMAND_INBOX_COLLECTION)\n .insertOne(commandClaimDocument(claim.commandId), { session }),\n );\n\n/**\n * Commits, retrying a commit whose result the server could not report. Such a\n * commit may or may not have applied, and Mongo makes retrying it safe.\n */\n/**\n * Retried on the **driver's** error, before translation: the label that says\n * the commit's result is unknown lives on it, and a translated failure no\n * longer carries it.\n */\nconst commit = (session: ClientSession) =>\n Effect.tryPromise({\n try: () => session.commitTransaction(),\n catch: error => error,\n }).pipe(\n Effect.retry({\n times: ATTEMPTS - 1,\n while: error => labelledInChain(error, 'UnknownTransactionCommitResult'),\n }),\n Effect.mapError(fault),\n );\n\n/** Rolls back, and never fails: the unit is already ending for another reason. */\nconst abort = (session: ClientSession) =>\n driver(() => session.abortTransaction()).pipe(Effect.ignore);\n\ntype Decision<A> =\n | { readonly kind: 'commit'; readonly value: A }\n | { readonly kind: 'duplicate' };\n\n/**\n * A {@link UnitOfWork} per database, sharing one client — the shape a\n * per-organization store needs, where the database is resolved inside the\n * request. Each database's indexes are ensured once, before its first unit.\n */\nexport const makeMongoUnitsOfWork = (client: MongoSessionSource) => {\n const ensured = new Map<string, Promise<void>>();\n\n const ensureIndexes = (db: Db) =>\n Effect.suspend(() => {\n const key = db.databaseName;\n const done = ensured.get(key);\n if (done !== undefined) {\n return driver(() => done);\n }\n const pending = Effect.runPromise(\n Effect.all([\n ensureOutboxIndexes(db),\n ensureInboxIndexes(db),\n ensureCommandInboxIndexes(db),\n ]),\n ).then(() => undefined);\n ensured.set(key, pending);\n // A failed ensure must be retried by the next unit, not remembered.\n pending.catch(() => ensured.delete(key));\n return driver(() => pending);\n });\n\n const forDatabase = (db: Db): UnitOfWork<MongoUnit> => {\n const attempt = <A, E, R>(\n body: (unit: {\n readonly handle: MongoUnit;\n emit(event: DomainEvent): void;\n }) => Effect.Effect<A, E, R>,\n options: UnitRunOptions<A, R> | undefined,\n ) =>\n Effect.acquireUseRelease(\n Effect.sync(() => client.startSession()),\n session =>\n Effect.gen(function* () {\n yield* Effect.try({\n try: () => session.startTransaction(),\n catch: fault,\n });\n\n const work = Effect.gen(function* () {\n const claim = options?.claim;\n if (\n claim &&\n (yield* takeClaim(db, session, claim)) === 'duplicate'\n ) {\n return { kind: 'duplicate' } as Decision<A>;\n }\n\n const emitted: DomainEvent[] = [];\n const value = yield* body({\n handle: { db, session },\n emit: event => void emitted.push(event),\n });\n\n if (emitted.length > 0) {\n const written = yield* insertOrDuplicate(() =>\n db\n .collection(OUTBOX_COLLECTION)\n .insertMany(emitted.map(outboxDocument), { session }),\n );\n // An event id already recorded is the outbox's own\n // idempotency claim: what it announces committed before.\n if (written === 'duplicate') {\n return { kind: 'duplicate' } as Decision<A>;\n }\n }\n\n if (claim?.kind === 'command' && options?.replay) {\n const replay = options.replay(value);\n yield* driver(() =>\n rememberCommandReplay(db, session, claim.commandId, replay),\n );\n }\n return { kind: 'commit', value } as Decision<A>;\n });\n\n const decided = yield* Effect.result(work);\n\n if (Result.isFailure(decided)) {\n yield* abort(session);\n const failure = decided.failure;\n if (isStoreFault(failure)) {\n return yield* Effect.fail(failure);\n }\n // A conflict the body surfaced without translating is still the\n // store's, not a business decision: it is retried, not aborted.\n if (labelledInChain(failure, 'TransientTransactionError')) {\n return yield* Effect.fail(new Contention());\n }\n return {\n _tag: 'aborted',\n reason: failure as Exclude<E, StoreFault>,\n } as UnitOutcome<A, Exclude<E, StoreFault>>;\n }\n\n if (decided.success.kind === 'duplicate') {\n yield* abort(session);\n const replay =\n options?.claim?.kind === 'command'\n ? yield* readCommandReplay(db, options.claim.commandId)\n : Option.none();\n return { _tag: 'duplicate', replay } as UnitOutcome<\n A,\n Exclude<E, StoreFault>\n >;\n }\n\n yield* commit(session);\n return {\n _tag: 'committed',\n value: decided.success.value,\n } as UnitOutcome<A, Exclude<E, StoreFault>>;\n }),\n session => driver(() => session.endSession()).pipe(Effect.ignore),\n );\n\n return {\n run: (body, options) =>\n Effect.gen(function* () {\n yield* ensureIndexes(db);\n const outcome = yield* attempt(body, options).pipe(\n // The whole unit runs again, body included: that is why the body\n // must be retry-safe.\n Effect.retry({\n times: ATTEMPTS - 1,\n while: isTransient,\n schedule: BACK_OFF,\n }),\n );\n if (outcome._tag === 'committed' && options?.afterCommit) {\n yield* options.afterCommit.pipe(Effect.ignore);\n }\n return outcome;\n }),\n replay: commandId => readCommandReplay(db, commandId),\n };\n };\n\n return { forDatabase };\n};\n\n/** A {@link UnitOfWork} over one database. */\nexport const makeMongoUnitOfWork = (options: {\n readonly client: MongoSessionSource;\n readonly db: Db;\n}): UnitOfWork<MongoUnit> =>\n makeMongoUnitsOfWork(options.client).forDatabase(options.db);\n\n/** The unit of work over the service's own database. */\nexport class MongoUnitOfWorkTag extends Context.Service<\n MongoUnitOfWorkTag,\n UnitOfWork<MongoUnit>\n>()('MongoUnitOfWorkTag') {}\n\n/**\n * Provides {@link MongoUnitOfWorkTag} from the client and the named database\n * {@link MongoDatabaseLayer} opened.\n */\nexport const MongoUnitOfWorkLayer = Layer.effect(\n MongoUnitOfWorkTag,\n Effect.gen(function* () {\n const client = yield* MongoClientTag;\n const db = yield* MongoDatabaseTag;\n return makeMongoUnitOfWork({ client, db });\n }),\n);\n"],"names":["Contention","isFailure","isStoreFault","Context","Effect","Layer","Option","Result","Schedule","translateMongoFault","ensureInboxIndexes","INBOX_COLLECTION","inboxDocument","isDuplicateKey","MongoClientTag","MongoDatabaseTag","ensureOutboxIndexes","OUTBOX_COLLECTION","outboxDocument","COMMAND_INBOX_COLLECTION","commandClaimDocument","ensureCommandInboxIndexes","readCommandReplay","rememberCommandReplay","ATTEMPTS","BACK_OFF","exponential","pipe","jittered","hasErrorLabel","error","label","candidate","errorLabels","includes","labelledInChain","current","depth","cause","isTransient","_tag","fault","driver","run","tryPromise","try","catch","insertOrDuplicate","takeClaim","db","session","claim","kind","collection","insertOne","consumer","eventId","commandId","commit","commitTransaction","retry","times","while","mapError","abort","abortTransaction","ignore","makeMongoUnitsOfWork","client","ensured","Map","ensureIndexes","suspend","key","databaseName","done","get","undefined","pending","runPromise","all","then","set","delete","forDatabase","attempt","body","options","acquireUseRelease","sync","startSession","gen","startTransaction","work","emitted","value","handle","emit","event","push","length","written","insertMany","map","replay","decided","result","failure","fail","reason","success","none","endSession","outcome","schedule","afterCommit","makeMongoUnitOfWork","MongoUnitOfWorkTag","Service","MongoUnitOfWorkLayer","effect"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@entifix/mongo",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "MongoDB adapter: an entity repository, RSQL-to-Mongo query translation, per-tenant databases, and an optional transactional outbox.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -57,17 +57,17 @@
57
57
  "dependencies": {
58
58
  "@swc/helpers": "0.5.23",
59
59
  "mongodb": "7.7.0",
60
- "@entifix/business": "0.8.0",
61
- "@entifix/core": "0.8.0"
60
+ "@entifix/core": "0.10.0",
61
+ "@entifix/business": "0.10.0"
62
62
  },
63
63
  "devDependencies": {
64
- "effect": "4.0.5",
65
- "@entifix/testing-unit": "0.8.0",
66
- "@entifix/transactions": "0.8.0"
64
+ "effect": "4.0.6",
65
+ "@entifix/testing-unit": "0.10.0",
66
+ "@entifix/transactions": "0.10.0"
67
67
  },
68
68
  "peerDependencies": {
69
- "effect": "^4.0.5",
70
- "@entifix/transactions": "0.8.0"
69
+ "effect": "^4.0.6",
70
+ "@entifix/transactions": "0.10.0"
71
71
  },
72
72
  "peerDependenciesMeta": {
73
73
  "@entifix/transactions": {