@entifix/mongo 0.9.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 (54) hide show
  1. package/dist/adapters/make-mongo-facet-counter.d.ts.map +1 -1
  2. package/dist/adapters/make-mongo-facet-counter.js +3 -4
  3. package/dist/adapters/make-mongo-facet-counter.js.map +1 -1
  4. package/dist/adapters/make-mongo-link-resolver.d.ts.map +1 -1
  5. package/dist/adapters/make-mongo-link-resolver.js +2 -4
  6. package/dist/adapters/make-mongo-link-resolver.js.map +1 -1
  7. package/dist/adapters/make-mongo-repository.d.ts +3 -3
  8. package/dist/adapters/make-mongo-repository.d.ts.map +1 -1
  9. package/dist/adapters/make-mongo-repository.js +11 -18
  10. package/dist/adapters/make-mongo-repository.js.map +1 -1
  11. package/dist/adapters/mongo-facet-counter-factory.d.ts.map +1 -1
  12. package/dist/adapters/mongo-facet-counter-factory.js +2 -4
  13. package/dist/adapters/mongo-facet-counter-factory.js.map +1 -1
  14. package/dist/adapters/mongo-repository-factory.d.ts.map +1 -1
  15. package/dist/adapters/mongo-repository-factory.js +2 -4
  16. package/dist/adapters/mongo-repository-factory.js.map +1 -1
  17. package/dist/errors/translate-mongo-error.d.ts +30 -0
  18. package/dist/errors/translate-mongo-error.d.ts.map +1 -0
  19. package/dist/errors/translate-mongo-error.js +74 -0
  20. package/dist/errors/translate-mongo-error.js.map +1 -0
  21. package/dist/inbox/store.d.ts +1 -2
  22. package/dist/inbox/store.d.ts.map +1 -1
  23. package/dist/inbox/store.js +3 -8
  24. package/dist/inbox/store.js.map +1 -1
  25. package/dist/mongo-database/mongo-database.d.ts +3 -3
  26. package/dist/mongo-database/mongo-database.d.ts.map +1 -1
  27. package/dist/mongo-database/mongo-database.js +2 -2
  28. package/dist/mongo-database/mongo-database.js.map +1 -1
  29. package/dist/mongo-database/mongo-tenant-directory.d.ts.map +1 -1
  30. package/dist/mongo-database/mongo-tenant-directory.js +2 -4
  31. package/dist/mongo-database/mongo-tenant-directory.js.map +1 -1
  32. package/dist/mongo-database/mongo-tenant-resolver.d.ts.map +1 -1
  33. package/dist/mongo-database/mongo-tenant-resolver.js +18 -7
  34. package/dist/mongo-database/mongo-tenant-resolver.js.map +1 -1
  35. package/dist/outbox/relay.d.ts +50 -9
  36. package/dist/outbox/relay.d.ts.map +1 -1
  37. package/dist/outbox/relay.js +67 -26
  38. package/dist/outbox/relay.js.map +1 -1
  39. package/dist/outbox/store.d.ts +3 -3
  40. package/dist/outbox/store.d.ts.map +1 -1
  41. package/dist/outbox/store.js +8 -22
  42. package/dist/outbox/store.js.map +1 -1
  43. package/dist/saga/store.d.ts +3 -4
  44. package/dist/saga/store.d.ts.map +1 -1
  45. package/dist/saga/store.js +10 -15
  46. package/dist/saga/store.js.map +1 -1
  47. package/dist/unit-of-work/command-inbox.d.ts +2 -3
  48. package/dist/unit-of-work/command-inbox.d.ts.map +1 -1
  49. package/dist/unit-of-work/command-inbox.js +3 -7
  50. package/dist/unit-of-work/command-inbox.js.map +1 -1
  51. package/dist/unit-of-work/mongo-unit-of-work.d.ts.map +1 -1
  52. package/dist/unit-of-work/mongo-unit-of-work.js +30 -26
  53. package/dist/unit-of-work/mongo-unit-of-work.js.map +1 -1
  54. package/package.json +8 -8
@@ -1,6 +1,6 @@
1
1
  import { _ as _extends } from "@swc/helpers/_/_extends";
2
- import { EntifixConnError } from '@entifix/core';
3
2
  import { Effect } from 'effect';
3
+ import { translateMongoError } from '../errors/translate-mongo-error.js';
4
4
  /**
5
5
  * The outbox collection, and it lives in the **same database as the entity it
6
6
  * describes** — never in a shared control-plane store.
@@ -83,10 +83,7 @@ const isDuplicateKey = (error)=>typeof error === 'object' && error !== null && e
83
83
  });
84
84
  return result.modifiedCount === 1;
85
85
  },
86
- catch: (error)=>new EntifixConnError('Failed to revive a quarantined outbox entry', error, {
87
- eventId,
88
- db: db.databaseName
89
- })
86
+ catch: (error)=>translateMongoError(error)
90
87
  });
91
88
  /**
92
89
  * Ensures the two indexes the outbox depends on.
@@ -123,9 +120,7 @@ const isDuplicateKey = (error)=>typeof error === 'object' && error !== null && e
123
120
  }
124
121
  });
125
122
  },
126
- catch: (error)=>new EntifixConnError('Failed to create outbox indexes', error, {
127
- db: db.databaseName
128
- })
123
+ catch: (error)=>translateMongoError(error)
129
124
  });
130
125
  /**
131
126
  * Mongo-backed {@link TransactionOutbox} over one database.
@@ -136,7 +131,6 @@ const isDuplicateKey = (error)=>typeof error === 'object' && error !== null && e
136
131
  * tested without a broker.
137
132
  */ export const makeMongoOutbox = (db)=>{
138
133
  const collection = db.collection(OUTBOX_COLLECTION);
139
- const fail = (message, error, details)=>new EntifixConnError(message, error, details);
140
134
  return {
141
135
  enqueue: (event)=>Effect.tryPromise({
142
136
  try: async ()=>{
@@ -152,9 +146,7 @@ const isDuplicateKey = (error)=>typeof error === 'object' && error !== null && e
152
146
  throw error;
153
147
  }
154
148
  },
155
- catch: (error)=>fail('Failed to enqueue outbox entry', error, {
156
- eventId: event.id
157
- })
149
+ catch: (error)=>translateMongoError(error)
158
150
  }),
159
151
  pending: (limit)=>Effect.tryPromise({
160
152
  // Oldest first, so `accepted` reaches the bus before the terminal event
@@ -165,9 +157,7 @@ const isDuplicateKey = (error)=>typeof error === 'object' && error !== null && e
165
157
  }, WITHOUT_MONGO_ID).sort({
166
158
  createdAt: 1
167
159
  }).limit(limit).toArray(),
168
- catch: (error)=>fail('Failed to read pending outbox entries', error, {
169
- db: db.databaseName
170
- })
160
+ catch: (error)=>translateMongoError(error)
171
161
  }),
172
162
  markSent: (entry)=>Effect.tryPromise({
173
163
  try: ()=>collection.updateOne({
@@ -177,9 +167,7 @@ const isDuplicateKey = (error)=>typeof error === 'object' && error !== null && e
177
167
  sent: true
178
168
  }
179
169
  }),
180
- catch: (error)=>fail('Failed to mark outbox entry sent', error, {
181
- eventId: entry.eventId
182
- })
170
+ catch: (error)=>translateMongoError(error)
183
171
  }).pipe(Effect.asVoid),
184
172
  stats: ()=>Effect.tryPromise({
185
173
  // Two counts and one `findOne`, rather than reading the entries: the
@@ -213,7 +201,7 @@ const isDuplicateKey = (error)=>typeof error === 'object' && error !== null && e
213
201
  oldestPendingAt: oldest.createdAt
214
202
  });
215
203
  },
216
- catch: (error)=>fail('Failed to read outbox stats', error)
204
+ catch: (error)=>translateMongoError(error)
217
205
  }),
218
206
  recordFailure: (entry, error, quarantine)=>Effect.tryPromise({
219
207
  // `$inc` rather than a read-modify-write: two relays can be draining the
@@ -231,9 +219,7 @@ const isDuplicateKey = (error)=>typeof error === 'object' && error !== null && e
231
219
  quarantined: quarantine
232
220
  }
233
221
  }),
234
- catch: (cause)=>fail('Failed to record an outbox publish failure', cause, {
235
- eventId: entry.eventId
236
- })
222
+ catch: (cause)=>translateMongoError(cause)
237
223
  }).pipe(Effect.asVoid)
238
224
  };
239
225
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/outbox/store.ts"],"sourcesContent":["import { type DomainEvent, EntifixConnError } from '@entifix/core';\nimport type { OutboxEntry, TransactionOutbox } from '@entifix/transactions';\nimport { Effect } from 'effect';\nimport type { Db } from 'mongodb';\n\n/**\n * The outbox collection, and it lives in the **same database as the entity it\n * describes** — never in a shared control-plane store.\n *\n * Two reasons, and the second only shows up later. Same database means the\n * entity write and its event are one single-database transaction, which stays a\n * single-*shard* transaction if this ever shards; a control-plane outbox would\n * be cross-database from the first commit. And an outbox holds event payloads:\n * a `TransactionEvent` carries no tenant data, but `catalog.published` carries a\n * whole offering and `order.placed` a whole receipt, and neither may land in the\n * control plane on its way to the bus.\n *\n * That rule is why this lives here rather than in one service. It applies\n * identically to a per-organization `tenant_<id>` and to a single platform-plane\n * database, and the second publisher is what turned a copy into a seam.\n */\nexport const OUTBOX_COLLECTION = 'transaction_outbox';\n\n/** Drop Mongo's internal `_id` from every read. */\nconst WITHOUT_MONGO_ID = { projection: { _id: 0 } } as const;\n\n/** Mongo's duplicate-key error, which is how a replayed command is recognised. */\nconst DUPLICATE_KEY = 11000;\n\nconst isDuplicateKey = (error: unknown): boolean =>\n typeof error === 'object' &&\n error !== null &&\n (error as { code?: unknown }).code === DUPLICATE_KEY;\n\n/**\n * The stored shape of an outbox entry.\n *\n * Exported because the `completed` entry is **not** written through the port:\n * it has to join the entity write's session to be atomic with it, and a session\n * cannot live in a framework-free contract. The handler inserts this document\n * itself; everything else goes through {@link makeMongoOutbox}.\n */\nexport const outboxDocument = (event: DomainEvent): OutboxEntry => ({\n eventId: event.id,\n event,\n sent: false,\n // Defaulted here rather than at each write site because there are two of\n // them: the relay's `enqueue`, and the transaction handler's own insert\n // inside the entity's Mongo session. An entry missing `attempts` would sort\n // into `pending` and then fail `attempts + 1` arithmetic on a `undefined`.\n attempts: 0,\n quarantined: false,\n createdAt: event.at,\n});\n\n/**\n * Puts a **quarantined** entry back in the queue, and touches nothing else.\n *\n * A rebuild walk's repair for the one failure measured on a live lab: with the\n * broker down long enough, the relay spends `outbox.maxAttempts` on an\n * announcement and quarantines it (the deliberate skip-so-the-head-moves),\n * after which the tenant store reads one thing and the storefront shows another\n * with nothing reporting it. Re-emitting cannot help on its own — the event id is\n * `<offeringId>:<publishedAt>` and the unique index answers `duplicate`.\n *\n * ⚠️ **`quarantined: true` is in the filter, not the update.** An entry that is\n * merely pending keeps its `attempts`, and one already `sent` is never re-sent —\n * which is what keeps this from quietly becoming \"re-announce everything\", a\n * walk that would re-publish the whole fleet on every boot and un-quarantine\n * genuine poison each time.\n *\n * Not a `TransactionOutbox` method: the port is framework-free and shared by\n * every publisher, and this is a repair specific to the rebuild. `outboxDocument`\n * is exported beside it for the same class of reason.\n *\n * Answers whether an entry was actually revived.\n */\nexport const reviveQuarantined = (db: Db, eventId: string) =>\n Effect.tryPromise({\n try: async () => {\n const result = await db\n .collection<OutboxEntry>(OUTBOX_COLLECTION)\n .updateOne(\n { eventId, quarantined: true },\n {\n $set: { sent: false, quarantined: false, attempts: 0 },\n // The failure that put it here is spent; leaving it would make the\n // next quarantine's log name an error from a different outage.\n $unset: { lastError: '' },\n },\n );\n return result.modifiedCount === 1;\n },\n catch: error =>\n new EntifixConnError(\n 'Failed to revive a quarantined outbox entry',\n error,\n {\n eventId,\n db: db.databaseName,\n },\n ),\n });\n\n/**\n * Ensures the two indexes the outbox depends on.\n *\n * The unique one is load-bearing rather than defensive: it *is* the idempotency\n * check behind the client-generated transaction id, so a replayed command is\n * rejected by the storage engine rather than by a read-then-write race. It is on\n * `eventId` — `<transactionId>:<step>` for a transaction — so the claim and the\n * bus's deduplication key are one value rather than two that can drift. The\n * partial one keeps the relay's `pending` query cheap once the collection fills\n * with sent entries; it filters on `quarantined` too, so an entry past its\n * ceiling leaves the index rather than being read and skipped forever.\n *\n * Mongo rejects a `createIndex` that reuses a key pattern with a different\n * `partialFilterExpression` (`IndexOptionsConflict`), and this runs on every\n * sweep and every create. A database that predates the `quarantined` filter\n * therefore fails here on every pass — which is loud only because the sweep now\n * logs what it catches. The fix is `pnpm run <app>:dev:reset`; nothing runs in\n * production, so there is no migration to write.\n */\nexport const ensureOutboxIndexes = (db: Db) =>\n Effect.tryPromise({\n try: async () => {\n const collection = db.collection(OUTBOX_COLLECTION);\n await collection.createIndex({ eventId: 1 }, { unique: true });\n await collection.createIndex(\n { createdAt: 1 },\n { partialFilterExpression: { sent: false, quarantined: false } },\n );\n },\n catch: error =>\n new EntifixConnError('Failed to create outbox indexes', error, {\n db: db.databaseName,\n }),\n });\n\n/**\n * Mongo-backed {@link TransactionOutbox} over one database.\n *\n * `db` is closed over, so every method's Effect has `R = never` — the same\n * technique `makeMongoRepository` uses. There is no publishing here: the relay\n * composes these methods with the event bus, which is what lets the relay be\n * tested without a broker.\n */\nexport const makeMongoOutbox = (db: Db): TransactionOutbox => {\n const collection = db.collection<OutboxEntry>(OUTBOX_COLLECTION);\n\n const fail = (\n message: string,\n error: unknown,\n details?: Record<string, unknown>,\n ) => new EntifixConnError(message, error, details);\n\n return {\n enqueue: event =>\n Effect.tryPromise({\n try: async () => {\n try {\n await collection.insertOne(outboxDocument(event));\n return 'enqueued' as const;\n } catch (error) {\n // Not a failure: the unique index rejected a second claim for this\n // message id, which is precisely how a retry is identified.\n if (isDuplicateKey(error)) {\n return 'duplicate' as const;\n }\n throw error;\n }\n },\n catch: error =>\n fail('Failed to enqueue outbox entry', error, { eventId: event.id }),\n }),\n\n pending: limit =>\n Effect.tryPromise({\n // Oldest first, so `accepted` reaches the bus before the terminal event\n // for the same transaction and the tracker folds them in order.\n try: () =>\n collection\n .find({ sent: false, quarantined: false }, WITHOUT_MONGO_ID)\n .sort({ createdAt: 1 })\n .limit(limit)\n .toArray(),\n catch: error =>\n fail('Failed to read pending outbox entries', error, {\n db: db.databaseName,\n }),\n }),\n\n markSent: entry =>\n Effect.tryPromise({\n try: () =>\n collection.updateOne(\n { eventId: entry.eventId },\n { $set: { sent: true } },\n ),\n catch: error =>\n fail('Failed to mark outbox entry sent', error, {\n eventId: entry.eventId,\n }),\n }).pipe(Effect.asVoid),\n\n stats: () =>\n Effect.tryPromise({\n // Two counts and one `findOne`, rather than reading the entries: the\n // pending set can be arbitrarily large when the relay is stuck, which\n // is exactly the moment this must stay cheap. Both queries ride the\n // partial index on `{ createdAt: 1 }` filtered to the same predicate.\n try: async () => {\n const pendingFilter = { sent: false, quarantined: false };\n const [pending, quarantined, oldest] = await Promise.all([\n collection.countDocuments(pendingFilter),\n collection.countDocuments({ quarantined: true }),\n collection.findOne(pendingFilter, {\n sort: { createdAt: 1 },\n projection: { _id: 0, createdAt: 1 },\n }),\n ]);\n\n return {\n pending,\n quarantined,\n // Absent rather than zero when the outbox is empty: an age of zero\n // and \"nothing is waiting\" are opposite conditions, and a gauge\n // that reports 0 for both is indistinguishable from a healthy relay\n // at exactly the moment it is not one.\n ...(oldest === null ? {} : { oldestPendingAt: oldest.createdAt }),\n };\n },\n catch: error => fail('Failed to read outbox stats', error),\n }),\n\n recordFailure: (entry, error, quarantine) =>\n Effect.tryPromise({\n // `$inc` rather than a read-modify-write: two relays can be draining the\n // same tenant (the request's inline drain and the daemon sweep), and an\n // absolute write would let one of them lose the other's attempt and\n // stretch the ceiling indefinitely.\n try: () =>\n collection.updateOne(\n { eventId: entry.eventId },\n {\n $inc: { attempts: 1 },\n $set: { lastError: error, quarantined: quarantine },\n },\n ),\n catch: cause =>\n fail('Failed to record an outbox publish failure', cause, {\n eventId: entry.eventId,\n }),\n }).pipe(Effect.asVoid),\n };\n};\n"],"names":["EntifixConnError","Effect","OUTBOX_COLLECTION","WITHOUT_MONGO_ID","projection","_id","DUPLICATE_KEY","isDuplicateKey","error","code","outboxDocument","event","eventId","id","sent","attempts","quarantined","createdAt","at","reviveQuarantined","db","tryPromise","try","result","collection","updateOne","$set","$unset","lastError","modifiedCount","catch","databaseName","ensureOutboxIndexes","createIndex","unique","partialFilterExpression","makeMongoOutbox","fail","message","details","enqueue","insertOne","pending","limit","find","sort","toArray","markSent","entry","pipe","asVoid","stats","pendingFilter","oldest","Promise","all","countDocuments","findOne","oldestPendingAt","recordFailure","quarantine","$inc","cause"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/outbox/store.ts"],"sourcesContent":["import type { DomainEvent } from '@entifix/core';\nimport type { OutboxEntry, TransactionOutbox } from '@entifix/transactions';\nimport { Effect } from 'effect';\nimport type { Db } from 'mongodb';\n\nimport { translateMongoError } from '../errors/translate-mongo-error.js';\n\n/**\n * The outbox collection, and it lives in the **same database as the entity it\n * describes** — never in a shared control-plane store.\n *\n * Two reasons, and the second only shows up later. Same database means the\n * entity write and its event are one single-database transaction, which stays a\n * single-*shard* transaction if this ever shards; a control-plane outbox would\n * be cross-database from the first commit. And an outbox holds event payloads:\n * a `TransactionEvent` carries no tenant data, but `catalog.published` carries a\n * whole offering and `order.placed` a whole receipt, and neither may land in the\n * control plane on its way to the bus.\n *\n * That rule is why this lives here rather than in one service. It applies\n * identically to a per-organization `tenant_<id>` and to a single platform-plane\n * database, and the second publisher is what turned a copy into a seam.\n */\nexport const OUTBOX_COLLECTION = 'transaction_outbox';\n\n/** Drop Mongo's internal `_id` from every read. */\nconst WITHOUT_MONGO_ID = { projection: { _id: 0 } } as const;\n\n/** Mongo's duplicate-key error, which is how a replayed command is recognised. */\nconst DUPLICATE_KEY = 11000;\n\nconst isDuplicateKey = (error: unknown): boolean =>\n typeof error === 'object' &&\n error !== null &&\n (error as { code?: unknown }).code === DUPLICATE_KEY;\n\n/**\n * The stored shape of an outbox entry.\n *\n * Exported because the `completed` entry is **not** written through the port:\n * it has to join the entity write's session to be atomic with it, and a session\n * cannot live in a framework-free contract. The handler inserts this document\n * itself; everything else goes through {@link makeMongoOutbox}.\n */\nexport const outboxDocument = (event: DomainEvent): OutboxEntry => ({\n eventId: event.id,\n event,\n sent: false,\n // Defaulted here rather than at each write site because there are two of\n // them: the relay's `enqueue`, and the transaction handler's own insert\n // inside the entity's Mongo session. An entry missing `attempts` would sort\n // into `pending` and then fail `attempts + 1` arithmetic on a `undefined`.\n attempts: 0,\n quarantined: false,\n createdAt: event.at,\n});\n\n/**\n * Puts a **quarantined** entry back in the queue, and touches nothing else.\n *\n * A rebuild walk's repair for the one failure measured on a live lab: with the\n * broker down long enough, the relay spends `outbox.maxAttempts` on an\n * announcement and quarantines it (the deliberate skip-so-the-head-moves),\n * after which the tenant store reads one thing and the storefront shows another\n * with nothing reporting it. Re-emitting cannot help on its own — the event id is\n * `<offeringId>:<publishedAt>` and the unique index answers `duplicate`.\n *\n * ⚠️ **`quarantined: true` is in the filter, not the update.** An entry that is\n * merely pending keeps its `attempts`, and one already `sent` is never re-sent —\n * which is what keeps this from quietly becoming \"re-announce everything\", a\n * walk that would re-publish the whole fleet on every boot and un-quarantine\n * genuine poison each time.\n *\n * Not a `TransactionOutbox` method: the port is framework-free and shared by\n * every publisher, and this is a repair specific to the rebuild. `outboxDocument`\n * is exported beside it for the same class of reason.\n *\n * Answers whether an entry was actually revived.\n */\nexport const reviveQuarantined = (db: Db, eventId: string) =>\n Effect.tryPromise({\n try: async () => {\n const result = await db\n .collection<OutboxEntry>(OUTBOX_COLLECTION)\n .updateOne(\n { eventId, quarantined: true },\n {\n $set: { sent: false, quarantined: false, attempts: 0 },\n // The failure that put it here is spent; leaving it would make the\n // next quarantine's log name an error from a different outage.\n $unset: { lastError: '' },\n },\n );\n return result.modifiedCount === 1;\n },\n catch: error => translateMongoError(error),\n });\n\n/**\n * Ensures the two indexes the outbox depends on.\n *\n * The unique one is load-bearing rather than defensive: it *is* the idempotency\n * check behind the client-generated transaction id, so a replayed command is\n * rejected by the storage engine rather than by a read-then-write race. It is on\n * `eventId` — `<transactionId>:<step>` for a transaction — so the claim and the\n * bus's deduplication key are one value rather than two that can drift. The\n * partial one keeps the relay's `pending` query cheap once the collection fills\n * with sent entries; it filters on `quarantined` too, so an entry past its\n * ceiling leaves the index rather than being read and skipped forever.\n *\n * Mongo rejects a `createIndex` that reuses a key pattern with a different\n * `partialFilterExpression` (`IndexOptionsConflict`), and this runs on every\n * sweep and every create. A database that predates the `quarantined` filter\n * therefore fails here on every pass — which is loud only because the sweep now\n * logs what it catches. The fix is `pnpm run <app>:dev:reset`; nothing runs in\n * production, so there is no migration to write.\n */\nexport const ensureOutboxIndexes = (db: Db) =>\n Effect.tryPromise({\n try: async () => {\n const collection = db.collection(OUTBOX_COLLECTION);\n await collection.createIndex({ eventId: 1 }, { unique: true });\n await collection.createIndex(\n { createdAt: 1 },\n { partialFilterExpression: { sent: false, quarantined: false } },\n );\n },\n catch: error => translateMongoError(error),\n });\n\n/**\n * Mongo-backed {@link TransactionOutbox} over one database.\n *\n * `db` is closed over, so every method's Effect has `R = never` — the same\n * technique `makeMongoRepository` uses. There is no publishing here: the relay\n * composes these methods with the event bus, which is what lets the relay be\n * tested without a broker.\n */\nexport const makeMongoOutbox = (db: Db): TransactionOutbox => {\n const collection = db.collection<OutboxEntry>(OUTBOX_COLLECTION);\n\n return {\n enqueue: event =>\n Effect.tryPromise({\n try: async () => {\n try {\n await collection.insertOne(outboxDocument(event));\n return 'enqueued' as const;\n } catch (error) {\n // Not a failure: the unique index rejected a second claim for this\n // message id, which is precisely how a retry is identified.\n if (isDuplicateKey(error)) {\n return 'duplicate' as const;\n }\n throw error;\n }\n },\n catch: error => translateMongoError(error),\n }),\n\n pending: limit =>\n Effect.tryPromise({\n // Oldest first, so `accepted` reaches the bus before the terminal event\n // for the same transaction and the tracker folds them in order.\n try: () =>\n collection\n .find({ sent: false, quarantined: false }, WITHOUT_MONGO_ID)\n .sort({ createdAt: 1 })\n .limit(limit)\n .toArray(),\n catch: error => translateMongoError(error),\n }),\n\n markSent: entry =>\n Effect.tryPromise({\n try: () =>\n collection.updateOne(\n { eventId: entry.eventId },\n { $set: { sent: true } },\n ),\n catch: error => translateMongoError(error),\n }).pipe(Effect.asVoid),\n\n stats: () =>\n Effect.tryPromise({\n // Two counts and one `findOne`, rather than reading the entries: the\n // pending set can be arbitrarily large when the relay is stuck, which\n // is exactly the moment this must stay cheap. Both queries ride the\n // partial index on `{ createdAt: 1 }` filtered to the same predicate.\n try: async () => {\n const pendingFilter = { sent: false, quarantined: false };\n const [pending, quarantined, oldest] = await Promise.all([\n collection.countDocuments(pendingFilter),\n collection.countDocuments({ quarantined: true }),\n collection.findOne(pendingFilter, {\n sort: { createdAt: 1 },\n projection: { _id: 0, createdAt: 1 },\n }),\n ]);\n\n return {\n pending,\n quarantined,\n // Absent rather than zero when the outbox is empty: an age of zero\n // and \"nothing is waiting\" are opposite conditions, and a gauge\n // that reports 0 for both is indistinguishable from a healthy relay\n // at exactly the moment it is not one.\n ...(oldest === null ? {} : { oldestPendingAt: oldest.createdAt }),\n };\n },\n catch: error => translateMongoError(error),\n }),\n\n recordFailure: (entry, error, quarantine) =>\n Effect.tryPromise({\n // `$inc` rather than a read-modify-write: two relays can be draining the\n // same tenant (the request's inline drain and the daemon sweep), and an\n // absolute write would let one of them lose the other's attempt and\n // stretch the ceiling indefinitely.\n try: () =>\n collection.updateOne(\n { eventId: entry.eventId },\n {\n $inc: { attempts: 1 },\n $set: { lastError: error, quarantined: quarantine },\n },\n ),\n catch: cause => translateMongoError(cause),\n }).pipe(Effect.asVoid),\n };\n};\n"],"names":["Effect","translateMongoError","OUTBOX_COLLECTION","WITHOUT_MONGO_ID","projection","_id","DUPLICATE_KEY","isDuplicateKey","error","code","outboxDocument","event","eventId","id","sent","attempts","quarantined","createdAt","at","reviveQuarantined","db","tryPromise","try","result","collection","updateOne","$set","$unset","lastError","modifiedCount","catch","ensureOutboxIndexes","createIndex","unique","partialFilterExpression","makeMongoOutbox","enqueue","insertOne","pending","limit","find","sort","toArray","markSent","entry","pipe","asVoid","stats","pendingFilter","oldest","Promise","all","countDocuments","findOne","oldestPendingAt","recordFailure","quarantine","$inc","cause"],"mappings":"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"}
@@ -1,4 +1,3 @@
1
- import { EntifixConnError } from '@entifix/core';
2
1
  import { type SagaStore, SagaStoreTag } from '@entifix/transactions';
3
2
  import { Effect, Layer } from 'effect';
4
3
  import type { Db } from 'mongodb';
@@ -14,14 +13,14 @@ export declare const SAGA_COLLECTION = "saga_instances";
14
13
  * second instance for one flow, and `{ state, updatedAt }` for the sweep —
15
14
  * without it every pass is a scan of every saga ever run, on a schedule.
16
15
  */
17
- export declare const ensureSagaIndexes: (db: Db) => Effect.Effect<void, EntifixConnError, never>;
16
+ export declare const ensureSagaIndexes: (db: Db) => Effect.Effect<void, import("@entifix/core").StoreFailure, never>;
18
17
  /** {@link SagaStore} over one Mongo database. */
19
18
  export declare const makeMongoSagaStore: (db: Db) => SagaStore;
20
19
  /**
21
20
  * Provides {@link SagaStoreTag} from the service's own database, creating the
22
21
  * indexes first.
23
22
  */
24
- export declare const MongoSagaStoreLayer: Layer.Layer<SagaStoreTag, EntifixConnError, MongoDatabaseTag>;
23
+ export declare const MongoSagaStoreLayer: Layer.Layer<SagaStoreTag, import("@entifix/core").StoreFailure, MongoDatabaseTag>;
25
24
  /**
26
25
  * Provides {@link SagaStoreTag} from a database the host **names**, resolved on
27
26
  * the shared client — for a process whose saga store is one of several stores
@@ -32,5 +31,5 @@ export declare const MongoSagaStoreLayer: Layer.Layer<SagaStoreTag, EntifixConnE
32
31
  */
33
32
  export declare const mongoSagaStoreLayer: (options: {
34
33
  readonly database: string;
35
- }) => Layer.Layer<SagaStoreTag, EntifixConnError, MongoClientTag>;
34
+ }) => Layer.Layer<SagaStoreTag, import("@entifix/core").StoreFailure, MongoClientTag>;
36
35
  //# sourceMappingURL=store.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/saga/store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAIL,KAAK,SAAS,EACd,YAAY,EACb,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAElC,OAAO,EACL,cAAc,EACd,gBAAgB,EACjB,MAAM,qCAAqC,CAAC;AAE7C;;;;GAIG;AACH,eAAO,MAAM,eAAe,mBAAmB,CAAC;AAahD;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,GAAI,IAAI,EAAE,iDAUpC,CAAC;AAEL,iDAAiD;AACjD,eAAO,MAAM,kBAAkB,GAAI,IAAI,EAAE,KAAG,SAoI3C,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,mBAAmB,+DAO/B,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,GAAI,SAAS;IAAE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,gEASvE,CAAC"}
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/saga/store.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,SAAS,EACd,YAAY,EACb,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAGlC,OAAO,EACL,cAAc,EACd,gBAAgB,EACjB,MAAM,qCAAqC,CAAC;AAE7C;;;;GAIG;AACH,eAAO,MAAM,eAAe,mBAAmB,CAAC;AAUhD;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,GAAI,IAAI,EAAE,qEAOpC,CAAC;AAEL,iDAAiD;AACjD,eAAO,MAAM,kBAAkB,GAAI,IAAI,EAAE,KAAG,SAoI3C,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,mBAAmB,mFAO/B,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,GAAI,SAAS;IAAE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,oFASvE,CAAC"}
@@ -1,7 +1,7 @@
1
1
  import { _ as _extends } from "@swc/helpers/_/_extends";
2
- import { EntifixConnError } from '@entifix/core';
3
2
  import { SagaStoreTag } from '@entifix/transactions';
4
3
  import { Effect, Layer } from 'effect';
4
+ import { translateMongoError } from '../errors/translate-mongo-error.js';
5
5
  import { MongoClientTag, MongoDatabaseTag } from '../mongo-database/mongo-database.js';
6
6
  /**
7
7
  * Saga instances: one document per flow, the durable record a coordinator
@@ -12,9 +12,6 @@ import { MongoClientTag, MongoDatabaseTag } from '../mongo-database/mongo-databa
12
12
  'RUNNING',
13
13
  'COMPENSATING'
14
14
  ];
15
- const failed = (message, sagaId)=>(error)=>new EntifixConnError(message, error, {
16
- sagaId
17
- });
18
15
  const sagaCollection = (db)=>db.collection(SAGA_COLLECTION);
19
16
  const deadline = (olderThanMs)=>new Date(Date.now() - olderThanMs).toISOString();
20
17
  /**
@@ -33,9 +30,7 @@ const deadline = (olderThanMs)=>new Date(Date.now() - olderThanMs).toISOString()
33
30
  updatedAt: 1
34
31
  });
35
32
  },
36
- catch: (error)=>new EntifixConnError('Failed to ensure saga indexes', error, {
37
- database: db.databaseName
38
- })
33
+ catch: (error)=>translateMongoError(error)
39
34
  });
40
35
  /** {@link SagaStore} over one Mongo database. */ export const makeMongoSagaStore = (db)=>{
41
36
  const collection = sagaCollection(db);
@@ -56,7 +51,7 @@ const deadline = (olderThanMs)=>new Date(Date.now() - olderThanMs).toISOString()
56
51
  upsert: true
57
52
  });
58
53
  },
59
- catch: failed('Failed to start the saga instance', instance.sagaId)
54
+ catch: (error)=>translateMongoError(error)
60
55
  }),
61
56
  beginStep: (sagaId, stepIndex)=>Effect.tryPromise({
62
57
  try: async ()=>{
@@ -69,7 +64,7 @@ const deadline = (olderThanMs)=>new Date(Date.now() - olderThanMs).toISOString()
69
64
  }
70
65
  });
71
66
  },
72
- catch: failed('Failed to record the step transition', sagaId)
67
+ catch: (error)=>translateMongoError(error)
73
68
  }),
74
69
  recordOutcome: (sagaId, outcome)=>Effect.tryPromise({
75
70
  try: async ()=>{
@@ -87,7 +82,7 @@ const deadline = (olderThanMs)=>new Date(Date.now() - olderThanMs).toISOString()
87
82
  }
88
83
  });
89
84
  },
90
- catch: failed('Failed to record the step outcome', sagaId)
85
+ catch: (error)=>translateMongoError(error)
91
86
  }),
92
87
  settle: (sagaId, state, error)=>Effect.tryPromise({
93
88
  try: async ()=>{
@@ -101,7 +96,7 @@ const deadline = (olderThanMs)=>new Date(Date.now() - olderThanMs).toISOString()
101
96
  }
102
97
  });
103
98
  },
104
- catch: failed('Failed to settle the saga instance', sagaId)
99
+ catch: (error)=>translateMongoError(error)
105
100
  }),
106
101
  /**
107
102
  * Take ownership of a stale instance, or answer `undefined`.
@@ -138,7 +133,7 @@ const deadline = (olderThanMs)=>new Date(Date.now() - olderThanMs).toISOString()
138
133
  });
139
134
  return claimed != null ? claimed : undefined;
140
135
  },
141
- catch: failed('Failed to claim the saga instance', sagaId)
136
+ catch: (error)=>translateMongoError(error)
142
137
  }),
143
138
  /**
144
139
  * Flag one step's calls as given back, addressing the outcome by its step
@@ -161,7 +156,7 @@ const deadline = (olderThanMs)=>new Date(Date.now() - olderThanMs).toISOString()
161
156
  ]
162
157
  });
163
158
  },
164
- catch: failed('Failed to mark the step compensated', sagaId)
159
+ catch: (error)=>translateMongoError(error)
165
160
  }),
166
161
  get: (sagaId)=>Effect.tryPromise({
167
162
  try: async ()=>{
@@ -174,7 +169,7 @@ const deadline = (olderThanMs)=>new Date(Date.now() - olderThanMs).toISOString()
174
169
  }
175
170
  })) != null ? _ref : undefined;
176
171
  },
177
- catch: failed('Failed to read the saga instance', sagaId)
172
+ catch: (error)=>translateMongoError(error)
178
173
  }),
179
174
  /**
180
175
  * Instances stuck mid-flight past a deadline. ⚠️ `COMPENSATING` counts: a
@@ -195,7 +190,7 @@ const deadline = (olderThanMs)=>new Date(Date.now() - olderThanMs).toISOString()
195
190
  _id: 0
196
191
  }
197
192
  }).toArray(),
198
- catch: failed('Failed to find stale saga instances', '')
193
+ catch: (error)=>translateMongoError(error)
199
194
  })
200
195
  };
201
196
  };
@@ -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":";AAAA,SAKEA,YAAY,QACP,wBAAwB;AAC/B,SAASC,MAAM,EAAEC,KAAK,QAAQ,SAAS;AAGvC,SAASC,mBAAmB,QAAQ,qCAAqC;AACzE,SACEC,cAAc,EACdC,gBAAgB,QACX,sCAAsC;AAE7C;;;;CAIC,GACD,OAAO,MAAMC,kBAAkB,iBAAiB;AAEhD,iDAAiD,GACjD,MAAMC,aAAmC;IAAC;IAAW;CAAe;AAEpE,MAAMC,iBAAiB,CAACC,KAAWA,GAAGC,UAAU,CAAeJ;AAE/D,MAAMK,WAAW,CAACC,cAChB,IAAIC,KAAKA,KAAKC,GAAG,KAAKF,aAAaG,WAAW;AAEhD;;;;CAIC,GACD,OAAO,MAAMC,oBAAoB,CAACP,KAChCR,OAAOgB,UAAU,CAAC;QAChBC,KAAK;YACH,MAAMV,eAAeC,IAAIU,WAAW,CAAC;gBAAEC,QAAQ;YAAE,GAAG;gBAAEC,QAAQ;YAAK;YACnE,MAAMb,eAAeC,IAAIU,WAAW,CAAC;gBAAEG,OAAO;gBAAGC,WAAW;YAAE;QAChE;QACAC,OAAOC,CAAAA,QAAStB,oBAAoBsB;IACtC,GAAG;AAEL,+CAA+C,GAC/C,OAAO,MAAMC,qBAAqB,CAACjB;IACjC,MAAMC,aAAaF,eAAeC;IAClC,MAAMkB,QAAQ,IAAM,IAAId,OAAOE,WAAW;IAE1C,OAAO;QACLa,OAAOC,CAAAA,WACL5B,OAAOgB,UAAU,CAAC;gBAChBC,KAAK;oBACH,uEAAuE;oBACvE,kEAAkE;oBAClE,qEAAqE;oBACrE,MAAMR,WAAWoB,SAAS,CACxB;wBAAEV,QAAQS,SAAST,MAAM;oBAAC,GAC1B;wBAAEW,cAAc,aAAKF;4BAAUN,WAAWI;;oBAAU,GACpD;wBAAEK,QAAQ;oBAAK;gBAEnB;gBACAR,OAAOC,CAAAA,QAAStB,oBAAoBsB;YACtC;QAEFQ,WAAW,CAACb,QAAQc,YAClBjC,OAAOgB,UAAU,CAAC;gBAChBC,KAAK;oBACH,MAAMR,WAAWoB,SAAS,CACxB;wBAAEV;oBAAO,GACT;wBAAEe,MAAM;4BAAED;4BAAWX,WAAWI;wBAAQ;oBAAE;gBAE9C;gBACAH,OAAOC,CAAAA,QAAStB,oBAAoBsB;YACtC;QAEFW,eAAe,CAAChB,QAAQiB,UACtBpC,OAAOgB,UAAU,CAAC;gBAChBC,KAAK;oBACH,MAAMR,WAAWoB,SAAS,CACxB;wBAAEV;oBAAO,GACT,gEAAgE;oBAChE,oEAAoE;oBACpE,qEAAqE;oBACrE;wBAAEkB,OAAO;4BAAEC,UAAUF;wBAAQ;wBAAGF,MAAM;4BAAEZ,WAAWI;wBAAQ;oBAAE;gBAEjE;gBACAH,OAAOC,CAAAA,QAAStB,oBAAoBsB;YACtC;QAEFe,QAAQ,CAACpB,QAAQE,OAAOG,QACtBxB,OAAOgB,UAAU,CAAC;gBAChBC,KAAK;oBACH,MAAMR,WAAWoB,SAAS,CACxB;wBAAEV;oBAAO,GACT;wBAAEe,MAAM;4BAAEb;4BAAOG;4BAAOF,WAAWI;wBAAQ;oBAAE;gBAEjD;gBACAH,OAAOC,CAAAA,QAAStB,oBAAoBsB;YACtC;QAEF;;;;;;;;KAQC,GACDgB,gBAAgB,CAACrB,QAAQR,cACvBX,OAAOgB,UAAU,CAAC;gBAChBC,KAAK;oBACH,MAAMwB,UAAU,MAAMhC,WAAWiC,gBAAgB,CAC/C;wBACEvB;wBACAE,OAAO;4BAAEsB,KAAK;mCAAIrC;6BAAW;wBAAC;wBAC9BgB,WAAW;4BAAEsB,KAAKlC,SAASC;wBAAa;oBAC1C,GACA;wBAAEkC,MAAM;4BAAEC,gBAAgB;wBAAE;wBAAGZ,MAAM;4BAAEZ,WAAWI;wBAAQ;oBAAE,GAC5D;wBAAEqB,gBAAgB;wBAASC,YAAY;4BAAEC,KAAK;wBAAE;oBAAE;oBAEpD,OAAQR,kBAAAA,UAAWS;gBACrB;gBACA3B,OAAOC,CAAAA,QAAStB,oBAAoBsB;YACtC;QAEF;;;;KAIC,GACD2B,iBAAiB,CAAChC,QAAQiC,SACxBpD,OAAOgB,UAAU,CAAC;gBAChBC,KAAK;oBACH,MAAMR,WAAWoB,SAAS,CACxB;wBAAEV;oBAAO,GACT;wBACEe,MAAM;4BACJ,iCAAiC;4BACjCZ,WAAWI;wBACb;oBACF,GACA;wBAAE2B,cAAc;4BAAC;gCAAE,gBAAgBD;4BAAO;yBAAE;oBAAC;gBAEjD;gBACA7B,OAAOC,CAAAA,QAAStB,oBAAoBsB;YACtC;QAEF8B,KAAKnC,CAAAA,SACHnB,OAAOgB,UAAU,CAAC;gBAChBC,KAAK;wBACD;4BAAA,OAAA,MAAMR,WAAW8C,OAAO,CAAC;wBAAEpC;oBAAO,GAAG;wBAAE6B,YAAY;4BAAEC,KAAK;wBAAE;oBAAE,cAA9D,OACAC;;gBACJ3B,OAAOC,CAAAA,QAAStB,oBAAoBsB;YACtC;QAEF;;;;KAIC,GACDgC,WAAW7C,CAAAA,cACTX,OAAOgB,UAAU,CAAC;gBAChBC,KAAK,UACF,MAAMR,WACJgD,IAAI,CACH;wBACEpC,OAAO;4BAAEsB,KAAK;mCAAIrC;6BAAW;wBAAC;wBAC9BgB,WAAW;4BAAEsB,KAAKlC,SAASC;wBAAa;oBAC1C,GACA;wBAAEqC,YAAY;4BAAEC,KAAK;wBAAE;oBAAE,GAE1BS,OAAO;gBACZnC,OAAOC,CAAAA,QAAStB,oBAAoBsB;YACtC;IACJ;AACF,EAAE;AAEF;;;CAGC,GACD,OAAO,MAAMmC,sBAAsB1D,MAAM2D,MAAM,CAC7C7D,cACAC,OAAO6D,GAAG,CAAC;IACT,MAAMrD,KAAK,OAAOJ;IAClB,OAAOW,kBAAkBP;IACzB,OAAOiB,mBAAmBjB;AAC5B,IACA;AAEF;;;;;;;CAOC,GACD,OAAO,MAAMsD,sBAAsB,CAACC,UAClC9D,MAAM2D,MAAM,CACV7D,cACAC,OAAO6D,GAAG,CAAC;QACT,MAAMG,SAAS,OAAO7D;QACtB,MAAMK,KAAKwD,OAAOxD,EAAE,CAACuD,QAAQE,QAAQ;QACrC,OAAOlD,kBAAkBP;QACzB,OAAOiB,mBAAmBjB;IAC5B,IACA"}
@@ -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);