@taladb/react 0.11.0 → 0.11.1
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/dist/{chunk-SEH233OC.mjs → chunk-RAGFVMZN.mjs} +44 -9
- package/dist/index.js +44 -9
- package/dist/index.mjs +1 -1
- package/dist/query/index.d.mts +1 -16
- package/dist/query/index.d.ts +1 -16
- package/dist/query/index.js +36 -10
- package/dist/query/index.mjs +37 -11
- package/package.json +3 -3
|
@@ -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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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
|
-
|
|
93
|
-
|
|
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
|
-
|
|
140
|
+
releaseDatabase(name, optionsKey);
|
|
106
141
|
setDb(null);
|
|
107
142
|
};
|
|
108
143
|
}, [name, optionsKey]);
|
package/dist/index.mjs
CHANGED
package/dist/query/index.d.mts
CHANGED
|
@@ -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.
|
package/dist/query/index.d.ts
CHANGED
|
@@ -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.
|
package/dist/query/index.js
CHANGED
|
@@ -777,18 +777,25 @@ function describe(queryKey) {
|
|
|
777
777
|
}
|
|
778
778
|
|
|
779
779
|
// src/query/hydrate.ts
|
|
780
|
-
|
|
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
|
|
791
|
+
const unique = /* @__PURE__ */ new Map();
|
|
783
792
|
for (const doc of docs) {
|
|
784
|
-
if (
|
|
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
|
|
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(
|
package/dist/query/index.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
useCollectionOptions,
|
|
5
5
|
useFind,
|
|
6
6
|
useTalaDB
|
|
7
|
-
} from "../chunk-
|
|
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
|
-
|
|
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
|
|
657
|
+
const unique = /* @__PURE__ */ new Map();
|
|
649
658
|
for (const doc of docs) {
|
|
650
|
-
if (
|
|
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
|
|
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.
|
|
3
|
+
"version": "0.11.1",
|
|
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.
|
|
51
|
+
"taladb": "^0.11.1"
|
|
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.
|
|
63
|
+
"taladb": "0.11.1"
|
|
64
64
|
},
|
|
65
65
|
"scripts": {
|
|
66
66
|
"build": "tsup",
|