@ahrowe/mongo 0.1.3 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +76 -0
- package/README.md +50 -23
- package/dist/db.d.ts +31 -13
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +163 -130
- package/dist/db.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/outboxRelay.d.ts +18 -26
- package/dist/outboxRelay.d.ts.map +1 -1
- package/dist/outboxRelay.js +78 -56
- package/dist/outboxRelay.js.map +1 -1
- package/dist/types/outbox.d.ts +13 -2
- package/dist/types/outbox.d.ts.map +1 -1
- package/dist/types/outbox.js +4 -1
- package/dist/types/outbox.js.map +1 -1
- package/docs/CLAUDE.md +112 -86
- package/package.json +5 -2
- package/dist/batchify.d.ts +0 -36
- package/dist/batchify.d.ts.map +0 -1
- package/dist/batchify.js +0 -57
- package/dist/batchify.js.map +0 -1
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
|
|
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
|
|
58
|
-
|
|
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`
|
|
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)
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
98
|
-
`findOneAndUpdate
|
|
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
|
|
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:
|
|
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
|
});
|
|
@@ -142,8 +145,19 @@ listeners on that event (e.g. a flag a specific handler checks to skip its own
|
|
|
142
145
|
side effect, without other listeners missing the event).
|
|
143
146
|
|
|
144
147
|
Set `{ emitOutboxEvents: false }` on `createService` for infra collections with
|
|
145
|
-
no listeners (sequences, the outbox itself, etc.)
|
|
146
|
-
|
|
148
|
+
no listeners (sequences, counters, the outbox itself, etc.) and for one-off
|
|
149
|
+
scripts such as migrations — writes then take a leaner fast path with no
|
|
150
|
+
pre-reads and no transaction-wrapped outbox insert (see "Transactions").
|
|
151
|
+
|
|
152
|
+
Set `{ toOutboxDoc: (doc) => ... }` on `createService` to decide what a
|
|
153
|
+
collection's outbox rows hold of a document, `doc` and `prevDoc` alike. Rows
|
|
154
|
+
reach every listener, and whatever those store (an audit log, a broker), so a
|
|
155
|
+
secret in a row leaks there. `({ secret, ...hook }) => hook` leaves it out (a
|
|
156
|
+
misspelled field is a type error), and an update that changes only that field
|
|
157
|
+
records no row. To have the change show without the value, return a hash of it
|
|
158
|
+
instead. The function must be pure and must not modify the document it gets. It
|
|
159
|
+
also gets stored documents that were never validated against `T` (older ones,
|
|
160
|
+
`skipValidation` writes), so handle a missing field: a throw fails the write.
|
|
147
161
|
|
|
148
162
|
If a listener needs request/actor context (e.g. an audit log), register
|
|
149
163
|
`setOutboxContextProvider(fn)` once at startup. It's called **synchronously at
|
|
@@ -169,11 +183,10 @@ await getOutboxCollection().updateOne(
|
|
|
169
183
|
);
|
|
170
184
|
```
|
|
171
185
|
|
|
172
|
-
`
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
service.
|
|
186
|
+
`getRegisteredCollections()` lists the collections with a service in this
|
|
187
|
+
process (the ones the relay claims rows for). Reach for it only if you're
|
|
188
|
+
building custom tooling around the outbox; normal listener registration is just
|
|
189
|
+
`.on(...)` on the service.
|
|
177
190
|
|
|
178
191
|
### Multiple consumers
|
|
179
192
|
|
|
@@ -190,7 +203,10 @@ move is to bridge: run one relay whose only job is to republish each row to a
|
|
|
190
203
|
message broker (Kafka, SNS, SQS, etc.), and let downstream services subscribe
|
|
191
204
|
there. The outbox guarantees the write reaches the broker exactly once; the
|
|
192
205
|
broker handles fan-out from there. That bridging code lives in your app — this
|
|
193
|
-
package has no broker dependency by design.
|
|
206
|
+
package has no broker dependency by design. On the consuming side, hand each
|
|
207
|
+
received event to `dispatchEvent(collectionName, event)`: it calls the
|
|
208
|
+
listeners registered via `.on(...)` exactly like the relay does (awaits all of
|
|
209
|
+
them, rejects with the first failure so your consumer can retry).
|
|
194
210
|
|
|
195
211
|
Until a second independent consumer appears, don't add the broker. Bundle the
|
|
196
212
|
relay and all listeners in the same process.
|
|
@@ -198,41 +214,35 @@ relay and all listeners in the same process.
|
|
|
198
214
|
### Outbox row retention
|
|
199
215
|
|
|
200
216
|
When a row is marked `done` or `failed`, the relay sets its `expireAt` from
|
|
201
|
-
`keepDoneMs` (default **7 days**) or `keepFailedMs` (default
|
|
202
|
-
index deletes it after that. MongoDB's TTL
|
|
203
|
-
deletion isn't instant. Retention is stored per
|
|
204
|
-
index migration; it applies to rows settled from
|
|
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
|
|
217
|
+
`keepDoneMs` (default **7 days**) or `keepFailedMs` (default **90 days**), and a
|
|
218
|
+
TTL index deletes it after that; `Infinity` keeps rows forever. MongoDB's TTL
|
|
219
|
+
task runs every 60 seconds, so deletion isn't instant. Retention is stored per
|
|
220
|
+
row, so changing it needs no index migration; it applies to rows settled from
|
|
221
|
+
then on. A row no relay processes stays until one does. When requeuing a failed row by hand, clear
|
|
210
222
|
`expireAt` (as in the snippet above), or the TTL index may delete it while it
|
|
211
223
|
waits.
|
|
212
224
|
|
|
213
|
-
|
|
214
|
-
index on `processedOn` on startup.
|
|
225
|
+
## How updateMany/removeMany work
|
|
215
226
|
|
|
216
|
-
|
|
227
|
+
Both collect the matched `_id`s, then work through them in chunks of 500 inside a
|
|
228
|
+
single transaction: per chunk, one read of the current documents, one native
|
|
229
|
+
`updateMany`/`deleteMany` by `_id`, and (for `updateMany`) one read of the
|
|
230
|
+
results, plus one outbox insert. A failure in any chunk rolls back every chunk,
|
|
231
|
+
data **and** outbox rows. Every updated document is validated, and each outbox
|
|
232
|
+
row's `{ prevDoc, doc }` pair is exact: once the transaction has read a document,
|
|
233
|
+
any concurrent write to it makes the transaction fail with a transient write
|
|
234
|
+
conflict, and the whole call is retried.
|
|
217
235
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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.
|
|
236
|
+
With `emitOutboxEvents: false`, `updateMany` still validates every document
|
|
237
|
+
(skipping the `prevDoc` reads and outbox rows); only without a validator, or
|
|
238
|
+
with `{ skipValidation: true }`, does it become one native `updateMany`.
|
|
226
239
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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.
|
|
240
|
+
There is **no size cap by default**. Pass `{ maxBatchSize }` (per-call, or as a
|
|
241
|
+
`createService` option) to fail fast: a single transaction processing a very
|
|
242
|
+
large matched set risks MongoDB's transaction lifetime limit (default 60s).
|
|
243
|
+
Past the cap it throws before writing anything; it never processes a partial
|
|
244
|
+
batch, so it can't be used to loop. For more than fits one transaction, split
|
|
245
|
+
the work into several calls (see the README for a rerunnable migration loop).
|
|
236
246
|
|
|
237
247
|
## Transactions
|
|
238
248
|
|
|
@@ -244,8 +254,13 @@ atomic transaction, pass a session **inside** `session.withTransaction(...)`, as
|
|
|
244
254
|
the README. A bare session from `startSession()`/`withSession()` gives each call
|
|
245
255
|
its own transaction, not a shared one.
|
|
246
256
|
|
|
247
|
-
Exceptions: `bulkWrite`, and
|
|
248
|
-
|
|
257
|
+
Exceptions: `bulkWrite`, and on a service with `emitOutboxEvents: false` the
|
|
258
|
+
writes a failure couldn't leave half done: `removeOne`/`removeMany`, and
|
|
259
|
+
`upsertOne`/`updateOne`/a single `insert` that validate nothing after the write
|
|
260
|
+
(no validator, or `skipValidation`). One `findOneAndUpdate` or `insertOne` is
|
|
261
|
+
atomic on its own, and a transaction of its own would cost a commit and, on a
|
|
262
|
+
contended document such as a counter, WriteConflict retries. They still join the
|
|
263
|
+
caller's transaction.
|
|
249
264
|
|
|
250
265
|
## Gotchas (read before integrating)
|
|
251
266
|
|
|
@@ -265,42 +280,29 @@ Exceptions: `bulkWrite`, and `removeOne`/`removeMany` on a service with
|
|
|
265
280
|
somewhere in the deployment.
|
|
266
281
|
- **Two MongoDB clients per `connect()` call** (primary + secondary-preferred reader).
|
|
267
282
|
`preferReadFromSecondary: true` only affects `find`/`findCursor`/`count`/`aggregate`.
|
|
268
|
-
- **`bulkWrite` requires `{ allowBulkwrite: true }`**
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
row
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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.
|
|
283
|
+
- **`bulkWrite` requires `{ allowBulkwrite: true }`** and a service with
|
|
284
|
+
`emitOutboxEvents: false` — no schema validation runs on bulk writes, and no
|
|
285
|
+
outbox rows are written.
|
|
286
|
+
- **Rows wait for a relay with a service for their collection.** A relay only
|
|
287
|
+
claims rows of collections with a service in its own process, and a pending
|
|
288
|
+
row never expires. A script that writes with events on, to a collection no
|
|
289
|
+
relay process has a service for, leaves rows behind forever; a relay warns
|
|
290
|
+
about such rows on start once they are an hour overdue. Write from scripts
|
|
291
|
+
through a service with `emitOutboxEvents: false`.
|
|
288
292
|
- **Nothing dispatches outbox events unless a relay is running.** Outbox rows
|
|
289
293
|
accumulate (durably, harmlessly) until some process calls `startOutboxRelay`.
|
|
290
294
|
Make sure at least one instance in your deployment runs it, or `.on(...)`
|
|
291
295
|
listeners will never fire.
|
|
292
|
-
- **`createService` called twice for the same collection name
|
|
293
|
-
call's
|
|
294
|
-
|
|
295
|
-
`EventEmitter` would silently replace the first's, orphaning any listeners
|
|
296
|
-
already registered on it. Reuse means `.on(...)`/`onPropertiesUpdated(...)`
|
|
296
|
+
- **`createService` called twice for the same collection name shares the first
|
|
297
|
+
call's listeners (and logs a warning).** Listeners are registered per
|
|
298
|
+
collection name, process-wide, so `.on(...)`/`onPropertiesUpdated(...)`
|
|
297
299
|
registered on *either* instance still receive dispatched events — but other
|
|
298
300
|
per-instance config is **not** shared, and a mismatch there is easy to miss
|
|
299
|
-
since the shared
|
|
301
|
+
since the shared listeners make everything else look consistent:
|
|
300
302
|
- **`emitOutboxEvents`** — if one instance has it `false`, writes through
|
|
301
|
-
that instance never enqueue an outbox row, so
|
|
302
|
-
silently never fire for those writes specifically (
|
|
303
|
-
|
|
303
|
+
that instance never enqueue an outbox row, so the shared listeners
|
|
304
|
+
silently never fire for those writes specifically (no event is generated
|
|
305
|
+
at all). This can be an intentional choice (e.g. a
|
|
304
306
|
backfill/migration instance skipping outbox writes on purpose), so it's
|
|
305
307
|
not something `createService` should reject — just be deliberate about it.
|
|
306
308
|
- **`validate`** — two instances can enforce different schemas on the same
|
|
@@ -310,10 +312,34 @@ Exceptions: `bulkWrite`, and `removeOne`/`removeMany` on a service with
|
|
|
310
312
|
- **`addCreatedOnField`/`addUpdatedOnField`** — documents written through
|
|
311
313
|
one instance may end up with `createdOn`/`updatedOn` and documents
|
|
312
314
|
written through the other may not, for the same collection.
|
|
315
|
+
- **`toOutboxDoc`** — a field one instance keeps out of the outbox reaches
|
|
316
|
+
the shared listeners through the other. Give every instance the same one.
|
|
313
317
|
Prefer creating each service once (e.g. in a shared module) and importing
|
|
314
318
|
that instance everywhere it's used, unless one of the above divergences is
|
|
315
|
-
actually what you want.
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
319
|
+
actually what you want.
|
|
320
|
+
|
|
321
|
+
## Upgrading
|
|
322
|
+
|
|
323
|
+
`CHANGELOG.md` (shipped with the package) lists what changed in each version.
|
|
324
|
+
The steps that have no place there run once per database, after no relay of the
|
|
325
|
+
older version runs any more. Each says which versions need it, and `dropIndex`
|
|
326
|
+
throws for an index that isn't there, so skip the ones you don't have.
|
|
327
|
+
|
|
328
|
+
```js
|
|
329
|
+
// From 0.1: rows a 0.1 relay left in status 'processing', which no newer relay claims.
|
|
330
|
+
db.outbox.updateMany({ status: 'processing' }, [
|
|
331
|
+
{ $set: { status: 'pending', leasedBy: null, nextAttemptAt: { $ifNull: ['$leasedUntil', '$$NOW'] } } },
|
|
332
|
+
{ $unset: 'leasedUntil' },
|
|
333
|
+
]);
|
|
334
|
+
// From 0.1.2 or older: done rows only the processedOn index expired. 7 days is keepDoneMs's
|
|
335
|
+
// default; use yours.
|
|
336
|
+
db.outbox.updateMany({ status: 'done', expireAt: { $exists: false } }, [
|
|
337
|
+
{ $set: { expireAt: { $add: ['$processedOn', 7 * 24 * 60 * 60 * 1000] } } },
|
|
338
|
+
]);
|
|
339
|
+
db.outbox.dropIndex('processedOn_1'); // 0.1.2 or older
|
|
340
|
+
db.outbox.dropIndex('status_1_nextAttemptAt_1_leasedUntil_1'); // 0.1
|
|
341
|
+
db.outbox.dropIndex('status_1_nextAttemptAt_1'); // 0.2.0
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
Rows a 0.1 relay dead-lettered keep no expiry, as they did in 0.1: delete them
|
|
345
|
+
once inspected.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ahrowe/mongo",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
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
|
}
|
package/dist/batchify.d.ts
DELETED
|
@@ -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
|
package/dist/batchify.d.ts.map
DELETED
|
@@ -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"}
|
package/dist/batchify.js
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
export async function batchify({ dataDelivery = () => [], batchSize = 1000, concurrency, functionToRun = () => undefined, onProgress = () => undefined, onError = () => undefined, returnOnError = false, throwOnError = false, pauseAfterBatch = 0, } = {}) {
|
|
2
|
-
const effectiveConcurrency = concurrency ?? batchSize;
|
|
3
|
-
let iteration = 0;
|
|
4
|
-
let hasError = false;
|
|
5
|
-
let firstError;
|
|
6
|
-
let currentBatch = [];
|
|
7
|
-
const getNextBatch = async () => {
|
|
8
|
-
if (typeof dataDelivery === 'function') {
|
|
9
|
-
currentBatch = await dataDelivery(batchSize, iteration * batchSize);
|
|
10
|
-
}
|
|
11
|
-
else {
|
|
12
|
-
currentBatch = dataDelivery.slice(batchSize * iteration, batchSize + batchSize * iteration);
|
|
13
|
-
}
|
|
14
|
-
};
|
|
15
|
-
const runItem = async (entry, globalIndex) => {
|
|
16
|
-
try {
|
|
17
|
-
await functionToRun(entry, globalIndex);
|
|
18
|
-
}
|
|
19
|
-
catch (error) {
|
|
20
|
-
hasError = true;
|
|
21
|
-
if (throwOnError && !firstError)
|
|
22
|
-
firstError = { error };
|
|
23
|
-
await onError(entry, error);
|
|
24
|
-
}
|
|
25
|
-
};
|
|
26
|
-
const processCurrentBatch = async () => {
|
|
27
|
-
for (let start = 0; start < currentBatch.length; start += effectiveConcurrency) {
|
|
28
|
-
if (firstError)
|
|
29
|
-
return;
|
|
30
|
-
const chunk = currentBatch.slice(start, start + effectiveConcurrency);
|
|
31
|
-
await Promise.allSettled(chunk.map((entry, i) => runItem(entry, batchSize * iteration + start + i)));
|
|
32
|
-
}
|
|
33
|
-
};
|
|
34
|
-
await getNextBatch();
|
|
35
|
-
await onProgress(0);
|
|
36
|
-
while (currentBatch.length) {
|
|
37
|
-
await processCurrentBatch();
|
|
38
|
-
if (firstError)
|
|
39
|
-
throw firstError.error;
|
|
40
|
-
if (currentBatch.length < batchSize) {
|
|
41
|
-
await onProgress(batchSize * iteration + currentBatch.length);
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
iteration += 1;
|
|
45
|
-
await onProgress(batchSize * iteration);
|
|
46
|
-
if (hasError && returnOnError) {
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
await getNextBatch();
|
|
50
|
-
if (currentBatch.length && pauseAfterBatch) {
|
|
51
|
-
await new Promise((resolve) => {
|
|
52
|
-
setTimeout(resolve, pauseAfterBatch);
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
//# sourceMappingURL=batchify.js.map
|
package/dist/batchify.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"batchify.js","sourceRoot":"","sources":["../src/batchify.ts"],"names":[],"mappings":"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"}
|