@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.
- package/README.md +149 -4
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +201 -90
- package/dist/index.js.map +10 -9
- package/dist/{follow.d.ts → run/follow.d.ts} +2 -2
- package/dist/run/follow.d.ts.map +1 -0
- package/dist/{lease.d.ts → run/lease.d.ts} +2 -2
- package/dist/run/lease.d.ts.map +1 -0
- package/dist/{reindex.d.ts → run/reindex.d.ts} +2 -2
- package/dist/run/reindex.d.ts.map +1 -0
- package/dist/sync/batch.d.ts.map +1 -0
- package/dist/{context.d.ts → sync/context.d.ts} +1 -1
- package/dist/sync/context.d.ts.map +1 -0
- package/dist/sync/create-search-sync.d.ts.map +1 -0
- package/dist/sync/documents.d.ts.map +1 -0
- package/dist/sync/errors.d.ts.map +1 -0
- package/dist/sync/running.d.ts.map +1 -0
- package/dist/sync/start.d.ts.map +1 -0
- package/dist/sync/state.d.ts.map +1 -0
- package/dist/sync/types.d.ts.map +1 -0
- package/dist/syncs/create-search-syncs.d.ts +13 -0
- package/dist/syncs/create-search-syncs.d.ts.map +1 -0
- package/dist/syncs/types.d.ts +89 -0
- package/dist/syncs/types.d.ts.map +1 -0
- package/docs/README.md +3 -1
- package/docs/guide/search-syncs/lifecycle.md +301 -0
- package/docs/guide/search-syncs/wiring.md +188 -0
- package/docs/roadmap.md +18 -0
- package/docs/troubleshooting.md +213 -1
- package/package.json +4 -4
- package/dist/batch.d.ts.map +0 -1
- package/dist/context.d.ts.map +0 -1
- package/dist/create-search-sync.d.ts.map +0 -1
- package/dist/documents.d.ts.map +0 -1
- package/dist/errors.d.ts.map +0 -1
- package/dist/follow.d.ts.map +0 -1
- package/dist/lease.d.ts.map +0 -1
- package/dist/reindex.d.ts.map +0 -1
- package/dist/running.d.ts.map +0 -1
- package/dist/start.d.ts.map +0 -1
- package/dist/state.d.ts.map +0 -1
- package/dist/types.d.ts.map +0 -1
- /package/dist/{batch.d.ts → sync/batch.d.ts} +0 -0
- /package/dist/{create-search-sync.d.ts → sync/create-search-sync.d.ts} +0 -0
- /package/dist/{documents.d.ts → sync/documents.d.ts} +0 -0
- /package/dist/{errors.d.ts → sync/errors.d.ts} +0 -0
- /package/dist/{running.d.ts → sync/running.d.ts} +0 -0
- /package/dist/{start.d.ts → sync/start.d.ts} +0 -0
- /package/dist/{state.d.ts → sync/state.d.ts} +0 -0
- /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
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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=
|
|
670
|
+
//# debugId=C960A5123B6682B664756E2164756E21
|
|
560
671
|
//# sourceMappingURL=index.js.map
|