@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.
@@ -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.4",
4
- "description": "Keeps a Meilisearch index in step with a MongoDB collection: a typed transform, a full reindex, and a change stream that resumes where it stopped",
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.0",
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.0",
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"