@ahrowe/mongo 0.1.3 → 0.2.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.
@@ -1,5 +1,4 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import { getOutboxCollection, getRegisteredCollections, getServiceBus } from './db.js';
1
+ import { dispatchEvent, getOutboxCollection, getRegisteredCollections } from './db.js';
3
2
  import { OutboxStatus } from './types/outbox.js';
4
3
  const logger = {
5
4
  info: console.info,
@@ -11,34 +10,33 @@ const defaultBackoff = (attempt) => Math.min(30000, 1000 * 2 ** Math.max(0, atte
11
10
  /** Rows past their retention are removed by the TTL index on `expireAt`. `null` keeps a row forever. */
12
11
  const expiryAfter = (keepMs) => (Number.isFinite(keepMs) ? new Date(Date.now() + keepMs) : null);
13
12
  /**
14
- * Due rows, plus rows whose lease ran out, for collections this process has a service for.
15
- * A row for any other collection waits for a relay that has its listeners, instead of
16
- * being marked done with nobody notified. Exported for the index test, not part of the public barrel.
13
+ * Due rows for collections this process has a service for. A claimed row whose lease ran out is
14
+ * due again, so it needs no condition of its own. A row for any other collection waits for a relay
15
+ * that has its listeners, instead of being marked done with nobody notified. Exported for the
16
+ * index test, not part of the public barrel.
17
17
  */
18
18
  export const claimFilter = (now, collectionNames) => ({
19
19
  collectionName: { $in: collectionNames },
20
- $or: [
21
- { status: OutboxStatus.Pending, nextAttemptAt: { $lte: now } },
22
- { status: OutboxStatus.Processing, leasedUntil: { $lt: now } },
23
- ],
20
+ status: OutboxStatus.Pending,
21
+ nextAttemptAt: { $lte: now },
24
22
  });
25
23
  /**
26
- * Longest-due first. nextAttemptAt (equal to createdOn for a fresh row) is in
27
- * the status index, so both $or branches come back pre-sorted and get merged.
28
- * Sorting by createdOn instead read and sorted every due row in memory, once
29
- * per claimed row.
24
+ * Longest-due first. nextAttemptAt (equal to createdOn for a fresh row) is in the status index,
25
+ * so the rows come back pre-sorted. Sorting by createdOn instead read and sorted every due row in
26
+ * memory, once per claimed row.
30
27
  */
31
28
  export const CLAIM_SORT = { nextAttemptAt: 1 };
32
29
  /**
33
30
  * Drains the transactional outbox and dispatches each event to the in-process
34
31
  * listeners registered on its source collection's bus.
35
32
  *
36
- * Restart-safe and multi-pod-safe: rows are claimed with an atomic
37
- * findOneAndUpdate + a lease, stale leases are reclaimed, and delivery is
38
- * at-least-once (consumers must be idempotent).
33
+ * Restart-safe and multi-pod-safe: a claim is an atomic findOneAndUpdate that
34
+ * pushes the row's nextAttemptAt past a lease, so a row whose holder died is
35
+ * due again once the lease runs out. Delivery is at-least-once (consumers must
36
+ * be idempotent).
39
37
  */
40
38
  export const startOutboxRelay = (options) => {
41
- const { instanceId, pollIntervalMs = 500, batchSize = 20, leaseMs = 30000, maxAttempts = 10, backoffMs = defaultBackoff, keepDoneMs = 7 * 24 * 60 * 60 * 1000, keepFailedMs = Infinity, autoStart = true, } = options;
39
+ const { instanceId, pollIntervalMs = 500, batchSize = 20, leaseMs = 30000, maxAttempts = 10, backoffMs = defaultBackoff, keepDoneMs = 7 * 24 * 60 * 60 * 1000, keepFailedMs = 90 * 24 * 60 * 60 * 1000, autoStart = true, } = options;
42
40
  let running = true;
43
41
  let loopPromise = null;
44
42
  let indexesReady = null;
@@ -46,15 +44,10 @@ export const startOutboxRelay = (options) => {
46
44
  if (!indexesReady) {
47
45
  const col = getOutboxCollection();
48
46
  indexesReady = (async () => {
49
- await col.createIndex({ status: 1, nextAttemptAt: 1, leasedUntil: 1 });
47
+ await col.createIndex({ status: 1, nextAttemptAt: 1 });
50
48
  // Retention is per row: markDone/markFailure set expireAt, so changing keepDoneMs
51
49
  // needs no index change (createIndex can't alter expireAfterSeconds of an existing index).
52
50
  await col.createIndex({ expireAt: 1 }, { expireAfterSeconds: 0 });
53
- // v0.1.x expired done rows 7 days after processedOn, which would override keepDoneMs.
54
- await col.dropIndex('processedOn_1').catch((err) => {
55
- if (err?.codeName !== 'IndexNotFound')
56
- throw err;
57
- });
58
51
  })().catch((err) => {
59
52
  // Don't cache a failure: the next tick tries again instead of failing forever.
60
53
  indexesReady = null;
@@ -67,37 +60,18 @@ export const startOutboxRelay = (options) => {
67
60
  const col = getOutboxCollection();
68
61
  const now = new Date();
69
62
  const claimed = await col.findOneAndUpdate(claimFilter(now, getRegisteredCollections()), {
70
- $set: {
71
- status: OutboxStatus.Processing,
72
- leasedBy: instanceId,
73
- leasedUntil: new Date(now.getTime() + leaseMs),
74
- },
63
+ $set: { leasedBy: instanceId, nextAttemptAt: new Date(now.getTime() + leaseMs) },
75
64
  $inc: { attempts: 1 },
76
65
  }, { sort: CLAIM_SORT, returnDocument: 'after' });
77
66
  return claimed ?? null;
78
67
  };
79
- const dispatch = async (row) => {
80
- const bus = getServiceBus(row.collectionName);
81
- // claimFilter only matches registered collections, so this can't happen. Never mark such a row done.
82
- if (!bus)
83
- throw new Error(`No service registered for collection '${row.collectionName}'`);
84
- const payload = {
85
- prevDoc: row.prevDoc ?? undefined,
86
- doc: row.doc,
87
- meta: row.meta ?? undefined,
88
- context: row.context ?? undefined,
89
- eventId: row._id,
90
- };
91
- const listeners = bus.listeners(row.op);
92
- await Promise.all(listeners.map(async (l) => l(payload)));
93
- };
94
68
  /**
95
69
  * Writes a claimed row's outcome, but only while this claim still holds it. attempts goes up
96
70
  * on every claim, so a holder whose lease ran out (and whose row another instance reclaimed)
97
71
  * matches nothing instead of overwriting the newer claim's outcome.
98
72
  */
99
73
  const settle = async (row, set) => {
100
- const { matchedCount } = await getOutboxCollection().updateOne({ _id: row._id, leasedBy: instanceId, attempts: row.attempts }, { $set: { ...set, leasedBy: null, leasedUntil: null } });
74
+ const { matchedCount } = await getOutboxCollection().updateOne({ _id: row._id, leasedBy: instanceId, attempts: row.attempts }, { $set: { ...set, leasedBy: null } });
101
75
  if (matchedCount === 0) {
102
76
  logger.warn(`Outbox row ${row._id} (${row.collectionName}/${row.op}) was reclaimed after its lease ran out. ` +
103
77
  'Raise leaseMs above your slowest listener.');
@@ -122,8 +96,21 @@ export const startOutboxRelay = (options) => {
122
96
  }
123
97
  };
124
98
  const processRow = async (row) => {
99
+ // markFailure dead-letters at maxAttempts, so only claims whose lease ran out get past it: a
100
+ // listener that crashes the process or outlasts leaseMs. Dispatching again would repeat that.
101
+ if (row.attempts > maxAttempts) {
102
+ await markFailure(row, new Error(`lease of ${leaseMs}ms ran out on earlier attempts; not dispatched again`));
103
+ return;
104
+ }
125
105
  try {
126
- await dispatch(row);
106
+ await dispatchEvent(row.collectionName, {
107
+ op: row.op,
108
+ eventId: row._id,
109
+ doc: row.doc,
110
+ prevDoc: row.prevDoc,
111
+ meta: row.meta,
112
+ context: row.context,
113
+ });
127
114
  await markDone(row);
128
115
  }
129
116
  catch (err) {
@@ -171,7 +158,9 @@ export const startOutboxRelay = (options) => {
171
158
  if (loopPromise)
172
159
  await loopPromise;
173
160
  try {
174
- await getOutboxCollection().updateMany({ status: OutboxStatus.Processing, leasedBy: instanceId }, { $set: { status: OutboxStatus.Pending, leasedBy: null, leasedUntil: null } });
161
+ // A held row's nextAttemptAt is its lease end, still ahead: the condition keeps this off the due backlog.
162
+ const now = new Date();
163
+ await getOutboxCollection().updateMany({ status: OutboxStatus.Pending, nextAttemptAt: { $gt: now }, leasedBy: instanceId }, { $set: { leasedBy: null, nextAttemptAt: now } });
175
164
  }
176
165
  catch (err) {
177
166
  logger.error('Outbox relay failed to release leased rows on stop:', err);
@@ -1 +1 @@
1
- {"version":3,"file":"outboxRelay.js","sourceRoot":"","sources":["../src/outboxRelay.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"outboxRelay.js","sourceRoot":"","sources":["../src/outboxRelay.ts"],"names":[],"mappings":"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"}
@@ -11,12 +11,24 @@ export declare enum OutboxOp {
11
11
  Updated = "updated",
12
12
  Removed = "removed"
13
13
  }
14
+ /**
15
+ * A claimed row stays `pending`: the claim pushes `nextAttemptAt` past its lease, so a row whose
16
+ * holder died is due again through the same query that finds new rows.
17
+ */
14
18
  export declare enum OutboxStatus {
15
19
  Pending = "pending",
16
- Processing = "processing",
17
20
  Done = "done",
18
21
  Failed = "failed"
19
22
  }
23
+ /** What `dispatchEvent` delivers, and listeners receive. `eventId` is the outbox row's `_id`. */
24
+ export interface OutboxEvent {
25
+ op: `${OutboxOp}`;
26
+ eventId: string;
27
+ doc: unknown;
28
+ prevDoc?: unknown;
29
+ meta?: Record<string, unknown> | null;
30
+ context?: unknown;
31
+ }
20
32
  export interface OutboxRow<T = Record<string, unknown>> {
21
33
  _id: string;
22
34
  collectionName: string;
@@ -28,7 +40,6 @@ export interface OutboxRow<T = Record<string, unknown>> {
28
40
  status: OutboxStatus;
29
41
  attempts: number;
30
42
  nextAttemptAt: Date;
31
- leasedUntil?: Date | null;
32
43
  leasedBy?: string | null;
33
44
  lastError?: string | null;
34
45
  createdOn: Date;
@@ -1 +1 @@
1
- {"version":3,"file":"outbox.d.ts","sourceRoot":"","sources":["../../src/types/outbox.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,WAAW,CAAC;AAE1C,oBAAY,QAAQ;IAClB,OAAO,YAAY;IACnB,OAAO,YAAY;IACnB,OAAO,YAAY;CACpB;AAED,oBAAY,YAAY;IACtB,OAAO,YAAY;IACnB,UAAU,eAAe;IACzB,IAAI,SAAS;IACb,MAAM,WAAW;CAClB;AAED,MAAM,WAAW,SAAS,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACpD,GAAG,EAAE,MAAM,CAAC;IACZ,cAAc,EAAE,MAAM,CAAC;IACvB,EAAE,EAAE,QAAQ,CAAC;IACb,GAAG,EAAE,CAAC,CAAC;IACP,OAAO,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACtC,OAAO,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IACzB,MAAM,EAAE,YAAY,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,IAAI,CAAC;IACpB,WAAW,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,SAAS,EAAE,IAAI,CAAC;IAChB,WAAW,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IAC1B,QAAQ,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;CACxB"}
1
+ {"version":3,"file":"outbox.d.ts","sourceRoot":"","sources":["../../src/types/outbox.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,WAAW,CAAC;AAE1C,oBAAY,QAAQ;IAClB,OAAO,YAAY;IACnB,OAAO,YAAY;IACnB,OAAO,YAAY;CACpB;AAED;;;GAGG;AACH,oBAAY,YAAY;IACtB,OAAO,YAAY;IACnB,IAAI,SAAS;IACb,MAAM,WAAW;CAClB;AAED,iGAAiG;AACjG,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,GAAG,QAAQ,EAAE,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,OAAO,CAAC;IACb,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACtC,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,SAAS,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACpD,GAAG,EAAE,MAAM,CAAC;IACZ,cAAc,EAAE,MAAM,CAAC;IACvB,EAAE,EAAE,QAAQ,CAAC;IACb,GAAG,EAAE,CAAC,CAAC;IACP,OAAO,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACtC,OAAO,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IACzB,MAAM,EAAE,YAAY,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,IAAI,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,SAAS,EAAE,IAAI,CAAC;IAChB,WAAW,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IAC1B,QAAQ,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;CACxB"}
@@ -12,10 +12,13 @@ export var OutboxOp;
12
12
  OutboxOp["Updated"] = "updated";
13
13
  OutboxOp["Removed"] = "removed";
14
14
  })(OutboxOp || (OutboxOp = {}));
15
+ /**
16
+ * A claimed row stays `pending`: the claim pushes `nextAttemptAt` past its lease, so a row whose
17
+ * holder died is due again through the same query that finds new rows.
18
+ */
15
19
  export var OutboxStatus;
16
20
  (function (OutboxStatus) {
17
21
  OutboxStatus["Pending"] = "pending";
18
- OutboxStatus["Processing"] = "processing";
19
22
  OutboxStatus["Done"] = "done";
20
23
  OutboxStatus["Failed"] = "failed";
21
24
  })(OutboxStatus || (OutboxStatus = {}));
@@ -1 +1 @@
1
- {"version":3,"file":"outbox.js","sourceRoot":"","sources":["../../src/types/outbox.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,QAAQ,CAAC;AAE1C,MAAM,CAAN,IAAY,QAIX;AAJD,WAAY,QAAQ;IAClB,+BAAmB,CAAA;IACnB,+BAAmB,CAAA;IACnB,+BAAmB,CAAA;AACrB,CAAC,EAJW,QAAQ,KAAR,QAAQ,QAInB;AAED,MAAM,CAAN,IAAY,YAKX;AALD,WAAY,YAAY;IACtB,mCAAmB,CAAA;IACnB,yCAAyB,CAAA;IACzB,6BAAa,CAAA;IACb,iCAAiB,CAAA;AACnB,CAAC,EALW,YAAY,KAAZ,YAAY,QAKvB"}
1
+ {"version":3,"file":"outbox.js","sourceRoot":"","sources":["../../src/types/outbox.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,QAAQ,CAAC;AAE1C,MAAM,CAAN,IAAY,QAIX;AAJD,WAAY,QAAQ;IAClB,+BAAmB,CAAA;IACnB,+BAAmB,CAAA;IACnB,+BAAmB,CAAA;AACrB,CAAC,EAJW,QAAQ,KAAR,QAAQ,QAInB;AAED;;;GAGG;AACH,MAAM,CAAN,IAAY,YAIX;AAJD,WAAY,YAAY;IACtB,mCAAmB,CAAA;IACnB,6BAAa,CAAA;IACb,iCAAiB,CAAA;AACnB,CAAC,EAJW,YAAY,KAAZ,YAAY,QAIvB"}
package/docs/CLAUDE.md CHANGED
@@ -49,13 +49,14 @@ Every `createService<T>(...)` call returns:
49
49
  is rejected**; use `upsertOne` for create-or-update, `insert` for explicit creates. Adds `updatedOn: new Date()` to the `$set`
50
50
  unless `addUpdatedOnField: false` was passed to `createService`. Enqueues an
51
51
  `'updated'` outbox row with `{ prevDoc, doc, meta }` for each document that
52
- actually changed. `updateMany` always uses the reliable per-document path (see
53
- "Events are delivered via a transactional outbox" below).
52
+ actually changed. For `updateMany`, see "How updateMany/removeMany work" below.
54
53
  - `removeOne(query, options?)` and `removeMany(query, options?)` — `removeMany`
55
54
  returns removed docs under `results` unless `{ returnRemoved: false }`. Enqueues
56
55
  a `'removed'` outbox row with `{ doc, meta }` for each removed document (no row
57
- if nothing matched). `removeMany` always uses the reliable per-document path,
58
- same as `updateMany`.
56
+ if nothing matched). For `removeMany`, see "How updateMany/removeMany work" below.
57
+ - Write methods reject `projection` and `includeResultMetadata` (types omit them,
58
+ and they throw at runtime): validation and outbox events need the full
59
+ document. Read the fields you need with `find`/`findOne` afterwards.
59
60
  - `createIndex(fields, options?)` — `fields` is `IndexSpec<T>`: any top-level key of
60
61
  `T` or a dotted path into it (`'users._id'`), following arrays into their element
61
62
  type, mapped to `IndexDirection` (`1 | -1 | 'text' | 'hashed' | '2dsphere' | '2d'`).
@@ -65,12 +66,13 @@ Every `createService<T>(...)` call returns:
65
66
  having to enumerate that far.
66
67
  - `count`, `exists`, `aggregate`, `distinct`, `dropIndex`, `bulkWrite`
67
68
  (requires `{ allowBulkwrite: true }` — guards against accidental unvalidated writes).
68
- `bulkWrite` does not enqueue outbox rows. `dropIndex(indexName, options?)` drops
69
+ `bulkWrite` writes no outbox rows, so it throws on a service that emits them.
70
+ `dropIndex(indexName, options?)` drops
69
71
  a single index by name (not by key spec) and does not enqueue outbox rows.
70
- - `on(event, cb)` / `onPropertiesUpdated(properties, cb)` — subscribe to the
71
- service's own `eventBus` (a plain `node:events` `EventEmitter`). Listeners
72
- registered here are dispatched to by `startOutboxRelay`, not by the write call
73
- itself.
72
+ - `on(event, cb)` / `onPropertiesUpdated(properties, cb)`: register a listener
73
+ for `'created' | 'updated' | 'removed'` on this collection. Returns a function
74
+ that removes it. Listeners are called by `startOutboxRelay` (or
75
+ `dispatchEvent`), not by the write call itself.
74
76
  - `withSession(cb)` / `startSession(options?)` — exported from the module (not
75
77
  per-service) for manual multi-document transactions across several services.
76
78
  - `disconnect()` — exported from the module; closes both MongoDB clients.
@@ -94,8 +96,9 @@ The contract is validator-agnostic — plug in Zod (`schema.safeParse(entity)`),
94
96
  directly — writing to the data collection can't be suppressed per-call, so for
95
97
  each document actually affected, one row is written to an internal `outbox`
96
98
  collection **in the same transaction** as the data write. A separate process,
97
- `startOutboxRelay({ instanceId, ... })`, claims pending rows (atomic leased
98
- `findOneAndUpdate`, safe across multiple pods) and dispatches each to whatever's
99
+ `startOutboxRelay({ instanceId, ... })`, claims pending rows (an atomic
100
+ `findOneAndUpdate` that pushes `nextAttemptAt` past a lease, safe across
101
+ multiple pods) and dispatches each to whatever's
99
102
  registered via `.on(event, cb)` on that collection's service, retrying with
100
103
  backoff and dead-lettering after `maxAttempts`. This means delivery is durable —
101
104
  it survives a crash between the write committing and a listener running — and
@@ -108,10 +111,10 @@ const relay = startOutboxRelay({
108
111
  instanceId: process.env.HOSTNAME ?? 'local', // required — identifies this lease holder
109
112
  pollIntervalMs: 500, // idle poll interval when the outbox is empty
110
113
  batchSize: 20, // rows claimed+dispatched per tick
111
- leaseMs: 30_000, // how long a claim is held before another instance may reclaim it
114
+ leaseMs: 30_000, // how long a claimed row stays hidden from other claims
112
115
  maxAttempts: 10, // attempts before a row is dead-lettered to 'failed'
113
116
  keepDoneMs: 7 * 24 * 60 * 60 * 1000, // how long 'done' rows are kept; Infinity = forever
114
- keepFailedMs: Infinity, // how long 'failed' rows are kept; default forever, for inspection/replay
117
+ keepFailedMs: 90 * 24 * 60 * 60 * 1000, // how long 'failed' rows are kept for inspection/replay; Infinity = forever
115
118
  // backoffMs: (attempt) => ms, // override the default capped-exponential backoff
116
119
  // autoStart: false, // don't start the poll loop — drive tick()/drain() manually (useful in tests)
117
120
  });
@@ -145,6 +148,16 @@ Set `{ emitOutboxEvents: false }` on `createService` for infra collections with
145
148
  no listeners (sequences, the outbox itself, etc.) — writes then take a leaner
146
149
  fast path with no pre-reads and no transaction-wrapped outbox insert.
147
150
 
151
+ Set `{ toOutboxDoc: (doc) => ... }` on `createService` to decide what a
152
+ collection's outbox rows hold of a document, `doc` and `prevDoc` alike. Rows
153
+ reach every listener, and whatever those store (an audit log, a broker), so a
154
+ secret in a row leaks there. `({ secret, ...hook }) => hook` leaves it out (a
155
+ misspelled field is a type error), and an update that changes only that field
156
+ records no row. To have the change show without the value, return a hash of it
157
+ instead. The function must be pure and must not modify the document it gets. It
158
+ also gets stored documents that were never validated against `T` (older ones,
159
+ `skipValidation` writes), so handle a missing field: a throw fails the write.
160
+
148
161
  If a listener needs request/actor context (e.g. an audit log), register
149
162
  `setOutboxContextProvider(fn)` once at startup. It's called **synchronously at
150
163
  write time**, in the same transaction as the data write, and the captured value
@@ -169,11 +182,10 @@ await getOutboxCollection().updateOne(
169
182
  );
170
183
  ```
171
184
 
172
- `getServiceBus(collectionName)` and `getRegisteredCollections()` are the same
173
- internals the relay itself uses to route a row to its listeners — reach for
174
- them only if you're building custom tooling around the outbox (a dashboard, a
175
- manual replay script); normal listener registration is just `.on(...)` on the
176
- service.
185
+ `getRegisteredCollections()` lists the collections with a service in this
186
+ process (the ones the relay claims rows for). Reach for it only if you're
187
+ building custom tooling around the outbox; normal listener registration is just
188
+ `.on(...)` on the service.
177
189
 
178
190
  ### Multiple consumers
179
191
 
@@ -190,7 +202,10 @@ move is to bridge: run one relay whose only job is to republish each row to a
190
202
  message broker (Kafka, SNS, SQS, etc.), and let downstream services subscribe
191
203
  there. The outbox guarantees the write reaches the broker exactly once; the
192
204
  broker handles fan-out from there. That bridging code lives in your app — this
193
- package has no broker dependency by design.
205
+ package has no broker dependency by design. On the consuming side, hand each
206
+ received event to `dispatchEvent(collectionName, event)`: it calls the
207
+ listeners registered via `.on(...)` exactly like the relay does (awaits all of
208
+ them, rejects with the first failure so your consumer can retry).
194
209
 
195
210
  Until a second independent consumer appears, don't add the broker. Bundle the
196
211
  relay and all listeners in the same process.
@@ -198,41 +213,35 @@ relay and all listeners in the same process.
198
213
  ### Outbox row retention
199
214
 
200
215
  When a row is marked `done` or `failed`, the relay sets its `expireAt` from
201
- `keepDoneMs` (default **7 days**) or `keepFailedMs` (default: never), and a TTL
202
- index deletes it after that. MongoDB's TTL task runs every 60 seconds, so
203
- deletion isn't instant. Retention is stored per row, so changing it needs no
204
- index migration; it applies to rows settled from then on.
205
-
206
- By default `failed` rows are **never** auto-deleted. They accumulate until
207
- manually requeued or removed, so a steady stream of permanently-failing events
208
- will grow the outbox collection unbounded until someone looks at it. Set
209
- `keepFailedMs` to cap that. When requeuing a failed row by hand, clear
216
+ `keepDoneMs` (default **7 days**) or `keepFailedMs` (default **90 days**), and a
217
+ TTL index deletes it after that; `Infinity` keeps rows forever. MongoDB's TTL
218
+ task runs every 60 seconds, so deletion isn't instant. Retention is stored per
219
+ row, so changing it needs no index migration; it applies to rows settled from
220
+ then on. A row no relay processes stays until one does. When requeuing a failed row by hand, clear
210
221
  `expireAt` (as in the snippet above), or the TTL index may delete it while it
211
222
  waits.
212
223
 
213
- Upgrading from v0.1.2 or earlier: the relay drops the old fixed 7-day TTL
214
- index on `processedOn` on startup.
215
-
216
- ## updateMany/removeMany always use the reliable per-document path
224
+ ## How updateMany/removeMany work
217
225
 
218
- Unlike a single native bulk `updateMany`/`deleteMany` call, both methods process
219
- every matched document one at a time, sequentially (MongoDB doesn't allow
220
- concurrent operations on one session), inside a single transaction so the whole
221
- batch still rolls back together (data **and** outbox rows) on any failure. Every
222
- document is individually validated, and each outbox row's `{ prevDoc, doc }` pair
223
- comes from its own atomic `findOneAndUpdate`/`findOneAndDelete` — not a diff
224
- between two independent before/after scans, which could otherwise miss or
225
- misattribute documents if the matched set changed mid-operation.
226
+ Both collect the matched `_id`s, then work through them in chunks of 500 inside a
227
+ single transaction: per chunk, one read of the current documents, one native
228
+ `updateMany`/`deleteMany` by `_id`, and (for `updateMany`) one read of the
229
+ results, plus one outbox insert. A failure in any chunk rolls back every chunk,
230
+ data **and** outbox rows. Every updated document is validated, and each outbox
231
+ row's `{ prevDoc, doc }` pair is exact: once the transaction has read a document,
232
+ any concurrent write to it makes the transaction fail with a transient write
233
+ conflict, and the whole call is retried.
226
234
 
227
- This path has **no size cap by default** — pass `{ maxBatchSize }` (per-call, or
228
- as a `createService` option) to enforce one and fail fast; a single transaction
229
- processing a very large matched set risks hitting MongoDB's transaction lifetime
230
- limit (default 60s). Past that limit it throws — use `bulkWrite` for larger jobs
231
- (no transaction/validation/event guarantees, but no cap either).
235
+ With `emitOutboxEvents: false`, `updateMany` still validates every document
236
+ (skipping the `prevDoc` reads and outbox rows); only without a validator, or
237
+ with `{ skipValidation: true }`, does it become one native `updateMany`.
232
238
 
233
- Internally this is powered by `src/batchify.ts` (sequential `concurrency: 1`,
234
- `throwOnError: true` so a per-document failure aborts the whole transaction) — an
235
- internal implementation detail, not exported from the package.
239
+ There is **no size cap by default**. Pass `{ maxBatchSize }` (per-call, or as a
240
+ `createService` option) to fail fast: a single transaction processing a very
241
+ large matched set risks MongoDB's transaction lifetime limit (default 60s).
242
+ Past the cap it throws before writing anything; it never processes a partial
243
+ batch, so it can't be used to loop. For more than fits one transaction, split
244
+ the work into several calls (see the README for a rerunnable migration loop).
236
245
 
237
246
  ## Transactions
238
247
 
@@ -265,42 +274,23 @@ Exceptions: `bulkWrite`, and `removeOne`/`removeMany` on a service with
265
274
  somewhere in the deployment.
266
275
  - **Two MongoDB clients per `connect()` call** (primary + secondary-preferred reader).
267
276
  `preferReadFromSecondary: true` only affects `find`/`findCursor`/`count`/`aggregate`.
268
- - **`bulkWrite` requires `{ allowBulkwrite: true }`** — there is no other safety net
269
- (no schema validation runs on bulk writes).
270
- - **`updateOne`'s `prevDoc` can be stale** — and so can `updateMany`/`removeMany`'s
271
- reliable path, since it reuses the exact same single-document logic
272
- (`performSingleUpdate` in `db.ts`). To populate `{ prevDoc, doc }` on the outbox
273
- row, the document is read with a separate `findOne` *before* running the atomic
274
- `findOneAndUpdate` — a concurrent write landing in that gap means `prevDoc` may
275
- not reflect the actual immediately-prior state. The atomic update itself is
276
- unaffected (Mongo still applies it correctly); only the `prevDoc` value in the
277
- outbox row can be stale. Treat `prevDoc` as best-effort, not authoritative, for
278
- anything safety-critical.
279
- - **`emitOutboxEvents: false` (no-outbox) writes only validate one sampled
280
- document on `updateMany`, not every updated document.** For `$set`-with-literal-values
281
- updates this is representative (every matched doc lands on the same value for
282
- the touched fields), but for operators whose result depends on a document's
283
- prior value (`$inc`, `$mul`, `$min`, `$max`, `$push`/`$addToSet`, `$rename`,
284
- positional array filters), the same update can be valid for the sampled
285
- document and invalid for another — undetected on that fast path. The default
286
- reliable path (`emitOutboxEvents: true`, the default) validates every document
287
- individually and doesn't have this gap.
277
+ - **`bulkWrite` requires `{ allowBulkwrite: true }`** and a service with
278
+ `emitOutboxEvents: false` — no schema validation runs on bulk writes, and no
279
+ outbox rows are written.
288
280
  - **Nothing dispatches outbox events unless a relay is running.** Outbox rows
289
281
  accumulate (durably, harmlessly) until some process calls `startOutboxRelay`.
290
282
  Make sure at least one instance in your deployment runs it, or `.on(...)`
291
283
  listeners will never fire.
292
- - **`createService` called twice for the same collection name reuses the first
293
- call's `eventBus` (and logs a warning).** The registry mapping collection name
294
- → `eventBus` is process-wide, so without this, a second call's fresh
295
- `EventEmitter` would silently replace the first's, orphaning any listeners
296
- already registered on it. Reuse means `.on(...)`/`onPropertiesUpdated(...)`
284
+ - **`createService` called twice for the same collection name shares the first
285
+ call's listeners (and logs a warning).** Listeners are registered per
286
+ collection name, process-wide, so `.on(...)`/`onPropertiesUpdated(...)`
297
287
  registered on *either* instance still receive dispatched events — but other
298
288
  per-instance config is **not** shared, and a mismatch there is easy to miss
299
- since the shared bus makes everything else look consistent:
289
+ since the shared listeners make everything else look consistent:
300
290
  - **`emitOutboxEvents`** — if one instance has it `false`, writes through
301
- that instance never enqueue an outbox row, so listeners on the shared bus
302
- silently never fire for those writes specifically (not a bus problem —
303
- no event is generated at all). This can be an intentional choice (e.g. a
291
+ that instance never enqueue an outbox row, so the shared listeners
292
+ silently never fire for those writes specifically (no event is generated
293
+ at all). This can be an intentional choice (e.g. a
304
294
  backfill/migration instance skipping outbox writes on purpose), so it's
305
295
  not something `createService` should reject — just be deliberate about it.
306
296
  - **`validate`** — two instances can enforce different schemas on the same
@@ -310,10 +300,24 @@ Exceptions: `bulkWrite`, and `removeOne`/`removeMany` on a service with
310
300
  - **`addCreatedOnField`/`addUpdatedOnField`** — documents written through
311
301
  one instance may end up with `createdOn`/`updatedOn` and documents
312
302
  written through the other may not, for the same collection.
303
+ - **`toOutboxDoc`** — a field one instance keeps out of the outbox reaches
304
+ the shared listeners through the other. Give every instance the same one.
313
305
  Prefer creating each service once (e.g. in a shared module) and importing
314
306
  that instance everywhere it's used, unless one of the above divergences is
315
- actually what you want. Because the bus is shared, `.eventBus
316
- .removeAllListeners()` (or `.off(...)`/`.removeListener(...)`) called on
317
- *either* instance also removes listeners registered via the *other*
318
- instance — teardown code needs to account for that instead of assuming it
319
- only touches its own listeners.
307
+ actually what you want.
308
+
309
+ ## Upgrading from 0.1
310
+
311
+ `CHANGELOG.md` (shipped with the package) lists what changed in 0.2.0. One step
312
+ has no place there: once no 0.1 relay runs any more, run this once per database.
313
+ It requeues rows a 0.1 relay left in status `processing` (a relay of this version
314
+ never claims them), and drops the indexes 0.1 created:
315
+
316
+ ```js
317
+ db.outbox.updateMany({ status: 'processing' }, [
318
+ { $set: { status: 'pending', leasedBy: null, nextAttemptAt: { $ifNull: ['$leasedUntil', '$$NOW'] } } },
319
+ { $unset: 'leasedUntil' },
320
+ ]);
321
+ db.outbox.dropIndex('status_1_nextAttemptAt_1_leasedUntil_1');
322
+ db.outbox.dropIndex('processedOn_1'); // only if it still exists
323
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ahrowe/mongo",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Typed MongoDB CRUD service factory with built-in transactions, event hooks, and optional document validation.",
@@ -23,7 +23,8 @@
23
23
  "types": "./dist/index.d.ts",
24
24
  "files": [
25
25
  "dist",
26
- "docs/CLAUDE.md"
26
+ "docs/CLAUDE.md",
27
+ "CHANGELOG.md"
27
28
  ],
28
29
  "sideEffects": false,
29
30
  "exports": {
@@ -40,6 +41,7 @@
40
41
  "mongodb": "^7.0.0"
41
42
  },
42
43
  "devDependencies": {
44
+ "@changesets/cli": "^3.0.3",
43
45
  "@eslint/js": "^10.0.1",
44
46
  "@faker-js/faker": "^10.1.0",
45
47
  "@types/lodash": "^4.17.21",
@@ -56,6 +58,7 @@
56
58
  "build": "tsc",
57
59
  "lint": "eslint .",
58
60
  "test": "vitest",
61
+ "changeset": "changeset",
59
62
  "release": "bash scripts/release.sh"
60
63
  }
61
64
  }
@@ -1,36 +0,0 @@
1
- interface BatchifyOptions<T> {
2
- /**
3
- * Returns the next batch of data, or an array containing all data (sliced into
4
- * batches automatically). When a function: receives the requested batch size
5
- * and the number of items already processed.
6
- */
7
- dataDelivery?: T[] | ((requestedBatchSize: number, alreadyProcessed: number) => T[] | Promise<T[]>);
8
- /** Number of items fetched per `dataDelivery` call. Default 1000. */
9
- batchSize?: number;
10
- /**
11
- * Number of items from the current batch processed concurrently. Defaults to
12
- * `batchSize` (the whole batch at once). Set to `1` for strictly sequential
13
- * processing — required when items share a MongoDB session/transaction, which
14
- * doesn't allow concurrent operations.
15
- */
16
- concurrency?: number;
17
- /** Function to run for each item. Receives the item and its global index. */
18
- functionToRun?: (data: T, index: number) => void | Promise<void>;
19
- /** Invoked after each batch completes, with the total number of items processed so far. */
20
- onProgress?: (processedAmount: number) => void | Promise<void>;
21
- /** Called when processing an item fails. Receives the failed item and the error. */
22
- onError?: (entry: T, err: unknown) => void | Promise<void>;
23
- /** If true, stop fetching further batches once any error has occurred. */
24
- returnOnError?: boolean;
25
- /**
26
- * If true, rethrow the first error out of `batchify()` itself once the batch
27
- * it occurred in has settled, instead of only invoking `onError`. Use this when
28
- * the caller needs the failure to actually propagate (e.g. to abort a transaction).
29
- */
30
- throwOnError?: boolean;
31
- /** Milliseconds to wait after completing each batch before starting the next. */
32
- pauseAfterBatch?: number;
33
- }
34
- export declare function batchify<T>({ dataDelivery, batchSize, concurrency, functionToRun, onProgress, onError, returnOnError, throwOnError, pauseAfterBatch, }?: BatchifyOptions<T>): Promise<void>;
35
- export {};
36
- //# sourceMappingURL=batchify.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"batchify.d.ts","sourceRoot":"","sources":["../src/batchify.ts"],"names":[],"mappings":"AAAA,UAAU,eAAe,CAAC,CAAC;IACzB;;;;OAIG;IACH,YAAY,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC,kBAAkB,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,KAAK,CAAC,EAAE,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACpG,qEAAqE;IACrE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6EAA6E;IAC7E,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjE,2FAA2F;IAC3F,UAAU,CAAC,EAAE,CAAC,eAAe,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/D,oFAAoF;IACpF,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,GAAG,EAAE,OAAO,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3D,0EAA0E;IAC1E,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,iFAAiF;IACjF,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,wBAAsB,QAAQ,CAAC,CAAC,EAAE,EAChC,YAAuB,EACvB,SAAgB,EAChB,WAAW,EACX,aAA+B,EAC/B,UAA4B,EAC5B,OAAyB,EACzB,aAAqB,EACrB,YAAoB,EACpB,eAAmB,GACpB,GAAE,eAAe,CAAC,CAAC,CAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAuDzC"}