@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.
- package/README.md +4 -4
- package/dist/adapters/filter-translator.d.ts +1 -1
- package/dist/adapters/filter-translator.d.ts.map +1 -1
- package/dist/adapters/filter-translator.js +6 -5
- package/dist/adapters/filter-translator.js.map +1 -1
- package/dist/adapters/make-mongo-facet-counter.d.ts +18 -0
- package/dist/adapters/make-mongo-facet-counter.d.ts.map +1 -0
- package/dist/adapters/make-mongo-facet-counter.js +76 -0
- package/dist/adapters/make-mongo-facet-counter.js.map +1 -0
- package/dist/adapters/make-mongo-link-resolver.d.ts.map +1 -1
- package/dist/adapters/make-mongo-link-resolver.js +2 -4
- package/dist/adapters/make-mongo-link-resolver.js.map +1 -1
- package/dist/adapters/make-mongo-repository.d.ts +5 -3
- package/dist/adapters/make-mongo-repository.d.ts.map +1 -1
- package/dist/adapters/make-mongo-repository.js +13 -20
- package/dist/adapters/make-mongo-repository.js.map +1 -1
- package/dist/adapters/mongo-facet-counter-factory.d.ts +18 -0
- package/dist/adapters/mongo-facet-counter-factory.d.ts.map +1 -0
- package/dist/adapters/mongo-facet-counter-factory.js +29 -0
- package/dist/adapters/mongo-facet-counter-factory.js.map +1 -0
- package/dist/adapters/mongo-repository-factory.d.ts.map +1 -1
- package/dist/adapters/mongo-repository-factory.js +2 -4
- package/dist/adapters/mongo-repository-factory.js.map +1 -1
- package/dist/errors/translate-mongo-error.d.ts +30 -0
- package/dist/errors/translate-mongo-error.d.ts.map +1 -0
- package/dist/errors/translate-mongo-error.js +74 -0
- package/dist/errors/translate-mongo-error.js.map +1 -0
- package/dist/inbox/store.d.ts +1 -2
- package/dist/inbox/store.d.ts.map +1 -1
- package/dist/inbox/store.js +3 -8
- package/dist/inbox/store.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/mongo-database/mongo-database.d.ts +3 -3
- package/dist/mongo-database/mongo-database.d.ts.map +1 -1
- package/dist/mongo-database/mongo-database.js +2 -2
- package/dist/mongo-database/mongo-database.js.map +1 -1
- package/dist/mongo-database/mongo-tenant-directory.d.ts.map +1 -1
- package/dist/mongo-database/mongo-tenant-directory.js +2 -4
- package/dist/mongo-database/mongo-tenant-directory.js.map +1 -1
- package/dist/mongo-database/mongo-tenant-resolver.d.ts.map +1 -1
- package/dist/mongo-database/mongo-tenant-resolver.js +18 -7
- package/dist/mongo-database/mongo-tenant-resolver.js.map +1 -1
- package/dist/outbox/relay.d.ts +50 -9
- package/dist/outbox/relay.d.ts.map +1 -1
- package/dist/outbox/relay.js +67 -26
- package/dist/outbox/relay.js.map +1 -1
- package/dist/outbox/store.d.ts +3 -3
- package/dist/outbox/store.d.ts.map +1 -1
- package/dist/outbox/store.js +8 -22
- package/dist/outbox/store.js.map +1 -1
- package/dist/saga/store.d.ts +3 -4
- package/dist/saga/store.d.ts.map +1 -1
- package/dist/saga/store.js +10 -15
- package/dist/saga/store.js.map +1 -1
- package/dist/unit-of-work/command-inbox.d.ts +2 -3
- package/dist/unit-of-work/command-inbox.d.ts.map +1 -1
- package/dist/unit-of-work/command-inbox.js +3 -7
- package/dist/unit-of-work/command-inbox.js.map +1 -1
- package/dist/unit-of-work/mongo-unit-of-work.d.ts.map +1 -1
- package/dist/unit-of-work/mongo-unit-of-work.js +30 -26
- package/dist/unit-of-work/mongo-unit-of-work.js.map +1 -1
- package/package.json +8 -8
package/dist/saga/store.js.map
CHANGED
|
@@ -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,
|
|
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>,
|
|
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,
|
|
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)=>
|
|
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)=>
|
|
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 {
|
|
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":"
|
|
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 {
|
|
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
|
|
36
|
-
const
|
|
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:
|
|
40
|
+
catch: fault
|
|
39
41
|
});
|
|
40
|
-
/** A duplicate-key rejection answered as `duplicate` rather than as an error. */ const insertOrDuplicate = (
|
|
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:
|
|
52
|
+
catch: fault
|
|
51
53
|
});
|
|
52
|
-
const takeClaim = (db, session, claim)=>claim.kind === 'event' ? insertOrDuplicate(
|
|
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
|
-
*/
|
|
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(
|
|
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(
|
|
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(
|
|
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:
|
|
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(
|
|
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(
|
|
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
|
|
142
|
+
if (isStoreFault(failure)) {
|
|
139
143
|
return yield* Effect.fail(failure);
|
|
140
144
|
}
|
|
141
|
-
// A conflict the body surfaced without
|
|
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
|
|
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(
|
|
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.
|
|
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/
|
|
61
|
-
"@entifix/
|
|
60
|
+
"@entifix/core": "0.10.0",
|
|
61
|
+
"@entifix/business": "0.10.0"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
|
-
"effect": "4.0.
|
|
65
|
-
"@entifix/testing-unit": "0.
|
|
66
|
-
"@entifix/transactions": "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.
|
|
70
|
-
"@entifix/transactions": "0.
|
|
69
|
+
"effect": "^4.0.6",
|
|
70
|
+
"@entifix/transactions": "0.10.0"
|
|
71
71
|
},
|
|
72
72
|
"peerDependenciesMeta": {
|
|
73
73
|
"@entifix/transactions": {
|