@entifix/mongo 0.1.0-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 (50) hide show
  1. package/LICENSE +21 -0
  2. package/dist/adapters/filter-translator.d.ts +19 -0
  3. package/dist/adapters/filter-translator.d.ts.map +1 -0
  4. package/dist/adapters/filter-translator.js +142 -0
  5. package/dist/adapters/filter-translator.js.map +1 -0
  6. package/dist/adapters/make-mongo-link-resolver.d.ts +14 -0
  7. package/dist/adapters/make-mongo-link-resolver.d.ts.map +1 -0
  8. package/dist/adapters/make-mongo-link-resolver.js +31 -0
  9. package/dist/adapters/make-mongo-link-resolver.js.map +1 -0
  10. package/dist/adapters/make-mongo-repository.d.ts +19 -0
  11. package/dist/adapters/make-mongo-repository.d.ts.map +1 -0
  12. package/dist/adapters/make-mongo-repository.js +122 -0
  13. package/dist/adapters/make-mongo-repository.js.map +1 -0
  14. package/dist/inbox/store.d.ts +50 -0
  15. package/dist/inbox/store.d.ts.map +1 -0
  16. package/dist/inbox/store.js +86 -0
  17. package/dist/inbox/store.js.map +1 -0
  18. package/dist/index.d.ts +7 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +8 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/mongo-database/mongo-database.d.ts +52 -0
  23. package/dist/mongo-database/mongo-database.d.ts.map +1 -0
  24. package/dist/mongo-database/mongo-database.js +52 -0
  25. package/dist/mongo-database/mongo-database.js.map +1 -0
  26. package/dist/mongo-database/mongo-health-probe.d.ts +28 -0
  27. package/dist/mongo-database/mongo-health-probe.d.ts.map +1 -0
  28. package/dist/mongo-database/mongo-health-probe.js +39 -0
  29. package/dist/mongo-database/mongo-health-probe.js.map +1 -0
  30. package/dist/mongo-database/mongo-tenant-resolver.d.ts +16 -0
  31. package/dist/mongo-database/mongo-tenant-resolver.d.ts.map +1 -0
  32. package/dist/mongo-database/mongo-tenant-resolver.js +40 -0
  33. package/dist/mongo-database/mongo-tenant-resolver.js.map +1 -0
  34. package/dist/outbox/metrics.d.ts +46 -0
  35. package/dist/outbox/metrics.d.ts.map +1 -0
  36. package/dist/outbox/metrics.js +63 -0
  37. package/dist/outbox/metrics.js.map +1 -0
  38. package/dist/outbox/relay.d.ts +89 -0
  39. package/dist/outbox/relay.d.ts.map +1 -0
  40. package/dist/outbox/relay.js +119 -0
  41. package/dist/outbox/relay.js.map +1 -0
  42. package/dist/outbox/store.d.ts +84 -0
  43. package/dist/outbox/store.d.ts.map +1 -0
  44. package/dist/outbox/store.js +242 -0
  45. package/dist/outbox/store.js.map +1 -0
  46. package/dist/transactions/index.d.ts +16 -0
  47. package/dist/transactions/index.d.ts.map +1 -0
  48. package/dist/transactions/index.js +16 -0
  49. package/dist/transactions/index.js.map +1 -0
  50. package/package.json +82 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/outbox/relay.ts"],"sourcesContent":["import { ShutdownRegistryTag } from '@entifix/business';\nimport {\n type EventBus,\n EventBusTag,\n type OutboxStats,\n type TransactionOutbox,\n} from '@entifix/transactions';\nimport { Context, Duration, Effect, Either, Fiber } from 'effect';\n\nimport { MongoDatabaseTag } from '../mongo-database/mongo-database.js';\nimport { recordOutboxStats } from './metrics.js';\nimport { ensureOutboxIndexes, makeMongoOutbox } from './store.js';\n\n/** How often the sweep looks for entries the fast path did not carry. */\nexport const SWEEP_INTERVAL = Duration.seconds(15);\n/** Entries drained per pass, per database. */\nexport const BATCH = 100;\n\n/**\n * Publish attempts an outbox entry gets before it is quarantined, from\n * config-service.\n *\n * Configuration rather than a constant because it is a genuine operational\n * tunable: raising it while a flaky broker settles is a config edit, and the\n * next sweep reads the new value. That is the opposite of a subscription's\n * `maxAttempts`, which becomes an immutable queue argument and therefore stays\n * a literal beside its declaration.\n *\n * ⚠️ **Read it with `getNumber`, never a cast.** A value of `'five'` casts to\n * `NaN`, every `attempts >= maxAttempts` comparison goes false, and nothing is\n * ever quarantined — a relay that looks healthy while its head never moves.\n */\nexport class OutboxMaxAttempts extends Context.Tag('OutboxMaxAttempts')<\n OutboxMaxAttempts,\n number\n>() {}\n\n/** What {@link drainOutbox} needs beyond the ports it publishes through. */\nexport interface DrainOptions {\n readonly maxAttempts: number;\n /** The database being drained; carried so a quarantine log names it. */\n readonly database: string;\n}\n\n/**\n * Publish one batch of pending entries, oldest first, and answer how many went.\n *\n * Two orderings are load-bearing here and neither is obvious:\n *\n * - A **still-deliverable** failure returns immediately rather than continuing.\n * The entries behind this one keep their order relative to it, which is what\n * stops a terminal event overtaking the `accepted` for the same transaction.\n * - A **quarantined** failure continues. That is ADR 0030's deliberate\n * skip-so-the-head-moves: an entry that can never publish must stop blocking\n * every entry behind it forever.\n */\nexport const drainOutbox = (\n outbox: TransactionOutbox,\n bus: EventBus,\n options: DrainOptions,\n) =>\n Effect.gen(function* () {\n const entries = yield* outbox.pending(BATCH);\n let sent = 0;\n for (const entry of entries) {\n const outcome = yield* Effect.either(bus.publish(entry.event));\n\n if (Either.isLeft(outcome)) {\n const attempts = entry.attempts + 1;\n const quarantine = attempts >= options.maxAttempts;\n\n yield* outbox.recordFailure(entry, outcome.left.message, quarantine);\n\n if (!quarantine) {\n return sent;\n }\n\n yield* Effect.logError('outbox entry quarantined').pipe(\n Effect.annotateLogs({\n database: options.database,\n eventId: entry.eventId,\n eventName: entry.event.name,\n attempts,\n lastError: outcome.left.message,\n }),\n );\n continue;\n }\n\n yield* outbox.markSent(entry);\n sent += 1;\n }\n return sent;\n });\n\n/**\n * Sample a sweep's depth and age.\n *\n * Defaults to {@link recordOutboxStats}, and a caller overrides it only to\n * assert what was sampled. There is no \"no sink\" option on purpose: an outbox\n * whose depth nothing reports is the failure the gauges exist to make visible.\n */\nexport type OutboxStatsSink = (\n database: string,\n stats: OutboxStats,\n) => Effect.Effect<void>;\n\n/**\n * Drain the one database this service owns, then sample its depth.\n *\n * The single-database counterpart to marketplace-admin's tenant walk. A\n * platform-plane store is one named database, so there is nothing to enumerate\n * and `listDatabases` — which needs a `MongoClient` and cluster-wide rights —\n * never comes into it.\n *\n * Stats are sampled **after** the drain, so the gauge reports what is still\n * waiting rather than what was waiting when the pass began.\n *\n * Every failure is caught and logged. This runs inside `Effect.forever`, and an\n * error that escapes ends the daemon silently — a relay that stopped is\n * indistinguishable from a relay with nothing to do.\n */\nexport const sweepOutbox = (\n outbox: TransactionOutbox,\n bus: EventBus,\n options: DrainOptions & { readonly onStats?: OutboxStatsSink },\n) =>\n Effect.gen(function* () {\n yield* drainOutbox(outbox, bus, options);\n const sink = options.onStats ?? recordOutboxStats;\n yield* sink(options.database, yield* outbox.stats());\n }).pipe(\n Effect.catchAll(error =>\n Effect.logError('outbox sweep failed').pipe(\n Effect.annotateLogs({\n database: options.database,\n error: String(error),\n }),\n ),\n ),\n );\n\n/**\n * Start the relay for a service that owns exactly one database.\n *\n * ⚠️ **The shutdown hook is `flush`, not `stop-intake`, and the phase is the\n * whole point.** `AmqpEventBusLayer` registers `stop-intake` to cancel consumers\n * and drain deliveries; this runs after that, so the last sweep publishes into a\n * connection that is still open. Registered the other way round it would try to\n * publish through a channel already closing, and the entries it was holding\n * would sit until the next boot.\n *\n * The daemon is `delay |> forever`, so the first sweep is one interval away\n * rather than at boot — the write path drains inline, and a sweep racing the\n * index bootstrap on a cold database only produces noise.\n */\nexport const startOutboxRelay = (options?: {\n readonly onStats?: OutboxStatsSink;\n}) =>\n Effect.gen(function* () {\n const db = yield* MongoDatabaseTag;\n const bus = yield* EventBusTag;\n const maxAttempts = yield* OutboxMaxAttempts;\n const shutdown = yield* ShutdownRegistryTag;\n\n yield* ensureOutboxIndexes(db);\n const outbox = makeMongoOutbox(db);\n\n const sweepOnce = sweepOutbox(outbox, bus, {\n maxAttempts,\n database: db.databaseName,\n ...(options?.onStats ? { onStats: options.onStats } : {}),\n });\n\n const daemon = yield* Effect.forkDaemon(\n sweepOnce.pipe(Effect.delay(SWEEP_INTERVAL), Effect.forever),\n );\n\n yield* shutdown.register({\n name: 'outbox-relay',\n phase: 'flush',\n run: Fiber.interrupt(daemon).pipe(Effect.andThen(sweepOnce)),\n });\n });\n"],"names":["ShutdownRegistryTag","EventBusTag","Context","Duration","Effect","Either","Fiber","MongoDatabaseTag","recordOutboxStats","ensureOutboxIndexes","makeMongoOutbox","SWEEP_INTERVAL","seconds","BATCH","OutboxMaxAttempts","Tag","drainOutbox","outbox","bus","options","gen","entries","pending","sent","entry","outcome","either","publish","event","isLeft","attempts","quarantine","maxAttempts","recordFailure","left","message","logError","pipe","annotateLogs","database","eventId","eventName","name","lastError","markSent","sweepOutbox","sink","onStats","stats","catchAll","error","String","startOutboxRelay","db","shutdown","sweepOnce","databaseName","daemon","forkDaemon","delay","forever","register","phase","run","interrupt","andThen"],"mappings":"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"}
@@ -0,0 +1,84 @@
1
+ import { type DomainEvent, EntifixConnError } from '@entifix/core';
2
+ import type { OutboxEntry, TransactionOutbox } from '@entifix/transactions';
3
+ import { Effect } from 'effect';
4
+ import type { Db } from 'mongodb';
5
+ /**
6
+ * The outbox collection, and it lives in the **same database as the entity it
7
+ * describes** — never in a shared control-plane store.
8
+ *
9
+ * Two reasons, and the second only shows up later. Same database means the
10
+ * entity write and its event are one single-database transaction, which stays a
11
+ * single-*shard* transaction if this ever shards; a control-plane outbox would
12
+ * be cross-database from the first commit. And an outbox holds event payloads:
13
+ * a `TransactionEvent` carries no tenant data, but `catalog.published` carries a
14
+ * whole offering and `order.placed` a whole receipt, and neither may land in the
15
+ * control plane on its way to the bus.
16
+ *
17
+ * That rule is why this lives here rather than in one service. It applies
18
+ * identically to a per-organization `tenant_<id>` and to a single platform-plane
19
+ * database, and the second publisher is what turned a copy into a seam
20
+ * ([ADR 0028](../../../../../../docs/adr/0028-the-transaction-id-is-the-clients-and-its-event-ships-with-the-write.md)).
21
+ */
22
+ export declare const OUTBOX_COLLECTION = "transaction_outbox";
23
+ /**
24
+ * The stored shape of an outbox entry.
25
+ *
26
+ * Exported because the `completed` entry is **not** written through the port:
27
+ * it has to join the entity write's session to be atomic with it, and a session
28
+ * cannot live in a framework-free contract. The handler inserts this document
29
+ * itself; everything else goes through {@link makeMongoOutbox}.
30
+ */
31
+ export declare const outboxDocument: (event: DomainEvent) => OutboxEntry;
32
+ /**
33
+ * Puts a **quarantined** entry back in the queue, and touches nothing else.
34
+ *
35
+ * The rebuild walk's repair for the one failure ADR 0048 measured: with the
36
+ * broker down long enough, the relay spends `outbox.maxAttempts` on an
37
+ * announcement and quarantines it (ADR 0030's deliberate skip-so-the-head-moves),
38
+ * after which the tenant store reads one thing and the storefront shows another
39
+ * with nothing reporting it. Re-emitting cannot help on its own — the event id is
40
+ * `<offeringId>:<publishedAt>` and the unique index answers `duplicate`.
41
+ *
42
+ * ⚠️ **`quarantined: true` is in the filter, not the update.** An entry that is
43
+ * merely pending keeps its `attempts`, and one already `sent` is never re-sent —
44
+ * which is what keeps this from quietly becoming "re-announce everything", a
45
+ * walk that would re-publish the whole fleet on every boot and un-quarantine
46
+ * genuine poison each time.
47
+ *
48
+ * Not a `TransactionOutbox` method: the port is framework-free and shared by
49
+ * every publisher, and this is a repair specific to the rebuild. `outboxDocument`
50
+ * is exported beside it for the same class of reason.
51
+ *
52
+ * Answers whether an entry was actually revived.
53
+ */
54
+ export declare const reviveQuarantined: (db: Db, eventId: string) => Effect.Effect<boolean, EntifixConnError, never>;
55
+ /**
56
+ * Ensures the two indexes the outbox depends on.
57
+ *
58
+ * The unique one is load-bearing rather than defensive: it *is* the idempotency
59
+ * check behind the client-generated transaction id, so a replayed command is
60
+ * rejected by the storage engine rather than by a read-then-write race. It is on
61
+ * `eventId` — `<transactionId>:<step>` for a transaction — so the claim and the
62
+ * bus's deduplication key are one value rather than two that can drift. The
63
+ * partial one keeps the relay's `pending` query cheap once the collection fills
64
+ * with sent entries; it filters on `quarantined` too, so an entry past its
65
+ * ceiling leaves the index rather than being read and skipped forever.
66
+ *
67
+ * Mongo rejects a `createIndex` that reuses a key pattern with a different
68
+ * `partialFilterExpression` (`IndexOptionsConflict`), and this runs on every
69
+ * sweep and every create. A database that predates the `quarantined` filter
70
+ * therefore fails here on every pass — which is loud only because the sweep now
71
+ * logs what it catches. The fix is `pnpm run <app>:dev:reset`; nothing runs in
72
+ * production, so there is no migration to write.
73
+ */
74
+ export declare const ensureOutboxIndexes: (db: Db) => Effect.Effect<void, EntifixConnError, never>;
75
+ /**
76
+ * Mongo-backed {@link TransactionOutbox} over one database.
77
+ *
78
+ * `db` is closed over, so every method's Effect has `R = never` — the same
79
+ * technique `makeMongoRepository` uses. There is no publishing here: the relay
80
+ * composes these methods with the event bus, which is what lets the relay be
81
+ * tested without a broker.
82
+ */
83
+ export declare const makeMongoOutbox: (db: Db) => TransactionOutbox;
84
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/outbox/store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,WAAW,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACnE,OAAO,KAAK,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC5E,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAElC;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,iBAAiB,uBAAuB,CAAC;AAatD;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,GAAI,OAAO,WAAW,KAAG,WAWlD,CAAC;AAEH;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,iBAAiB,GAAI,IAAI,EAAE,EAAE,SAAS,MAAM,oDAyBrD,CAAC;AAEL;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,mBAAmB,GAAI,IAAI,EAAE,iDActC,CAAC;AAEL;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,GAAI,IAAI,EAAE,KAAG,iBA4GxC,CAAC"}
@@ -0,0 +1,242 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ import { EntifixConnError } from '@entifix/core';
3
+ import { Effect } from 'effect';
4
+ /**
5
+ * The outbox collection, and it lives in the **same database as the entity it
6
+ * describes** — never in a shared control-plane store.
7
+ *
8
+ * Two reasons, and the second only shows up later. Same database means the
9
+ * entity write and its event are one single-database transaction, which stays a
10
+ * single-*shard* transaction if this ever shards; a control-plane outbox would
11
+ * be cross-database from the first commit. And an outbox holds event payloads:
12
+ * a `TransactionEvent` carries no tenant data, but `catalog.published` carries a
13
+ * whole offering and `order.placed` a whole receipt, and neither may land in the
14
+ * control plane on its way to the bus.
15
+ *
16
+ * That rule is why this lives here rather than in one service. It applies
17
+ * identically to a per-organization `tenant_<id>` and to a single platform-plane
18
+ * database, and the second publisher is what turned a copy into a seam
19
+ * ([ADR 0028](../../../../../../docs/adr/0028-the-transaction-id-is-the-clients-and-its-event-ships-with-the-write.md)).
20
+ */ export const OUTBOX_COLLECTION = 'transaction_outbox';
21
+ /** Drop Mongo's internal `_id` from every read. */ const WITHOUT_MONGO_ID = {
22
+ projection: {
23
+ _id: 0
24
+ }
25
+ };
26
+ /** Mongo's duplicate-key error, which is how a replayed command is recognised. */ const DUPLICATE_KEY = 11000;
27
+ const isDuplicateKey = (error)=>typeof error === 'object' && error !== null && error.code === DUPLICATE_KEY;
28
+ /**
29
+ * The stored shape of an outbox entry.
30
+ *
31
+ * Exported because the `completed` entry is **not** written through the port:
32
+ * it has to join the entity write's session to be atomic with it, and a session
33
+ * cannot live in a framework-free contract. The handler inserts this document
34
+ * itself; everything else goes through {@link makeMongoOutbox}.
35
+ */ export const outboxDocument = (event)=>({
36
+ eventId: event.id,
37
+ event,
38
+ sent: false,
39
+ // Defaulted here rather than at each write site because there are two of
40
+ // them: the relay's `enqueue`, and the transaction handler's own insert
41
+ // inside the entity's Mongo session. An entry missing `attempts` would sort
42
+ // into `pending` and then fail `attempts + 1` arithmetic on a `undefined`.
43
+ attempts: 0,
44
+ quarantined: false,
45
+ createdAt: event.at
46
+ });
47
+ /**
48
+ * Puts a **quarantined** entry back in the queue, and touches nothing else.
49
+ *
50
+ * The rebuild walk's repair for the one failure ADR 0048 measured: with the
51
+ * broker down long enough, the relay spends `outbox.maxAttempts` on an
52
+ * announcement and quarantines it (ADR 0030's deliberate skip-so-the-head-moves),
53
+ * after which the tenant store reads one thing and the storefront shows another
54
+ * with nothing reporting it. Re-emitting cannot help on its own — the event id is
55
+ * `<offeringId>:<publishedAt>` and the unique index answers `duplicate`.
56
+ *
57
+ * ⚠️ **`quarantined: true` is in the filter, not the update.** An entry that is
58
+ * merely pending keeps its `attempts`, and one already `sent` is never re-sent —
59
+ * which is what keeps this from quietly becoming "re-announce everything", a
60
+ * walk that would re-publish the whole fleet on every boot and un-quarantine
61
+ * genuine poison each time.
62
+ *
63
+ * Not a `TransactionOutbox` method: the port is framework-free and shared by
64
+ * every publisher, and this is a repair specific to the rebuild. `outboxDocument`
65
+ * is exported beside it for the same class of reason.
66
+ *
67
+ * Answers whether an entry was actually revived.
68
+ */ export const reviveQuarantined = (db, eventId)=>Effect.tryPromise({
69
+ try: async ()=>{
70
+ const result = await db.collection(OUTBOX_COLLECTION).updateOne({
71
+ eventId,
72
+ quarantined: true
73
+ }, {
74
+ $set: {
75
+ sent: false,
76
+ quarantined: false,
77
+ attempts: 0
78
+ },
79
+ // The failure that put it here is spent; leaving it would make the
80
+ // next quarantine's log name an error from a different outage.
81
+ $unset: {
82
+ lastError: ''
83
+ }
84
+ });
85
+ return result.modifiedCount === 1;
86
+ },
87
+ catch: (error)=>new EntifixConnError('Failed to revive a quarantined outbox entry', error, {
88
+ eventId,
89
+ db: db.databaseName
90
+ })
91
+ });
92
+ /**
93
+ * Ensures the two indexes the outbox depends on.
94
+ *
95
+ * The unique one is load-bearing rather than defensive: it *is* the idempotency
96
+ * check behind the client-generated transaction id, so a replayed command is
97
+ * rejected by the storage engine rather than by a read-then-write race. It is on
98
+ * `eventId` — `<transactionId>:<step>` for a transaction — so the claim and the
99
+ * bus's deduplication key are one value rather than two that can drift. The
100
+ * partial one keeps the relay's `pending` query cheap once the collection fills
101
+ * with sent entries; it filters on `quarantined` too, so an entry past its
102
+ * ceiling leaves the index rather than being read and skipped forever.
103
+ *
104
+ * Mongo rejects a `createIndex` that reuses a key pattern with a different
105
+ * `partialFilterExpression` (`IndexOptionsConflict`), and this runs on every
106
+ * sweep and every create. A database that predates the `quarantined` filter
107
+ * therefore fails here on every pass — which is loud only because the sweep now
108
+ * logs what it catches. The fix is `pnpm run <app>:dev:reset`; nothing runs in
109
+ * production, so there is no migration to write.
110
+ */ export const ensureOutboxIndexes = (db)=>Effect.tryPromise({
111
+ try: async ()=>{
112
+ const collection = db.collection(OUTBOX_COLLECTION);
113
+ await collection.createIndex({
114
+ eventId: 1
115
+ }, {
116
+ unique: true
117
+ });
118
+ await collection.createIndex({
119
+ createdAt: 1
120
+ }, {
121
+ partialFilterExpression: {
122
+ sent: false,
123
+ quarantined: false
124
+ }
125
+ });
126
+ },
127
+ catch: (error)=>new EntifixConnError('Failed to create outbox indexes', error, {
128
+ db: db.databaseName
129
+ })
130
+ });
131
+ /**
132
+ * Mongo-backed {@link TransactionOutbox} over one database.
133
+ *
134
+ * `db` is closed over, so every method's Effect has `R = never` — the same
135
+ * technique `makeMongoRepository` uses. There is no publishing here: the relay
136
+ * composes these methods with the event bus, which is what lets the relay be
137
+ * tested without a broker.
138
+ */ export const makeMongoOutbox = (db)=>{
139
+ const collection = db.collection(OUTBOX_COLLECTION);
140
+ const fail = (message, error, details)=>new EntifixConnError(message, error, details);
141
+ return {
142
+ enqueue: (event)=>Effect.tryPromise({
143
+ try: async ()=>{
144
+ try {
145
+ await collection.insertOne(outboxDocument(event));
146
+ return 'enqueued';
147
+ } catch (error) {
148
+ // Not a failure: the unique index rejected a second claim for this
149
+ // message id, which is precisely how a retry is identified.
150
+ if (isDuplicateKey(error)) {
151
+ return 'duplicate';
152
+ }
153
+ throw error;
154
+ }
155
+ },
156
+ catch: (error)=>fail('Failed to enqueue outbox entry', error, {
157
+ eventId: event.id
158
+ })
159
+ }),
160
+ pending: (limit)=>Effect.tryPromise({
161
+ // Oldest first, so `accepted` reaches the bus before the terminal event
162
+ // for the same transaction and the tracker folds them in order.
163
+ try: ()=>collection.find({
164
+ sent: false,
165
+ quarantined: false
166
+ }, WITHOUT_MONGO_ID).sort({
167
+ createdAt: 1
168
+ }).limit(limit).toArray(),
169
+ catch: (error)=>fail('Failed to read pending outbox entries', error, {
170
+ db: db.databaseName
171
+ })
172
+ }),
173
+ markSent: (entry)=>Effect.tryPromise({
174
+ try: ()=>collection.updateOne({
175
+ eventId: entry.eventId
176
+ }, {
177
+ $set: {
178
+ sent: true
179
+ }
180
+ }),
181
+ catch: (error)=>fail('Failed to mark outbox entry sent', error, {
182
+ eventId: entry.eventId
183
+ })
184
+ }).pipe(Effect.asVoid),
185
+ stats: ()=>Effect.tryPromise({
186
+ // Two counts and one `findOne`, rather than reading the entries: the
187
+ // pending set can be arbitrarily large when the relay is stuck, which
188
+ // is exactly the moment this must stay cheap. Both queries ride the
189
+ // partial index on `{ createdAt: 1 }` filtered to the same predicate.
190
+ try: async ()=>{
191
+ const pendingFilter = {
192
+ sent: false,
193
+ quarantined: false
194
+ };
195
+ const [pending, quarantined, oldest] = await Promise.all([
196
+ collection.countDocuments(pendingFilter),
197
+ collection.countDocuments({
198
+ quarantined: true
199
+ }),
200
+ collection.findOne(pendingFilter, {
201
+ sort: {
202
+ createdAt: 1
203
+ },
204
+ projection: {
205
+ _id: 0,
206
+ createdAt: 1
207
+ }
208
+ })
209
+ ]);
210
+ return _extends({
211
+ pending,
212
+ quarantined
213
+ }, oldest === null ? {} : {
214
+ oldestPendingAt: oldest.createdAt
215
+ });
216
+ },
217
+ catch: (error)=>fail('Failed to read outbox stats', error)
218
+ }),
219
+ recordFailure: (entry, error, quarantine)=>Effect.tryPromise({
220
+ // `$inc` rather than a read-modify-write: two relays can be draining the
221
+ // same tenant (the request's inline drain and the daemon sweep), and an
222
+ // absolute write would let one of them lose the other's attempt and
223
+ // stretch the ceiling indefinitely.
224
+ try: ()=>collection.updateOne({
225
+ eventId: entry.eventId
226
+ }, {
227
+ $inc: {
228
+ attempts: 1
229
+ },
230
+ $set: {
231
+ lastError: error,
232
+ quarantined: quarantine
233
+ }
234
+ }),
235
+ catch: (cause)=>fail('Failed to record an outbox publish failure', cause, {
236
+ eventId: entry.eventId
237
+ })
238
+ }).pipe(Effect.asVoid)
239
+ };
240
+ };
241
+
242
+ //# sourceMappingURL=store.js.map
@@ -0,0 +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 * ([ADR 0028](../../../../../../docs/adr/0028-the-transaction-id-is-the-clients-and-its-event-ships-with-the-write.md)).\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 * The rebuild walk's repair for the one failure ADR 0048 measured: with the\n * broker down long enough, the relay spends `outbox.maxAttempts` on an\n * announcement and quarantines it (ADR 0030's 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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"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The half of this package that needs `@entifix/transactions`: the
3
+ * transactional outbox ADR 0028 writes inside the entity's own Mongo
4
+ * transaction, and the inbox that makes a redelivered command idempotent.
5
+ *
6
+ * Behind a subpath so entity CRUD over Mongo does not arrive with the saga
7
+ * engine attached. Package-level dependencies are not per-subpath, so what
8
+ * actually keeps it optional is the `peerDependenciesMeta.optional` entry in
9
+ * the manifest — importing this module without that peer installed is the
10
+ * error, and it is the adopter's to make deliberately.
11
+ */
12
+ export * from '../inbox/store.js';
13
+ export * from '../outbox/metrics.js';
14
+ export * from '../outbox/relay.js';
15
+ export * from '../outbox/store.js';
16
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/transactions/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The half of this package that needs `@entifix/transactions`: the
3
+ * transactional outbox ADR 0028 writes inside the entity's own Mongo
4
+ * transaction, and the inbox that makes a redelivered command idempotent.
5
+ *
6
+ * Behind a subpath so entity CRUD over Mongo does not arrive with the saga
7
+ * engine attached. Package-level dependencies are not per-subpath, so what
8
+ * actually keeps it optional is the `peerDependenciesMeta.optional` entry in
9
+ * the manifest — importing this module without that peer installed is the
10
+ * error, and it is the adopter's to make deliberately.
11
+ */ export * from '../inbox/store.js';
12
+ export * from '../outbox/metrics.js';
13
+ export * from '../outbox/relay.js';
14
+ export * from '../outbox/store.js';
15
+
16
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/transactions/index.ts"],"sourcesContent":["/**\n * The half of this package that needs `@entifix/transactions`: the\n * transactional outbox ADR 0028 writes inside the entity's own Mongo\n * transaction, and the inbox that makes a redelivered command idempotent.\n *\n * Behind a subpath so entity CRUD over Mongo does not arrive with the saga\n * engine attached. Package-level dependencies are not per-subpath, so what\n * actually keeps it optional is the `peerDependenciesMeta.optional` entry in\n * the manifest — importing this module without that peer installed is the\n * error, and it is the adopter's to make deliberately.\n */\nexport * from '../inbox/store.js';\nexport * from '../outbox/metrics.js';\nexport * from '../outbox/relay.js';\nexport * from '../outbox/store.js';\n"],"names":[],"mappings":"AAAA;;;;;;;;;;CAUC,GACD,cAAc,oBAAoB;AAClC,cAAc,uBAAuB;AACrC,cAAc,qBAAqB;AACnC,cAAc,qBAAqB"}
package/package.json ADDED
@@ -0,0 +1,82 @@
1
+ {
2
+ "name": "@entifix/mongo",
3
+ "version": "0.1.0-0",
4
+ "license": "MIT",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/r10c-technologies/entifix.git",
8
+ "directory": "packages/ts/mongo-client"
9
+ },
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "type": "module",
14
+ "main": "./dist/index.js",
15
+ "module": "./dist/index.js",
16
+ "types": "./dist/index.d.ts",
17
+ "exports": {
18
+ "./package.json": "./package.json",
19
+ ".": {
20
+ "@entifix/source": "./src/index.ts",
21
+ "types": "./dist/index.d.ts",
22
+ "import": "./dist/index.js",
23
+ "default": "./dist/index.js"
24
+ },
25
+ "./transactions": {
26
+ "@entifix/source": "./src/transactions/index.ts",
27
+ "types": "./dist/transactions/index.d.ts",
28
+ "import": "./dist/transactions/index.js",
29
+ "default": "./dist/transactions/index.js"
30
+ }
31
+ },
32
+ "files": [
33
+ "dist",
34
+ "!**/*.tsbuildinfo",
35
+ "LICENSE"
36
+ ],
37
+ "nx": {
38
+ "tags": [
39
+ "layer:entifix",
40
+ "scope:shared",
41
+ "entifix:client",
42
+ "runtime:datastore",
43
+ "tier:2"
44
+ ],
45
+ "sourceRoot": "packages/ts/mongo-client/src",
46
+ "targets": {
47
+ "build": {
48
+ "executor": "@nx/js:swc",
49
+ "outputs": [
50
+ "{options.outputPath}"
51
+ ],
52
+ "options": {
53
+ "outputPath": "packages/ts/mongo-client/dist",
54
+ "main": "packages/ts/mongo-client/src/index.ts",
55
+ "tsConfig": "packages/ts/mongo-client/tsconfig.lib.json",
56
+ "skipTypeCheck": true,
57
+ "stripLeadingPaths": true
58
+ }
59
+ }
60
+ }
61
+ },
62
+ "dependencies": {
63
+ "@swc/helpers": "0.5.23",
64
+ "mongodb": "7.5.0",
65
+ "@entifix/business": "0.1.0-0",
66
+ "@entifix/core": "0.1.0-0"
67
+ },
68
+ "devDependencies": {
69
+ "effect": "3.22.1",
70
+ "@entifix/transactions": "0.1.0-0",
71
+ "@entifix/testing-unit": "0.1.0-0"
72
+ },
73
+ "peerDependencies": {
74
+ "effect": ">=3.22 <4",
75
+ "@entifix/transactions": "0.1.0-0"
76
+ },
77
+ "peerDependenciesMeta": {
78
+ "@entifix/transactions": {
79
+ "optional": true
80
+ }
81
+ }
82
+ }