@taladb/react 0.11.0 → 0.11.2

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.
@@ -41,6 +41,47 @@ function TalaDBProvider(props) {
41
41
  }
42
42
  return /* @__PURE__ */ jsx(NamedProvider, { ...props });
43
43
  }
44
+ var openDatabases = /* @__PURE__ */ new Map();
45
+ function databaseKey(name, optionsKey) {
46
+ return `${name}\0${optionsKey}`;
47
+ }
48
+ function acquireDatabase(name, optionsKey, options) {
49
+ const key = databaseKey(name, optionsKey);
50
+ let entry = openDatabases.get(key);
51
+ if (entry) {
52
+ if (entry.closing !== void 0) {
53
+ clearTimeout(entry.closing);
54
+ entry.closing = void 0;
55
+ }
56
+ } else {
57
+ entry = {
58
+ // Dynamic import so `taladb`'s runtime never loads during SSR module
59
+ // evaluation (its Node entry pulls in the native binding, which a web
60
+ // app's server bundle does not ship).
61
+ promise: import("taladb").then(({ openDB }) => openDB(name, options)),
62
+ refs: 0
63
+ };
64
+ openDatabases.set(key, entry);
65
+ }
66
+ entry.refs++;
67
+ return entry.promise;
68
+ }
69
+ function releaseDatabase(name, optionsKey) {
70
+ const key = databaseKey(name, optionsKey);
71
+ const entry = openDatabases.get(key);
72
+ if (!entry) return;
73
+ entry.refs--;
74
+ if (entry.refs > 0 || entry.closing !== void 0) return;
75
+ entry.closing = setTimeout(() => {
76
+ if (entry.refs > 0) {
77
+ entry.closing = void 0;
78
+ return;
79
+ }
80
+ openDatabases.delete(key);
81
+ void entry.promise.then((db) => db.close()).catch(() => {
82
+ });
83
+ }, 0);
84
+ }
44
85
  function NamedProvider({
45
86
  name,
46
87
  options,
@@ -54,20 +95,14 @@ function NamedProvider({
54
95
  useEffect(() => {
55
96
  setError(null);
56
97
  let cancelled = false;
57
- let opened = null;
58
- import("taladb").then(({ openDB }) => openDB(name, options)).then((instance) => {
59
- if (cancelled) {
60
- void instance.close();
61
- return;
62
- }
63
- opened = instance;
64
- setDb(instance);
98
+ acquireDatabase(name, optionsKey, options).then((instance) => {
99
+ if (!cancelled) setDb(instance);
65
100
  }).catch((e) => {
66
101
  if (!cancelled) setError(e);
67
102
  });
68
103
  return () => {
69
104
  cancelled = true;
70
- if (opened) void opened.close();
105
+ releaseDatabase(name, optionsKey);
71
106
  setDb(null);
72
107
  };
73
108
  }, [name, optionsKey]);
package/dist/index.js CHANGED
@@ -76,6 +76,47 @@ function TalaDBProvider(props) {
76
76
  }
77
77
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(NamedProvider, { ...props });
78
78
  }
79
+ var openDatabases = /* @__PURE__ */ new Map();
80
+ function databaseKey(name, optionsKey) {
81
+ return `${name}\0${optionsKey}`;
82
+ }
83
+ function acquireDatabase(name, optionsKey, options) {
84
+ const key = databaseKey(name, optionsKey);
85
+ let entry = openDatabases.get(key);
86
+ if (entry) {
87
+ if (entry.closing !== void 0) {
88
+ clearTimeout(entry.closing);
89
+ entry.closing = void 0;
90
+ }
91
+ } else {
92
+ entry = {
93
+ // Dynamic import so `taladb`'s runtime never loads during SSR module
94
+ // evaluation (its Node entry pulls in the native binding, which a web
95
+ // app's server bundle does not ship).
96
+ promise: import("taladb").then(({ openDB }) => openDB(name, options)),
97
+ refs: 0
98
+ };
99
+ openDatabases.set(key, entry);
100
+ }
101
+ entry.refs++;
102
+ return entry.promise;
103
+ }
104
+ function releaseDatabase(name, optionsKey) {
105
+ const key = databaseKey(name, optionsKey);
106
+ const entry = openDatabases.get(key);
107
+ if (!entry) return;
108
+ entry.refs--;
109
+ if (entry.refs > 0 || entry.closing !== void 0) return;
110
+ entry.closing = setTimeout(() => {
111
+ if (entry.refs > 0) {
112
+ entry.closing = void 0;
113
+ return;
114
+ }
115
+ openDatabases.delete(key);
116
+ void entry.promise.then((db) => db.close()).catch(() => {
117
+ });
118
+ }, 0);
119
+ }
79
120
  function NamedProvider({
80
121
  name,
81
122
  options,
@@ -89,20 +130,14 @@ function NamedProvider({
89
130
  (0, import_react.useEffect)(() => {
90
131
  setError(null);
91
132
  let cancelled = false;
92
- let opened = null;
93
- import("taladb").then(({ openDB }) => openDB(name, options)).then((instance) => {
94
- if (cancelled) {
95
- void instance.close();
96
- return;
97
- }
98
- opened = instance;
99
- setDb(instance);
133
+ acquireDatabase(name, optionsKey, options).then((instance) => {
134
+ if (!cancelled) setDb(instance);
100
135
  }).catch((e) => {
101
136
  if (!cancelled) setError(e);
102
137
  });
103
138
  return () => {
104
139
  cancelled = true;
105
- if (opened) void opened.close();
140
+ releaseDatabase(name, optionsKey);
106
141
  setDb(null);
107
142
  };
108
143
  }, [name, optionsKey]);
package/dist/index.mjs CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  useCollectionOptions,
6
6
  useFind,
7
7
  useTalaDB
8
- } from "./chunk-SEH233OC.mjs";
8
+ } from "./chunk-RAGFVMZN.mjs";
9
9
 
10
10
  // src/useFindOne.ts
11
11
  import { useCallback, useRef, useSyncExternalStore } from "react";
@@ -1440,22 +1440,7 @@ interface HydrateResult {
1440
1440
  /** Documents left alone because they carry unsynced local work. */
1441
1441
  skipped: number;
1442
1442
  }
1443
- /**
1444
- * Write a server response into the local collection.
1445
- *
1446
- * The rule that matters, and the one a naive bulk upsert gets wrong: **a
1447
- * response must never overwrite a document with unsynced local work.** The
1448
- * moment connectivity returns, a refetch and a queued edit race, and the
1449
- * obvious implementation silently discards whatever the user did offline.
1450
- * Anything not `synced` is therefore left exactly as it is — the drain loop
1451
- * owns those documents until the server accepts them.
1452
- *
1453
- * Inserts go through one `insertMany`; updates cannot be batched, because each
1454
- * document sets different values and the engine exposes no upsert. That matters
1455
- * on a secondary browser tab, where every write is forwarded to the primary and
1456
- * the forward buffer is bounded — see PLAN-query.md §5.
1457
- */
1458
- declare function hydrate<T extends Document>(collection: Collection<T>, docs: T[], now: number): Promise<HydrateResult>;
1443
+ declare function hydrate<T extends Document>(collection: Collection<T>, docs: T[], now: number, collectionName?: string): Promise<HydrateResult>;
1459
1444
 
1460
1445
  /**
1461
1446
  * Rules 1 and 2 — client ids are authoritative, and writes are idempotent.
@@ -1440,22 +1440,7 @@ interface HydrateResult {
1440
1440
  /** Documents left alone because they carry unsynced local work. */
1441
1441
  skipped: number;
1442
1442
  }
1443
- /**
1444
- * Write a server response into the local collection.
1445
- *
1446
- * The rule that matters, and the one a naive bulk upsert gets wrong: **a
1447
- * response must never overwrite a document with unsynced local work.** The
1448
- * moment connectivity returns, a refetch and a queued edit race, and the
1449
- * obvious implementation silently discards whatever the user did offline.
1450
- * Anything not `synced` is therefore left exactly as it is — the drain loop
1451
- * owns those documents until the server accepts them.
1452
- *
1453
- * Inserts go through one `insertMany`; updates cannot be batched, because each
1454
- * document sets different values and the engine exposes no upsert. That matters
1455
- * on a secondary browser tab, where every write is forwarded to the primary and
1456
- * the forward buffer is bounded — see PLAN-query.md §5.
1457
- */
1458
- declare function hydrate<T extends Document>(collection: Collection<T>, docs: T[], now: number): Promise<HydrateResult>;
1443
+ declare function hydrate<T extends Document>(collection: Collection<T>, docs: T[], now: number, collectionName?: string): Promise<HydrateResult>;
1459
1444
 
1460
1445
  /**
1461
1446
  * Rules 1 and 2 — client ids are authoritative, and writes are idempotent.
@@ -777,18 +777,25 @@ function describe(queryKey) {
777
777
  }
778
778
 
779
779
  // src/query/hydrate.ts
780
- async function hydrate(collection, docs, now) {
780
+ var import_taladb2 = require("taladb");
781
+ var hydrationChains = /* @__PURE__ */ new WeakMap();
782
+ function hydrate(collection, docs, now, collectionName) {
783
+ const key = collection;
784
+ const previous = hydrationChains.get(key) ?? Promise.resolve();
785
+ const run = previous.then(() => hydrateOnce(collection, docs, now, collectionName));
786
+ hydrationChains.set(key, run.catch(() => void 0));
787
+ return run;
788
+ }
789
+ async function hydrateOnce(collection, docs, now, collectionName) {
781
790
  if (docs.length === 0) return { inserted: 0, updated: 0, skipped: 0 };
782
- const ids = [];
791
+ const unique = /* @__PURE__ */ new Map();
783
792
  for (const doc of docs) {
784
- if (typeof doc._id !== "string" || doc._id === "") {
785
- throw new Error(
786
- "A hydrated document has no `_id`. Client ids are authoritative: the endpoint must echo back the `_id` it was sent, and must not assign its own."
787
- );
788
- }
789
- ids.push(doc._id);
793
+ if (!(0, import_taladb2.isDocId)(doc._id)) throw badDocId(doc._id, collectionName);
790
794
  if (process.env.NODE_ENV !== "production") warnOnReservedFields(doc);
795
+ unique.set(doc._id, doc);
791
796
  }
797
+ const deduped = [...unique.values()];
798
+ const ids = [...unique.keys()];
792
799
  const existing = await collection.find({ _id: { $in: ids } });
793
800
  const state = /* @__PURE__ */ new Map();
794
801
  for (const doc of existing) {
@@ -797,7 +804,7 @@ async function hydrate(collection, docs, now) {
797
804
  const fresh = [];
798
805
  let updated = 0;
799
806
  let skipped = 0;
800
- for (const doc of docs) {
807
+ for (const doc of deduped) {
801
808
  const id = doc._id;
802
809
  if (!state.has(id)) {
803
810
  fresh.push(stampSynced(doc, now));
@@ -815,6 +822,25 @@ async function hydrate(collection, docs, now) {
815
822
  if (fresh.length > 0) await collection.insertMany(fresh);
816
823
  return { inserted: fresh.length, updated, skipped };
817
824
  }
825
+ function badDocId(value, collectionName) {
826
+ const where = collectionName ? ` for collection '${collectionName}'` : "";
827
+ if (value === void 0 || value === null || value === "") {
828
+ return new Error(
829
+ `[@taladb/react/query] A document in the response${where} has no \`_id\`. \`data\` is resolved from the local collection, not from what \`queryFn\` returned, so every document needs one. Map your rows in \`queryFn\`: \`rows.map((row) => ({ ...row, _id: deriveDocId(collection, String(row.id)) }))\`.`
830
+ );
831
+ }
832
+ if (typeof value !== "string") {
833
+ return new Error(
834
+ `[@taladb/react/query] \`_id\` must be a string${where}, got ${typeof value}. Derive one from your natural key with \`deriveDocId(collection, String(row.id))\`.`
835
+ );
836
+ }
837
+ return new Error(
838
+ `[@taladb/react/query] \`_id\` ${JSON.stringify(value)}${where} is not a ULID, so the engine cannot store it \u2014 ids are 16 raw bytes on disk. This is what a natural key (a numeric id, a slug, a UUID) looks like when it is passed straight through from the server. Fold it into a ULID in \`queryFn\` instead:
839
+ import { deriveDocId } from 'taladb'
840
+ rows.map((row) => ({ ...row, _id: deriveDocId(${collectionName ? `'${collectionName}'` : "collection"}, String(row.id)) }))
841
+ The same key always maps to the same id, which is what keeps refetches idempotent.`
842
+ );
843
+ }
818
844
  function warnOnReservedFields(doc) {
819
845
  const reserved = reservedFieldsIn(doc);
820
846
  if (reserved.length === 0) return;
@@ -1257,7 +1283,7 @@ function useQuery(options) {
1257
1283
  queryKey: effectiveKey,
1258
1284
  documents: documentsRef.current
1259
1285
  });
1260
- await hydrate(collection, docs, fetchedAt);
1286
+ await hydrate(collection, docs, fetchedAt, name);
1261
1287
  const nextIds = docs.map((doc) => doc._id);
1262
1288
  const payload2 = shape3 === "envelope" && assembleRef.current !== void 0 ? raw : void 0;
1263
1289
  await writeQueryRecord(
@@ -4,7 +4,7 @@ import {
4
4
  useCollectionOptions,
5
5
  useFind,
6
6
  useTalaDB
7
- } from "../chunk-SEH233OC.mjs";
7
+ } from "../chunk-RAGFVMZN.mjs";
8
8
 
9
9
  // src/query/context.tsx
10
10
  import {
@@ -643,18 +643,25 @@ function describe(queryKey) {
643
643
  }
644
644
 
645
645
  // src/query/hydrate.ts
646
- async function hydrate(collection, docs, now) {
646
+ import { isDocId } from "taladb";
647
+ var hydrationChains = /* @__PURE__ */ new WeakMap();
648
+ function hydrate(collection, docs, now, collectionName) {
649
+ const key = collection;
650
+ const previous = hydrationChains.get(key) ?? Promise.resolve();
651
+ const run = previous.then(() => hydrateOnce(collection, docs, now, collectionName));
652
+ hydrationChains.set(key, run.catch(() => void 0));
653
+ return run;
654
+ }
655
+ async function hydrateOnce(collection, docs, now, collectionName) {
647
656
  if (docs.length === 0) return { inserted: 0, updated: 0, skipped: 0 };
648
- const ids = [];
657
+ const unique = /* @__PURE__ */ new Map();
649
658
  for (const doc of docs) {
650
- if (typeof doc._id !== "string" || doc._id === "") {
651
- throw new Error(
652
- "A hydrated document has no `_id`. Client ids are authoritative: the endpoint must echo back the `_id` it was sent, and must not assign its own."
653
- );
654
- }
655
- ids.push(doc._id);
659
+ if (!isDocId(doc._id)) throw badDocId(doc._id, collectionName);
656
660
  if (process.env.NODE_ENV !== "production") warnOnReservedFields(doc);
661
+ unique.set(doc._id, doc);
657
662
  }
663
+ const deduped = [...unique.values()];
664
+ const ids = [...unique.keys()];
658
665
  const existing = await collection.find({ _id: { $in: ids } });
659
666
  const state = /* @__PURE__ */ new Map();
660
667
  for (const doc of existing) {
@@ -663,7 +670,7 @@ async function hydrate(collection, docs, now) {
663
670
  const fresh = [];
664
671
  let updated = 0;
665
672
  let skipped = 0;
666
- for (const doc of docs) {
673
+ for (const doc of deduped) {
667
674
  const id = doc._id;
668
675
  if (!state.has(id)) {
669
676
  fresh.push(stampSynced(doc, now));
@@ -681,6 +688,25 @@ async function hydrate(collection, docs, now) {
681
688
  if (fresh.length > 0) await collection.insertMany(fresh);
682
689
  return { inserted: fresh.length, updated, skipped };
683
690
  }
691
+ function badDocId(value, collectionName) {
692
+ const where = collectionName ? ` for collection '${collectionName}'` : "";
693
+ if (value === void 0 || value === null || value === "") {
694
+ return new Error(
695
+ `[@taladb/react/query] A document in the response${where} has no \`_id\`. \`data\` is resolved from the local collection, not from what \`queryFn\` returned, so every document needs one. Map your rows in \`queryFn\`: \`rows.map((row) => ({ ...row, _id: deriveDocId(collection, String(row.id)) }))\`.`
696
+ );
697
+ }
698
+ if (typeof value !== "string") {
699
+ return new Error(
700
+ `[@taladb/react/query] \`_id\` must be a string${where}, got ${typeof value}. Derive one from your natural key with \`deriveDocId(collection, String(row.id))\`.`
701
+ );
702
+ }
703
+ return new Error(
704
+ `[@taladb/react/query] \`_id\` ${JSON.stringify(value)}${where} is not a ULID, so the engine cannot store it \u2014 ids are 16 raw bytes on disk. This is what a natural key (a numeric id, a slug, a UUID) looks like when it is passed straight through from the server. Fold it into a ULID in \`queryFn\` instead:
705
+ import { deriveDocId } from 'taladb'
706
+ rows.map((row) => ({ ...row, _id: deriveDocId(${collectionName ? `'${collectionName}'` : "collection"}, String(row.id)) }))
707
+ The same key always maps to the same id, which is what keeps refetches idempotent.`
708
+ );
709
+ }
684
710
  function warnOnReservedFields(doc) {
685
711
  const reserved = reservedFieldsIn(doc);
686
712
  if (reserved.length === 0) return;
@@ -1123,7 +1149,7 @@ function useQuery(options) {
1123
1149
  queryKey: effectiveKey,
1124
1150
  documents: documentsRef.current
1125
1151
  });
1126
- await hydrate(collection, docs, fetchedAt);
1152
+ await hydrate(collection, docs, fetchedAt, name);
1127
1153
  const nextIds = docs.map((doc) => doc._id);
1128
1154
  const payload2 = shape3 === "envelope" && assembleRef.current !== void 0 ? raw : void 0;
1129
1155
  await writeQueryRecord(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taladb/react",
3
- "version": "0.11.0",
3
+ "version": "0.11.2",
4
4
  "description": "React hooks for TalaDB — useFind, useFindOne, and useCollection for React and React Native",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -48,7 +48,7 @@
48
48
  },
49
49
  "peerDependencies": {
50
50
  "react": ">=18.0.0",
51
- "taladb": "^0.11.0"
51
+ "taladb": "^0.11.2"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@testing-library/react": "^16.0.0",
@@ -60,7 +60,7 @@
60
60
  "tsup": "^8.0.0",
61
61
  "typescript": "^5.9.3",
62
62
  "vitest": "^3.2.0",
63
- "taladb": "0.11.0"
63
+ "taladb": "0.11.2"
64
64
  },
65
65
  "scripts": {
66
66
  "build": "tsup",