@nxgt/mongo-meilisearch 0.4.3 → 0.5.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 (51) hide show
  1. package/README.md +149 -4
  2. package/dist/index.d.ts +5 -3
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +201 -90
  5. package/dist/index.js.map +10 -9
  6. package/dist/{follow.d.ts → run/follow.d.ts} +2 -2
  7. package/dist/run/follow.d.ts.map +1 -0
  8. package/dist/{lease.d.ts → run/lease.d.ts} +2 -2
  9. package/dist/run/lease.d.ts.map +1 -0
  10. package/dist/{reindex.d.ts → run/reindex.d.ts} +2 -2
  11. package/dist/run/reindex.d.ts.map +1 -0
  12. package/dist/sync/batch.d.ts.map +1 -0
  13. package/dist/{context.d.ts → sync/context.d.ts} +1 -1
  14. package/dist/sync/context.d.ts.map +1 -0
  15. package/dist/sync/create-search-sync.d.ts.map +1 -0
  16. package/dist/sync/documents.d.ts.map +1 -0
  17. package/dist/sync/errors.d.ts.map +1 -0
  18. package/dist/sync/running.d.ts.map +1 -0
  19. package/dist/sync/start.d.ts.map +1 -0
  20. package/dist/sync/state.d.ts.map +1 -0
  21. package/dist/sync/types.d.ts.map +1 -0
  22. package/dist/syncs/create-search-syncs.d.ts +13 -0
  23. package/dist/syncs/create-search-syncs.d.ts.map +1 -0
  24. package/dist/syncs/types.d.ts +89 -0
  25. package/dist/syncs/types.d.ts.map +1 -0
  26. package/docs/README.md +3 -1
  27. package/docs/guide/search-syncs/lifecycle.md +301 -0
  28. package/docs/guide/search-syncs/wiring.md +188 -0
  29. package/docs/roadmap.md +18 -0
  30. package/docs/troubleshooting.md +213 -1
  31. package/package.json +4 -4
  32. package/dist/batch.d.ts.map +0 -1
  33. package/dist/context.d.ts.map +0 -1
  34. package/dist/create-search-sync.d.ts.map +0 -1
  35. package/dist/documents.d.ts.map +0 -1
  36. package/dist/errors.d.ts.map +0 -1
  37. package/dist/follow.d.ts.map +0 -1
  38. package/dist/lease.d.ts.map +0 -1
  39. package/dist/reindex.d.ts.map +0 -1
  40. package/dist/running.d.ts.map +0 -1
  41. package/dist/start.d.ts.map +0 -1
  42. package/dist/state.d.ts.map +0 -1
  43. package/dist/types.d.ts.map +0 -1
  44. /package/dist/{batch.d.ts → sync/batch.d.ts} +0 -0
  45. /package/dist/{create-search-sync.d.ts → sync/create-search-sync.d.ts} +0 -0
  46. /package/dist/{documents.d.ts → sync/documents.d.ts} +0 -0
  47. /package/dist/{errors.d.ts → sync/errors.d.ts} +0 -0
  48. /package/dist/{running.d.ts → sync/running.d.ts} +0 -0
  49. /package/dist/{start.d.ts → sync/start.d.ts} +0 -0
  50. /package/dist/{state.d.ts → sync/state.d.ts} +0 -0
  51. /package/dist/{types.d.ts → sync/types.d.ts} +0 -0
package/README.md CHANGED
@@ -1,9 +1,49 @@
1
1
  # @nxgt/mongo-meilisearch
2
2
 
3
- Keeps a [Meilisearch](https://www.meilisearch.com) index in step with a
4
- MongoDB collection: a typed transform from one to the other, a full reindex,
5
- and a change stream that picks up where it stopped, its position kept in
6
- MongoDB.
3
+ Keeps [Meilisearch](https://www.meilisearch.com) indexes in step with MongoDB
4
+ collections: a typed transform from one to the other, a full reindex, and a
5
+ change stream that picks up where it stopped, its position kept in MongoDB.
6
+ `createSearchSyncs` does it for every collection an `openMongo` wires, from one
7
+ config and one `start()`; `createSearchSync` does it for one collection.
8
+
9
+ ```ts
10
+ import { bindIndex } from '@nxgt/meilisearch';
11
+ import { defineMongo, openMongo } from '@nxgt/mongo';
12
+ import { createSearchSyncs } from '@nxgt/mongo-meilisearch';
13
+
14
+ const mongo = await openMongo(defineMongo({ uri, collections })); // @nxgt/mongo
15
+
16
+ const search = createSearchSyncs(mongo, {
17
+ articles: {
18
+ index: bindIndex(meili, articleIndex), // @nxgt/meilisearch
19
+ // `article` is typed by the collection the key names, and the result
20
+ // by the index this entry carries. `null` keeps a document out.
21
+ transform: (article) =>
22
+ article.draft ? null : { id: String(article._id), title: article.title },
23
+ },
24
+ authors: {
25
+ index: bindIndex(meili, authorIndex),
26
+ transform: (author) => ({ id: String(author._id), name: author.name }),
27
+ },
28
+ });
29
+
30
+ await search.syncIndexes(); // every index, with its settings
31
+ const running = await search.start(); // reindexes the first time, then follows
32
+ running.failed.catch(exit); // await it or catch it — see Traps
33
+ // …
34
+ await running.close(); // flushes and stops every sync
35
+ ```
36
+
37
+ Each collection's sync is the `createSearchSync` below, unchanged. What
38
+ `createSearchSyncs` adds is the wiring: the collections come from the Mongo,
39
+ under the key they are exported under (the same one `mongo.db.articles`
40
+ answers to), so nothing is named twice, and the syncs are started and stopped
41
+ together. A key the Mongo wires no collection for does not compile, and is
42
+ refused by name at run time too. `createSearchSyncs` sends nothing:
43
+ `syncIndexes`, `reindexAll` and `start` do. See [Several
44
+ collections](#several-collections-createsearchsyncs) for its API.
45
+
46
+ ## One collection at a time
7
47
 
8
48
  ```ts
9
49
  import { createSearchSync } from '@nxgt/mongo-meilisearch';
@@ -359,10 +399,76 @@ before it could be read),
359
399
  `cause`. Its constructor takes `(message, options: SearchSyncErrorOptions)`,
360
400
  that is `{ code, sync, cause?, holder?, expiresAt? }`; both types are exported.
361
401
 
402
+ ## Several collections: `createSearchSyncs`
403
+
404
+ ```ts
405
+ function createSearchSyncs<C, const I extends IndexMap<I>>(
406
+ mongo: Mongo<C>,
407
+ config: SearchSyncsConfig<C, I>,
408
+ ): SearchSyncs<I>;
409
+ ```
410
+
411
+ `mongo` is what `@nxgt/mongo`'s `openMongo` returned, and `config` is one entry
412
+ per collection, under the key the Mongo wires it under. An entry is everything
413
+ `createSearchSync` takes **except `collection`**, which the Mongo already
414
+ holds: `index`, `transform`, `toIndexId` (optional while the index's ids are
415
+ strings, required otherwise), and optionally `name`, `stateCollection`,
416
+ `batchSize`, `flushIntervalMs`, `positionIntervalMs`, `leaseMs`, `pageSize` and
417
+ `onHistoryLost`.
418
+
419
+ `SearchSyncs<I>`:
420
+
421
+ | Member | |
422
+ | --- | --- |
423
+ | `syncs: { [K in keyof I]: SearchSync }` | each sync as `createSearchSync` built it, so anything this does not wrap is still reachable |
424
+ | `state()` | where each sync stands, under its key; `undefined` for one that never reindexed |
425
+ | `syncIndexes(options?)` | every index created and its settings applied where they differ: `@nxgt/meilisearch`'s `SyncReport` per key, `dryRun` and `wait` passed through. One after another; the first that throws stops the rest |
426
+ | `reindexAll()` | a `ReindexReport` per key. One after another, and the first that throws stops the rest |
427
+ | `start()` | every sync, resolving once they are all hearing changes; one that fails to start closes the ones already started |
428
+
429
+ `RunningSearchSyncs<I>`, also `AsyncDisposable`:
430
+
431
+ | Member | |
432
+ | --- | --- |
433
+ | `running: { [K in keyof I]: RunningSearchSync }` | each running sync, so its `ready`, its `closed` and its own `flush` are still reachable |
434
+ | `failed: Promise<never>` | rejects with the **first** sync that stops on an error, and never settles otherwise. A later failure is `close()`'s to report |
435
+ | `flush()` | sends what every sync holds, and records where each one is. Stops at the first that fails |
436
+ | `close()` | flushes, then stops every sync. Idempotent |
437
+
438
+ The types: `SearchSyncsConfig<C, I>` (a `SearchSyncEntry` per key of
439
+ `SoleCollections<C>`, and for a key the Mongo wires no collection for, a
440
+ refusal string that names it in place of the entry), `SearchSyncEntry<Col, I>`
441
+ (`Omit<SearchSyncOptions<Col, I>, 'collection'>`), `SoleCollections<C>` (what a
442
+ Mongo's sole database holds, by the name each definition is exported under;
443
+ `never` when it holds several), `IndexMap<I>` (the index definitions a config
444
+ names, from which `I` is inferred, which is what lets a `transform` be written
445
+ inline) and `ByKey<S, T>` (`{ readonly [K in keyof S]: T }`, the shape of every
446
+ per-key result).
447
+
448
+ What it does not do:
449
+
450
+ - **Follow several databases.** A Mongo that holds several gives `never` for
451
+ its keys, as `mongo.db` itself does, and `createSearchSyncs` throws naming
452
+ them. Build one `createSearchSyncs` per database, from a Mongo that wires
453
+ that database alone.
454
+ - **Coordinate processes on its own.** Each entry's sync takes the lease on its
455
+ name when it starts: a second process starting the same syncs is refused
456
+ with `RUNNING` for the first sync whose name is held, and starts nothing.
457
+ - **Own the Mongo.** Closing the search syncs stops them and nothing else:
458
+ the clients, the databases and the collections are the Mongo's, and
459
+ `mongo.close()` is still the caller's to make.
460
+
461
+ The [guides](docs/guide/search-syncs/wiring.md) cover the config and the
462
+ [lifecycle](docs/guide/search-syncs/lifecycle.md) in full.
463
+
362
464
  ## What does not compile
363
465
 
364
466
  Each is a `@ts-expect-error` case in this package's type tests.
365
467
 
468
+ - A key the Mongo wires no collection for (the message names it), a key that
469
+ is a member of the driver's `Db`, such as `command`, and a Mongo that holds
470
+ more than one database: all `createSearchSyncs`'s. Its entries are
471
+ checked as below, the `transform` typed by the collection the key names.
366
472
  - A transform that reads a field the collection's schema does not have.
367
473
  - A transform that leaves out a field the index's document has, or gives an
368
474
  id of the wrong type (an `ObjectId` for a string id). A field the document
@@ -420,9 +526,48 @@ Each is a `@ts-expect-error` case in this package's type tests.
420
526
  `documents.delete` and `tasks.get`, plus `indexes.create` unless the index
421
527
  already exists — the first write to an index that does not creates it.
422
528
 
529
+ Of `createSearchSyncs`:
530
+
531
+ - **A failure nobody handles is silent, not loud.** A sync that stops on an
532
+ error rejects `failed`, and `createSearchSyncs` takes that rejection itself,
533
+ as it takes each sync's own `closed`, so nothing ends the process and nothing
534
+ is printed. That index simply stops updating. `failed` is the only place the
535
+ failure surfaces, so handle it; unhandled, the first sign is stale search
536
+ results.
537
+ - **`failed` never resolves.** A clean stop is not an event to wait for, so
538
+ `await running.failed` after `close()` waits forever. It is for `catch`, or
539
+ for racing against your own shutdown.
540
+ - **A dropped collection leaves `failed` silent.** An invalidated stream is a
541
+ clean stop (`closed` *resolves* with `'invalidated'`), and `failed` forwards
542
+ failures only. That sync is dead and nothing settles. Watch
543
+ `running.running.<key>.closed` if a drop has to be noticed.
544
+ - **`close()` does not report the failure `failed` already carried.** That one
545
+ sync rejects its own `close` with what it stopped on, and it is swallowed, so
546
+ a caller does not hear it twice. Every *other* failure is thrown, including a
547
+ **second** sync that fell over after `failed` settled, and anything that goes
548
+ wrong *while* closing. If more than one throws, `close` reports the first.
549
+ - **A failed `start()` leaves nothing running.** The syncs already started are
550
+ closed before the error comes back.
551
+ - **`reindexAll()` and `flush()` stop at the first failure.** The keys already
552
+ done are lost with the rejection of `reindexAll`, because a reindex removes
553
+ what a collection no longer gives and a half-finished run is not a state to
554
+ keep going from: read the error, fix, run it again, since the reindexes that
555
+ succeeded are idempotent. A sync that has already fallen over rejects
556
+ `flush` at once and the ones after it are neither sent nor recorded, which is
557
+ safe on restart but not free. `close()` keeps going past a sync that fails.
558
+ - **Two `createSearchSyncs` over one collection are two syncs over one
559
+ index.** The default `name` is `<collection>:<index uid>`, so two built from
560
+ the same config share their resume point and their lease: the second is
561
+ refused with `RUNNING` while the first runs. Give `name` if you mean them to
562
+ differ.
563
+
423
564
  ## Documentation
424
565
 
425
566
  - [Guide index](docs/README.md) — every page, and when to read it.
567
+ - [Wiring it over a Mongo](docs/guide/search-syncs/wiring.md) — the
568
+ `createSearchSyncs` config, its keys, and every option an entry takes.
569
+ - [The search syncs' lifecycle](docs/guide/search-syncs/lifecycle.md) —
570
+ `syncIndexes`, `reindexAll`, `start`, `failed`, `flush` and `close`.
426
571
  - [The sync's lifecycle](docs/guide/sync-lifecycle.md) — the two definitions,
427
572
  the transform, and every option with its default.
428
573
  - [Reindexing](docs/guide/reindex.md) — the full fill, and what it removes.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
- export { createSearchSync } from './create-search-sync';
2
- export { SearchSyncError, type SearchSyncErrorCode, type SearchSyncErrorOptions, } from './errors';
3
- export type { ReindexReport, RunningSearchSync, SearchSync, SearchSyncOptions, SearchSyncState, ToIndexId, Transform, } from './types';
1
+ export { createSearchSync } from './sync/create-search-sync';
2
+ export { SearchSyncError, type SearchSyncErrorCode, type SearchSyncErrorOptions, } from './sync/errors';
3
+ export type { ReindexReport, RunningSearchSync, SearchSync, SearchSyncOptions, SearchSyncState, ToIndexId, Transform, } from './sync/types';
4
+ export { createSearchSyncs } from './syncs/create-search-syncs';
5
+ export type { ByKey, IndexMap, RunningSearchSyncs, SearchSyncEntry, SearchSyncs, SearchSyncsConfig, SoleCollections, } from './syncs/types';
4
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,EACN,eAAe,EACf,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,GAC3B,MAAM,UAAU,CAAC;AAClB,YAAY,EACX,aAAa,EACb,iBAAiB,EACjB,UAAU,EACV,iBAAiB,EACjB,eAAe,EACf,SAAS,EACT,SAAS,GACT,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAC7D,OAAO,EACN,eAAe,EACf,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,GAC3B,MAAM,eAAe,CAAC;AACvB,YAAY,EACX,aAAa,EACb,iBAAiB,EACjB,UAAU,EACV,iBAAiB,EACjB,eAAe,EACf,SAAS,EACT,SAAS,GACT,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAChE,YAAY,EACX,KAAK,EACL,QAAQ,EACR,kBAAkB,EAClB,eAAe,EACf,WAAW,EACX,iBAAiB,EACjB,eAAe,GACf,MAAM,eAAe,CAAC"}
package/dist/index.js CHANGED
@@ -1,49 +1,7 @@
1
- // src/context.ts
2
- var DEFAULT_STATE_COLLECTION = "nxgt_search_sync";
3
- function positive(option, value, fallback) {
4
- const resolved = value ?? fallback;
5
- if (!Number.isInteger(resolved) || resolved < 1) {
6
- throw new TypeError(`createSearchSync: ${option} must be a whole number above 0, not ${String(value)}`);
7
- }
8
- return resolved;
9
- }
10
- function createContext(options) {
11
- const { collection, index } = options;
12
- if (typeof options.transform !== "function") {
13
- throw new TypeError("createSearchSync: transform must be a function");
14
- }
15
- const name = options.name ?? `${collection.collectionName}:${index.uid}`;
16
- if (name === "") {
17
- throw new TypeError("createSearchSync: name must not be empty");
18
- }
19
- const flushIntervalMs = options.flushIntervalMs ?? 1000;
20
- if (!Number.isInteger(flushIntervalMs) || flushIntervalMs < 0) {
21
- throw new TypeError(`createSearchSync: flushIntervalMs must be a whole number of milliseconds, not ${String(options.flushIntervalMs)}`);
22
- }
23
- const stateCollection = options.stateCollection ?? DEFAULT_STATE_COLLECTION;
24
- return {
25
- name,
26
- collection,
27
- index,
28
- primaryKey: index.definition.primaryKey,
29
- state: collection.db.collection(stateCollection),
30
- leases: collection.db.collection(stateCollection),
31
- leaseMs: positive("leaseMs", options.leaseMs, 30000),
32
- batchSize: positive("batchSize", options.batchSize, 500),
33
- positionIntervalMs: positive("positionIntervalMs", options.positionIntervalMs, 60000),
34
- flushIntervalMs,
35
- pageSize: positive("pageSize", options.pageSize, 100),
36
- onHistoryLost: options.onHistoryLost ?? "reindex",
37
- transform: options.transform,
38
- toIndexId: options.toIndexId ?? String,
39
- follower: { running: false }
40
- };
41
- }
42
-
43
- // src/reindex.ts
1
+ // src/run/reindex.ts
44
2
  import { MeilisearchApiError } from "meilisearch";
45
3
 
46
- // src/batch.ts
4
+ // src/sync/batch.ts
47
5
  var WAIT = { wait: { timeout: 120000 } };
48
6
  function chunks(items, size) {
49
7
  const out = [];
@@ -75,7 +33,7 @@ async function send(ctx, entries) {
75
33
  await deleteIds(ctx, ids);
76
34
  }
77
35
 
78
- // src/errors.ts
36
+ // src/sync/errors.ts
79
37
  class SearchSyncError extends Error {
80
38
  name = "SearchSyncError";
81
39
  code;
@@ -101,7 +59,7 @@ function failed(sync, doing, error) {
101
59
  });
102
60
  }
103
61
 
104
- // src/documents.ts
62
+ // src/sync/documents.ts
105
63
  var keyOf = (id) => `${typeof id}:${String(id)}`;
106
64
  var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
107
65
  function describe(value) {
@@ -129,7 +87,44 @@ async function entryOf(ctx, mongoId, document) {
129
87
  return { kind: "upsert", key, document: indexed };
130
88
  }
131
89
 
132
- // src/lease.ts
90
+ // src/sync/running.ts
91
+ function checkIdle(ctx, doing) {
92
+ if (!ctx.follower.running)
93
+ return;
94
+ throw new SearchSyncError(`Search sync "${ctx.name}" is already following changes in this ` + `process: close it before you ${doing}.`, { code: "RUNNING", sync: ctx.name });
95
+ }
96
+
97
+ // src/sync/state.ts
98
+ async function readState(ctx) {
99
+ try {
100
+ return await ctx.state.findOne({ _id: ctx.name }) ?? undefined;
101
+ } catch (error) {
102
+ throw failed(ctx.name, "reading its resume point", error);
103
+ }
104
+ }
105
+ async function clearState(ctx) {
106
+ try {
107
+ await ctx.state.deleteOne({ _id: ctx.name });
108
+ } catch (error) {
109
+ throw failed(ctx.name, "forgetting its resume point", error);
110
+ }
111
+ }
112
+ async function saveState(ctx, resumeToken, reindexed) {
113
+ const now = new Date;
114
+ try {
115
+ await ctx.state.updateOne({ _id: ctx.name }, {
116
+ $set: {
117
+ resumeToken,
118
+ updatedAt: now,
119
+ ...reindexed ? { reindexedAt: now } : {}
120
+ }
121
+ }, { upsert: true });
122
+ } catch (error) {
123
+ throw failed(ctx.name, "recording its resume point", error);
124
+ }
125
+ }
126
+
127
+ // src/run/lease.ts
133
128
  import { hostname } from "node:os";
134
129
  import { ObjectId } from "mongodb";
135
130
  var idOf = (ctx) => ({ lease: ctx.name });
@@ -239,44 +234,7 @@ async function withLease(ctx, doing, fn) {
239
234
  }
240
235
  }
241
236
 
242
- // src/running.ts
243
- function checkIdle(ctx, doing) {
244
- if (!ctx.follower.running)
245
- return;
246
- throw new SearchSyncError(`Search sync "${ctx.name}" is already following changes in this ` + `process: close it before you ${doing}.`, { code: "RUNNING", sync: ctx.name });
247
- }
248
-
249
- // src/state.ts
250
- async function readState(ctx) {
251
- try {
252
- return await ctx.state.findOne({ _id: ctx.name }) ?? undefined;
253
- } catch (error) {
254
- throw failed(ctx.name, "reading its resume point", error);
255
- }
256
- }
257
- async function clearState(ctx) {
258
- try {
259
- await ctx.state.deleteOne({ _id: ctx.name });
260
- } catch (error) {
261
- throw failed(ctx.name, "forgetting its resume point", error);
262
- }
263
- }
264
- async function saveState(ctx, resumeToken, reindexed) {
265
- const now = new Date;
266
- try {
267
- await ctx.state.updateOne({ _id: ctx.name }, {
268
- $set: {
269
- resumeToken,
270
- updatedAt: now,
271
- ...reindexed ? { reindexedAt: now } : {}
272
- }
273
- }, { upsert: true });
274
- } catch (error) {
275
- throw failed(ctx.name, "recording its resume point", error);
276
- }
277
- }
278
-
279
- // src/reindex.ts
237
+ // src/run/reindex.ts
280
238
  async function currentToken(ctx) {
281
239
  const stream = ctx.collection.raw.watch([], { maxAwaitTimeMS: 1 });
282
240
  try {
@@ -355,7 +313,49 @@ async function reindexHeld(ctx, lease) {
355
313
  }
356
314
  }
357
315
 
358
- // src/follow.ts
316
+ // src/sync/context.ts
317
+ var DEFAULT_STATE_COLLECTION = "nxgt_search_sync";
318
+ function positive(option, value, fallback) {
319
+ const resolved = value ?? fallback;
320
+ if (!Number.isInteger(resolved) || resolved < 1) {
321
+ throw new TypeError(`createSearchSync: ${option} must be a whole number above 0, not ${String(value)}`);
322
+ }
323
+ return resolved;
324
+ }
325
+ function createContext(options) {
326
+ const { collection, index } = options;
327
+ if (typeof options.transform !== "function") {
328
+ throw new TypeError("createSearchSync: transform must be a function");
329
+ }
330
+ const name = options.name ?? `${collection.collectionName}:${index.uid}`;
331
+ if (name === "") {
332
+ throw new TypeError("createSearchSync: name must not be empty");
333
+ }
334
+ const flushIntervalMs = options.flushIntervalMs ?? 1000;
335
+ if (!Number.isInteger(flushIntervalMs) || flushIntervalMs < 0) {
336
+ throw new TypeError(`createSearchSync: flushIntervalMs must be a whole number of milliseconds, not ${String(options.flushIntervalMs)}`);
337
+ }
338
+ const stateCollection = options.stateCollection ?? DEFAULT_STATE_COLLECTION;
339
+ return {
340
+ name,
341
+ collection,
342
+ index,
343
+ primaryKey: index.definition.primaryKey,
344
+ state: collection.db.collection(stateCollection),
345
+ leases: collection.db.collection(stateCollection),
346
+ leaseMs: positive("leaseMs", options.leaseMs, 30000),
347
+ batchSize: positive("batchSize", options.batchSize, 500),
348
+ positionIntervalMs: positive("positionIntervalMs", options.positionIntervalMs, 60000),
349
+ flushIntervalMs,
350
+ pageSize: positive("pageSize", options.pageSize, 100),
351
+ onHistoryLost: options.onHistoryLost ?? "reindex",
352
+ transform: options.transform,
353
+ toIndexId: options.toIndexId ?? String,
354
+ follower: { running: false }
355
+ };
356
+ }
357
+
358
+ // src/run/follow.ts
359
359
  var noop = () => {
360
360
  return;
361
361
  };
@@ -492,7 +492,7 @@ async function open(ctx, token, lease) {
492
492
  };
493
493
  }
494
494
 
495
- // src/start.ts
495
+ // src/sync/start.ts
496
496
  function historyLost(error) {
497
497
  const code = error?.serverCode;
498
498
  return code === 286 || code === 280;
@@ -541,7 +541,7 @@ async function startHeld(ctx, lease) {
541
541
  }
542
542
  }
543
543
 
544
- // src/create-search-sync.ts
544
+ // src/sync/create-search-sync.ts
545
545
  function createSearchSync(options) {
546
546
  const ctx = createContext(options);
547
547
  return {
@@ -551,10 +551,121 @@ function createSearchSync(options) {
551
551
  state: () => readState(ctx)
552
552
  };
553
553
  }
554
+ // src/syncs/create-search-syncs.ts
555
+ function soleScope(mongo) {
556
+ const names = Object.keys(mongo.databases);
557
+ if (names.length !== 1) {
558
+ throw new TypeError("createSearchSyncs: this Mongo holds " + `${names.length} databases (${names.join(", ")}), and ` + "createSearchSyncs follows the collections of one. Build one " + "`createSearchSyncs` per database, from a Mongo that wires that " + "database alone");
559
+ }
560
+ return mongo.db;
561
+ }
562
+ function collectionAt(scope, key) {
563
+ const collection = scope[key];
564
+ const definition = typeof collection === "object" && collection !== null ? collection.definition : undefined;
565
+ if (typeof definition !== "object" || definition === null || !("schema" in definition)) {
566
+ throw new TypeError(`createSearchSyncs: this Mongo wires no collection called "${key}"`);
567
+ }
568
+ return collection;
569
+ }
570
+ function createSearchSyncs(mongo, config) {
571
+ const scope = soleScope(mongo);
572
+ const keys = Object.keys(config);
573
+ const syncs = {};
574
+ const indexes = {};
575
+ for (const key of keys) {
576
+ const entry = config[key];
577
+ indexes[key] = entry["index"];
578
+ syncs[key] = createSearchSync({
579
+ ...entry,
580
+ collection: collectionAt(scope, key)
581
+ });
582
+ }
583
+ return {
584
+ syncs,
585
+ async state() {
586
+ const state = {};
587
+ for (const key of keys)
588
+ state[key] = await syncs[key].state();
589
+ return state;
590
+ },
591
+ async syncIndexes(options) {
592
+ const reports = {};
593
+ for (const key of keys)
594
+ reports[key] = await indexes[key].sync(options);
595
+ return reports;
596
+ },
597
+ async reindexAll() {
598
+ const reports = {};
599
+ for (const key of keys)
600
+ reports[key] = await syncs[key].reindex();
601
+ return reports;
602
+ },
603
+ async start() {
604
+ const running = {};
605
+ const started = [];
606
+ for (const key of keys) {
607
+ let one;
608
+ try {
609
+ one = await syncs[key].start();
610
+ } catch (error) {
611
+ await Promise.allSettled(started.map((it) => it.close()));
612
+ throw error;
613
+ }
614
+ one.closed.catch(() => {
615
+ return;
616
+ });
617
+ running[key] = one;
618
+ started.push(one);
619
+ }
620
+ return runningSyncs(running, started);
621
+ }
622
+ };
623
+ }
624
+ function runningSyncs(running, started) {
625
+ let closing;
626
+ let reported;
627
+ const failed = Promise.race(started.map((one) => one.closed.then(() => new Promise(() => {}), (reason) => {
628
+ reported ??= one;
629
+ throw reason;
630
+ })));
631
+ failed.catch(() => {
632
+ return;
633
+ });
634
+ const close = () => {
635
+ closing ??= (async () => {
636
+ let first;
637
+ let caught = false;
638
+ for (const one of started) {
639
+ try {
640
+ await one.close();
641
+ } catch (error) {
642
+ if (one === reported || caught)
643
+ continue;
644
+ first = error;
645
+ caught = true;
646
+ }
647
+ }
648
+ if (caught)
649
+ throw first;
650
+ })();
651
+ return closing;
652
+ };
653
+ return {
654
+ running,
655
+ failed,
656
+ async flush() {
657
+ for (const one of started)
658
+ await one.flush();
659
+ },
660
+ close,
661
+ [Symbol.asyncDispose]: close
662
+ };
663
+ }
554
664
  export {
555
665
  SearchSyncError,
556
- createSearchSync
666
+ createSearchSync,
667
+ createSearchSyncs
557
668
  };
558
669
 
559
- //# debugId=CB9FE6862D67E10C64756E2164756E21
670
+ //# debugId=C960A5123B6682B664756E2164756E21
560
671
  //# sourceMappingURL=index.js.map