@nxgt/mongo-meilisearch 0.4.4 → 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 +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +113 -2
- package/dist/index.js.map +5 -4
- 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/docs/troubleshooting.md
CHANGED
|
@@ -9,7 +9,9 @@ Everything this package throws is a `SearchSyncError` carrying a `code`
|
|
|
9
9
|
(`HISTORY_LOST`, `ID_MISMATCH`, `NOT_A_DOCUMENT`, `RUNNING`, `LEASE_LOST`,
|
|
10
10
|
`FAILED`), the
|
|
11
11
|
sync's `name`, and the original error as `cause` — except the options, which
|
|
12
|
-
are refused with a `TypeError` before anything is opened
|
|
12
|
+
are refused with a `TypeError` before anything is opened, and the
|
|
13
|
+
refusals of `createSearchSyncs`, which are a `TypeError` too — see
|
|
14
|
+
[Several collections](#several-collections-createsearchsyncs).
|
|
13
15
|
|
|
14
16
|
| Area | Entries |
|
|
15
17
|
| --- | --- |
|
|
@@ -19,6 +21,7 @@ are refused with a `TypeError` before anything is opened.
|
|
|
19
21
|
| [The transform](#the-transform) | [an id that is not the index's](#search-sync-articlesarticles-transform-gave-id-other-for-the-document--whose-index-id-is-) · [not a document](#search-sync-articlesarticles-transform-gave-a-string-for-the-document-) · [it threw](#search-sync-articlesarticles-failed-following-changes-boom) |
|
|
20
22
|
| [Sending](#sending) | [an id Meilisearch refuses](#search-sync-articlesarticles-failed-sending-changes-task-3-add-on-index-articles-failed-invalid_document_id) |
|
|
21
23
|
| [Stopping](#stopping) | [lease lost](#search-sync-articlesarticles-lost-its-lease-another-process-holds-the-name-now-or-the-lease-was-removed-it-lapses-when-not-renewed-within-30000-ms-it-stopped-rather-than-run-beside-it) · [a dropped collection](#the-sync-stops-and-closed-resolves-with-invalidated) |
|
|
24
|
+
| [Several collections](#several-collections-createsearchsyncs) | [a key the Mongo does not wire](#type-typedindex-is-not-assignable-to-type-createsearchsyncs-this-mongo-wires-no-collection-called-comments) · [the same, at run time](#createsearchsyncs-this-mongo-wires-no-collection-called-comments) · [several databases](#createsearchsyncs-this-mongo-holds-2-databases-main-analytics-and-createsearchsyncs-follows-the-collections-of-one) · [a partial reindex](#search-sync-authorsauthors-failed-reindexing-) · [a silent failure](#one-index-stops-updating-and-nothing-is-thrown) · [`failed` never resolves](#await-runningfailed-never-resolves) · [a dropped collection is silent](#a-sync-stops-and-nothing-settles) · [`close()` rejects](#closing-rejects-with-an-error-failed-never-reported) |
|
|
22
25
|
|
|
23
26
|
## Install
|
|
24
27
|
|
|
@@ -542,3 +545,212 @@ running.closed.then(
|
|
|
542
545
|
|
|
543
546
|
Whatever the collection holds after it is recreated is indexed by the reindex
|
|
544
547
|
the next `start()` runs.
|
|
548
|
+
|
|
549
|
+
## Several collections: `createSearchSyncs`
|
|
550
|
+
|
|
551
|
+
These are the entries of `createSearchSyncs`, which wires one `createSearchSync`
|
|
552
|
+
per collection over what `openMongo` returned and starts and stops them
|
|
553
|
+
together. Each sync's own errors are the ones above; what is here is the
|
|
554
|
+
config, and the syncs run as one.
|
|
555
|
+
|
|
556
|
+
### `Type 'TypedIndex<…>' is not assignable to type '"createSearchSyncs: this Mongo wires no collection called \"comments\""'`
|
|
557
|
+
|
|
558
|
+
The whole line is a `TS2322`:
|
|
559
|
+
|
|
560
|
+
```
|
|
561
|
+
error TS2322: Type 'TypedIndex<IndexDefinition<…>>' is not assignable to type
|
|
562
|
+
'"createSearchSyncs: this Mongo wires no collection called \"comments\""'.
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
**When:** compiling the `createSearchSyncs` call. The refusal lands on that
|
|
566
|
+
entry's `index`, which is why the message reads as a type. A key the Mongo
|
|
567
|
+
wires as a GridFS **bucket** is refused the same way: a bucket is not a
|
|
568
|
+
collection, and has nothing to follow.
|
|
569
|
+
|
|
570
|
+
**Why:** a config key is the name a collection is **exported** under — the same
|
|
571
|
+
key `mongo.db.<key>` answers to. `comments` is not one of them: either the model
|
|
572
|
+
is not exported from the module the Mongo's config passes as `collections`, or
|
|
573
|
+
the export was renamed.
|
|
574
|
+
|
|
575
|
+
**Fix:** use the Mongo's own key:
|
|
576
|
+
|
|
577
|
+
```ts
|
|
578
|
+
const search = createSearchSyncs(mongo, {
|
|
579
|
+
articles: { index: bindIndex(meili, articleIndex), transform: toArticleHit },
|
|
580
|
+
// ^ `mongo.db.articles`, so `export const articles = defineCollection(…)`
|
|
581
|
+
});
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
A key that is a member of the driver's `Db` — `command`, `watch` — is refused
|
|
585
|
+
the same way: `@nxgt/mongo` never wires a collection under one of those.
|
|
586
|
+
|
|
587
|
+
### `createSearchSyncs: this Mongo wires no collection called "comments"`
|
|
588
|
+
|
|
589
|
+
**When:** calling `createSearchSyncs`, when the types were bypassed — an `as
|
|
590
|
+
never`, a config built at run time, or JavaScript.
|
|
591
|
+
|
|
592
|
+
**Why:** the same cause as the
|
|
593
|
+
[type error above](#type-typedindex-is-not-assignable-to-type-createsearchsyncs-this-mongo-wires-no-collection-called-comments).
|
|
594
|
+
It is checked again at run time because a `Db` answers to its own members: a
|
|
595
|
+
key that is one would give something that is not a collection rather than
|
|
596
|
+
`undefined`. The same holds for a key the Mongo wires a **GridFS bucket**
|
|
597
|
+
under (`@nxgt/mongo` 0.19.0 and later): a bucket sits on the scope beside
|
|
598
|
+
the collections, but there is nothing in it to search.
|
|
599
|
+
|
|
600
|
+
**Fix:** write the config as a literal argument to `createSearchSyncs`, so the
|
|
601
|
+
compiler refuses it first — a config assigned to a variable of a wider type
|
|
602
|
+
loses the refusal.
|
|
603
|
+
|
|
604
|
+
### `createSearchSyncs: this Mongo holds 2 databases (main, analytics), and createSearchSyncs follows the collections of one`
|
|
605
|
+
|
|
606
|
+
The message ends: *Build one `createSearchSyncs` per database, from a Mongo that wires
|
|
607
|
+
that database alone*.
|
|
608
|
+
|
|
609
|
+
**When:** calling `createSearchSyncs` with a Mongo built from a `databases` config.
|
|
610
|
+
|
|
611
|
+
**Why:** the config's keys come from the Mongo's **sole** database, the same way
|
|
612
|
+
`mongo.db` does. With several there is no sole one, so the key type is already
|
|
613
|
+
`never` — this is what the call gets at run time.
|
|
614
|
+
|
|
615
|
+
**Fix:** open one Mongo per database, and one `createSearchSyncs` over each:
|
|
616
|
+
|
|
617
|
+
```ts
|
|
618
|
+
const mainMongo = await openMongo(defineMongo({ uri, collections }));
|
|
619
|
+
const search = createSearchSyncs(mainMongo, {
|
|
620
|
+
articles: { index: bindIndex(meili, articleIndex), transform: toArticleHit },
|
|
621
|
+
});
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
### `Search sync "authors:authors" failed reindexing: …`
|
|
625
|
+
|
|
626
|
+
**When:** `reindexAll()`, on the first key that fails. The keys after it in the
|
|
627
|
+
config are not run, and the reports of the keys already done are **lost with
|
|
628
|
+
the rejection**.
|
|
629
|
+
|
|
630
|
+
**Why:** a reindex removes what a collection no longer gives, so a half-finished
|
|
631
|
+
run is not a state to carry on from. The error names the sync that stopped it;
|
|
632
|
+
what it stopped on is its `cause`.
|
|
633
|
+
|
|
634
|
+
**Fix:** read the error, fix it, and run it again — the reindexes that succeeded
|
|
635
|
+
are idempotent:
|
|
636
|
+
|
|
637
|
+
```ts
|
|
638
|
+
try {
|
|
639
|
+
await search.reindexAll();
|
|
640
|
+
} catch (error) {
|
|
641
|
+
log.error(error); // `error.sync` names the key that failed
|
|
642
|
+
throw error;
|
|
643
|
+
}
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
For a report per key whatever happens, reindex the syncs one at a time through
|
|
647
|
+
`search.syncs`:
|
|
648
|
+
|
|
649
|
+
```ts
|
|
650
|
+
for (const [key, sync] of Object.entries(search.syncs)) {
|
|
651
|
+
const report = await sync.reindex().catch((error: unknown) => error);
|
|
652
|
+
log.info({ key, report });
|
|
653
|
+
}
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
### One index stops updating, and nothing is thrown
|
|
657
|
+
|
|
658
|
+
The error that stopped it is
|
|
659
|
+
`Search sync "articles:articles" failed following changes: boom`, and it is
|
|
660
|
+
never printed: nothing in your process asked for it.
|
|
661
|
+
|
|
662
|
+
**When:** while the search syncs are running, after one sync stops on an error — a
|
|
663
|
+
transform that threw, a batch Meilisearch refused, a server that went away.
|
|
664
|
+
The other syncs carry on, so the symptom is one index falling behind.
|
|
665
|
+
|
|
666
|
+
**Why:** `failed` is a promise that **rejects** with the first sync that stops.
|
|
667
|
+
`createSearchSyncs` takes every rejection it makes so that none of them ends the process —
|
|
668
|
+
each sync's own `closed`, and `failed` itself — which means a failure nobody
|
|
669
|
+
took is a failure nobody hears about. `close()` will not report it either: it
|
|
670
|
+
deliberately stays quiet about the one `failed` carried.
|
|
671
|
+
|
|
672
|
+
**Fix:** take `failed` the moment you have it:
|
|
673
|
+
|
|
674
|
+
```ts
|
|
675
|
+
const running = await search.start();
|
|
676
|
+
running.failed.catch((error: unknown) => {
|
|
677
|
+
log.error(error);
|
|
678
|
+
void shutdown();
|
|
679
|
+
});
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
Or race it against your own shutdown, which is the other shape:
|
|
683
|
+
|
|
684
|
+
```ts
|
|
685
|
+
await Promise.race([running.failed, stopSignal]);
|
|
686
|
+
await running.close();
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
### `await running.failed` never resolves
|
|
690
|
+
|
|
691
|
+
Not an error: the line simply never returns.
|
|
692
|
+
|
|
693
|
+
**When:** awaiting `failed` after a clean `close()`, or on syncs where nothing
|
|
694
|
+
goes wrong.
|
|
695
|
+
|
|
696
|
+
**Why:** `failed` rejects on the first failure and **never resolves**: a clean
|
|
697
|
+
stop is not an event, so there is nothing for it to settle with.
|
|
698
|
+
|
|
699
|
+
**Fix:** use it for `catch`, or race it — never as the last `await` of a
|
|
700
|
+
shutdown:
|
|
701
|
+
|
|
702
|
+
```ts
|
|
703
|
+
running.failed.catch(exit); // handled, not awaited
|
|
704
|
+
await running.close(); // this is what resolves when the syncs stop
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
### A sync stops and nothing settles
|
|
708
|
+
|
|
709
|
+
**When:** the collection behind one sync is dropped or renamed while the search syncs
|
|
710
|
+
are running.
|
|
711
|
+
|
|
712
|
+
**Why:** the bridge treats an invalidated change stream as a **clean stop** —
|
|
713
|
+
that sync's `closed` *resolves* with `'invalidated'` — and `createSearchSyncs` forwards
|
|
714
|
+
failures only. So `failed` stays quiet, `close()` throws nothing, and that one
|
|
715
|
+
sync is dead while the others carry on.
|
|
716
|
+
|
|
717
|
+
**Fix:** watch the sync's own `closed` when a drop has to be noticed:
|
|
718
|
+
|
|
719
|
+
```ts
|
|
720
|
+
const running = await search.start();
|
|
721
|
+
for (const [key, one] of Object.entries(running.running)) {
|
|
722
|
+
one.closed.then((reason) => {
|
|
723
|
+
if (reason === 'invalidated') log.warn({ key }, 'collection dropped');
|
|
724
|
+
}, () => undefined); // failures are `failed`'s to report
|
|
725
|
+
}
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
What the collection holds after it is recreated is indexed by the reindex the
|
|
729
|
+
next `start()` runs for that sync.
|
|
730
|
+
|
|
731
|
+
### Closing rejects with an error `failed` never reported
|
|
732
|
+
|
|
733
|
+
**When:** `await running.close()` — including the implicit one of
|
|
734
|
+
`await using` — after something has already gone wrong.
|
|
735
|
+
|
|
736
|
+
**Why:** `close()` deliberately swallows the failure `failed` already carried,
|
|
737
|
+
so you do not have to hear the same error twice. Every **other** failure is
|
|
738
|
+
thrown: a second sync that fell over after `failed` had settled, and anything
|
|
739
|
+
that goes wrong while closing — the last flush of a sync that cannot reach
|
|
740
|
+
Meilisearch, for one. If more than one throws, `close()` reports the first.
|
|
741
|
+
|
|
742
|
+
**Fix:** handle both, and treat `close()` as able to fail:
|
|
743
|
+
|
|
744
|
+
```ts
|
|
745
|
+
const running = await search.start();
|
|
746
|
+
running.failed.catch((error: unknown) => log.error(error)); // the first failure
|
|
747
|
+
try {
|
|
748
|
+
await running.close();
|
|
749
|
+
} catch (error) {
|
|
750
|
+
log.error(error); // a second one, or a failure while closing
|
|
751
|
+
}
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
`close()` is idempotent, and it keeps going past a sync that fails, so the
|
|
755
|
+
others are still flushed and stopped. The Mongo is **not** closed with it:
|
|
756
|
+
`mongo.close()` stays yours to call.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nxgt/mongo-meilisearch",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Keeps
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Keeps Meilisearch indexes in step with MongoDB collections, one or every collection an openMongo wires: a typed transform, a full reindex, and a change stream that resumes where it stopped",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./dist/index.js",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@nxgt/meilisearch": "^0.6.1",
|
|
53
|
-
"@nxgt/mongo": "^0.19.
|
|
53
|
+
"@nxgt/mongo": "^0.19.1",
|
|
54
54
|
"@types/bun": "^1.4.0",
|
|
55
55
|
"meilisearch": "0.62.0",
|
|
56
56
|
"mongodb": "7.6.0",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
61
|
"@nxgt/meilisearch": "^0.6.1",
|
|
62
|
-
"@nxgt/mongo": "^0.19.
|
|
62
|
+
"@nxgt/mongo": "^0.19.1",
|
|
63
63
|
"meilisearch": ">=0.62.0 <1",
|
|
64
64
|
"mongodb": ">=7.0.0 <8",
|
|
65
65
|
"typescript": "^6.0.3"
|