@taladb/react 0.10.2 → 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/LICENSE-MIT +21 -0
- package/README.md +187 -0
- package/dist/chunk-RAGFVMZN.mjs +164 -0
- package/dist/index.d.mts +40 -309
- package/dist/index.d.ts +40 -309
- package/dist/index.js +67 -584
- package/dist/index.mjs +32 -707
- package/dist/query/index.d.mts +1649 -0
- package/dist/query/index.d.ts +1649 -0
- package/dist/query/index.js +2252 -0
- package/dist/query/index.mjs +2117 -0
- package/package.json +14 -5
- /package/{LICENSE → LICENSE-APACHE} +0 -0
|
@@ -0,0 +1,2252 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
"use strict";
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/query/index.ts
|
|
22
|
+
var query_exports = {};
|
|
23
|
+
__export(query_exports, {
|
|
24
|
+
DEFAULT_METHODS: () => DEFAULT_METHODS,
|
|
25
|
+
DEFAULT_RETRY: () => DEFAULT_RETRY,
|
|
26
|
+
DEFAULT_STALE_TIME: () => DEFAULT_STALE_TIME,
|
|
27
|
+
ENVELOPE_FIELDS: () => ENVELOPE_FIELDS,
|
|
28
|
+
ENVELOPE_INDEXES: () => ENVELOPE_INDEXES,
|
|
29
|
+
QUERY_COLLECTION: () => QUERY_COLLECTION,
|
|
30
|
+
QueryProvider: () => QueryProvider,
|
|
31
|
+
backoffMs: () => backoffMs,
|
|
32
|
+
cancelShared: () => cancelShared,
|
|
33
|
+
checkClassification: () => checkClassification,
|
|
34
|
+
checkMutationFilter: () => checkMutationFilter,
|
|
35
|
+
checkResponseBody: () => checkResponseBody,
|
|
36
|
+
checkResponseId: () => checkResponseId,
|
|
37
|
+
coalesce: () => coalesce,
|
|
38
|
+
contains: () => contains,
|
|
39
|
+
defaultClassify: () => defaultClassify,
|
|
40
|
+
defineParams: () => defineParams,
|
|
41
|
+
deleteQueryRecord: () => deleteQueryRecord,
|
|
42
|
+
drainOnce: () => drainOnce,
|
|
43
|
+
ensureEnvelopeIndexes: () => ensureEnvelopeIndexes,
|
|
44
|
+
eq: () => eq,
|
|
45
|
+
extractDocuments: () => extractDocuments,
|
|
46
|
+
gt: () => gt,
|
|
47
|
+
gte: () => gte,
|
|
48
|
+
hashQueryKey: () => hashQueryKey,
|
|
49
|
+
hydrate: () => hydrate,
|
|
50
|
+
inflightKey: () => inflightKey,
|
|
51
|
+
isBoundParams: () => isBoundParams,
|
|
52
|
+
isDev: () => isDev2,
|
|
53
|
+
isOffline: () => isOffline,
|
|
54
|
+
isShared: () => isShared,
|
|
55
|
+
isStale: () => isStale,
|
|
56
|
+
keepPreviousData: () => keepPreviousData,
|
|
57
|
+
lt: () => lt,
|
|
58
|
+
lte: () => lte,
|
|
59
|
+
ne: () => ne,
|
|
60
|
+
newDocId: () => newDocId,
|
|
61
|
+
normalizeParams: () => normalizeParams,
|
|
62
|
+
noteIgnoredOptions: () => noteIgnoredOptions,
|
|
63
|
+
oneOf: () => oneOf,
|
|
64
|
+
openQueryCollection: () => openQueryCollection,
|
|
65
|
+
queryRecordId: () => queryRecordId,
|
|
66
|
+
readQueryRecord: () => readQueryRecord,
|
|
67
|
+
replaceEqualDeep: () => replaceEqualDeep,
|
|
68
|
+
reservedFieldsIn: () => reservedFieldsIn,
|
|
69
|
+
resetCollectionWarnings: () => resetWarnings,
|
|
70
|
+
resetForgetTimers: () => resetForgetTimers,
|
|
71
|
+
resetInflight: () => resetInflight,
|
|
72
|
+
resetMutationScopes: () => resetMutationScopes,
|
|
73
|
+
resetWarnings: () => resetWarnings,
|
|
74
|
+
resolveCollectionName: () => resolveCollectionName,
|
|
75
|
+
resolveUrl: () => resolveUrl,
|
|
76
|
+
retryDelayMs: () => retryDelayMs,
|
|
77
|
+
runShared: () => runShared,
|
|
78
|
+
sendWrite: () => sendWrite,
|
|
79
|
+
shape: () => shape,
|
|
80
|
+
shouldRetry: () => shouldRetry,
|
|
81
|
+
stampPending: () => stampPending,
|
|
82
|
+
stampSynced: () => stampSynced,
|
|
83
|
+
toPendingWrite: () => toPendingWrite,
|
|
84
|
+
useMutation: () => useMutation,
|
|
85
|
+
useQuery: () => useQuery,
|
|
86
|
+
useQueryContext: () => useQueryContext,
|
|
87
|
+
useSyncStatus: () => useSyncStatus,
|
|
88
|
+
warnOnce: () => warnOnce,
|
|
89
|
+
writeConfirmed: () => writeConfirmed,
|
|
90
|
+
writeLocal: () => writeLocal,
|
|
91
|
+
writeQueryRecord: () => writeQueryRecord
|
|
92
|
+
});
|
|
93
|
+
module.exports = __toCommonJS(query_exports);
|
|
94
|
+
|
|
95
|
+
// src/query/context.tsx
|
|
96
|
+
var import_react2 = require("react");
|
|
97
|
+
|
|
98
|
+
// src/context.tsx
|
|
99
|
+
var import_react = require("react");
|
|
100
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
101
|
+
var TalaDBContext = (0, import_react.createContext)(null);
|
|
102
|
+
var CollectionOptionsContext = (0, import_react.createContext)({
|
|
103
|
+
get: () => void 0,
|
|
104
|
+
names: () => []
|
|
105
|
+
});
|
|
106
|
+
function useCollectionOptions() {
|
|
107
|
+
return (0, import_react.useContext)(CollectionOptionsContext);
|
|
108
|
+
}
|
|
109
|
+
function useTalaDB() {
|
|
110
|
+
const db = (0, import_react.useContext)(TalaDBContext);
|
|
111
|
+
if (db === null) {
|
|
112
|
+
throw new Error('useTalaDB must be used inside <TalaDBProvider db={...}> or <TalaDBProvider name="...">');
|
|
113
|
+
}
|
|
114
|
+
return db;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/query/envelope.ts
|
|
118
|
+
var ENVELOPE_FIELDS = [
|
|
119
|
+
"_sync",
|
|
120
|
+
"_op",
|
|
121
|
+
"_attempt",
|
|
122
|
+
"_error",
|
|
123
|
+
"_fetched_at",
|
|
124
|
+
"_retry_at",
|
|
125
|
+
"_endpoint",
|
|
126
|
+
"_method",
|
|
127
|
+
"_revision"
|
|
128
|
+
];
|
|
129
|
+
var ENVELOPE_INDEXES = ["_sync", "_changed_at"];
|
|
130
|
+
var revisionCounter = 0;
|
|
131
|
+
function nextRevision() {
|
|
132
|
+
revisionCounter = (revisionCounter + 1) % Number.MAX_SAFE_INTEGER;
|
|
133
|
+
return `${Date.now().toString(36)}-${revisionCounter.toString(36)}`;
|
|
134
|
+
}
|
|
135
|
+
function reservedFieldsIn(doc) {
|
|
136
|
+
return ENVELOPE_FIELDS.filter((field) => field in doc);
|
|
137
|
+
}
|
|
138
|
+
function stampSynced(doc, now) {
|
|
139
|
+
return {
|
|
140
|
+
...doc,
|
|
141
|
+
_sync: "synced",
|
|
142
|
+
_attempt: 0,
|
|
143
|
+
_error: null,
|
|
144
|
+
_fetched_at: now,
|
|
145
|
+
_retry_at: 0,
|
|
146
|
+
_revision: null
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function stampPending(doc, op, previous) {
|
|
150
|
+
return {
|
|
151
|
+
...doc,
|
|
152
|
+
_sync: "pending",
|
|
153
|
+
_op: op,
|
|
154
|
+
_attempt: 0,
|
|
155
|
+
_error: null,
|
|
156
|
+
_fetched_at: previous?._fetched_at ?? 0,
|
|
157
|
+
// Eligible immediately; the drain pushes this forward when it backs off.
|
|
158
|
+
_retry_at: 0,
|
|
159
|
+
_revision: nextRevision()
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function coalesce(pending, next) {
|
|
163
|
+
if (pending === void 0) return next;
|
|
164
|
+
if (pending === "insert") return next === "delete" ? null : "insert";
|
|
165
|
+
if (pending === "update") return next === "delete" ? "delete" : "update";
|
|
166
|
+
return "delete";
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// src/query/canonical.ts
|
|
170
|
+
var ENGINE_FIELDS = /* @__PURE__ */ new Set(["_id", "_changed_at"]);
|
|
171
|
+
function withoutEnvelope(doc) {
|
|
172
|
+
const application = {};
|
|
173
|
+
for (const [field, value] of Object.entries(doc)) {
|
|
174
|
+
if (!ENVELOPE_FIELDS.includes(field)) application[field] = value;
|
|
175
|
+
}
|
|
176
|
+
return application;
|
|
177
|
+
}
|
|
178
|
+
async function applyCanonical(collection, current, canonical, now) {
|
|
179
|
+
const id = canonical._id;
|
|
180
|
+
const stamped = stampSynced(canonical, now);
|
|
181
|
+
if (current === null) {
|
|
182
|
+
await collection.insert(stamped);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const fields = { ...stamped };
|
|
186
|
+
delete fields._id;
|
|
187
|
+
delete fields._changed_at;
|
|
188
|
+
const unset = {};
|
|
189
|
+
for (const field of Object.keys(current)) {
|
|
190
|
+
if (!ENGINE_FIELDS.has(field) && !(field in fields)) unset[field] = true;
|
|
191
|
+
}
|
|
192
|
+
const update = { $set: fields };
|
|
193
|
+
if (Object.keys(unset).length > 0) update.$unset = unset;
|
|
194
|
+
await collection.updateOne({ _id: id }, update);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// src/query/endpoint.ts
|
|
198
|
+
var DEFAULT_METHODS = {
|
|
199
|
+
insert: "POST",
|
|
200
|
+
update: "PUT",
|
|
201
|
+
delete: "DELETE"
|
|
202
|
+
};
|
|
203
|
+
function resolveUrl(template, collection, id, type) {
|
|
204
|
+
const withCollection = template.split(":collection").join(collection);
|
|
205
|
+
if (type === "insert") {
|
|
206
|
+
const trimmed = withCollection.replace(/\/:id\/?$/, "");
|
|
207
|
+
return trimmed.split(":id").join(id);
|
|
208
|
+
}
|
|
209
|
+
return withCollection.split(":id").join(id);
|
|
210
|
+
}
|
|
211
|
+
function defaultClassify(response) {
|
|
212
|
+
if (response.ok) return "ok";
|
|
213
|
+
if (response.status === 409) return "applied";
|
|
214
|
+
if (response.status >= 500 || response.status === 408 || response.status === 429) return "retry";
|
|
215
|
+
return "terminal";
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// src/query/validate.ts
|
|
219
|
+
var PREFIX = "[@taladb/react/query]";
|
|
220
|
+
var isDev = () => typeof process === "undefined" || process.env.NODE_ENV !== "production";
|
|
221
|
+
function checkResponseId(op, body, attempt) {
|
|
222
|
+
if (body === null) return;
|
|
223
|
+
const returned = body._id;
|
|
224
|
+
if (typeof returned !== "string" || returned === op.id) return;
|
|
225
|
+
console.warn(
|
|
226
|
+
attempt > 0 ? `${PREFIX} Contract rule 2 (idempotent writes): a retried ${op.type} on "${op.collection}" came back with a different id (sent ${op.id}, received ${returned}). The server treated the retry as a new record, so this write has just been duplicated. Make the endpoint upsert on the supplied \`_id\`.` : `${PREFIX} Contract rule 1 (client ids are authoritative): the endpoint for "${op.collection}" returned id ${returned} for a document sent as ${op.id}. Every write will duplicate on the server. Accept \`_id\` from the body and do not reassign it.`
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
function checkResponseBody(op, body) {
|
|
230
|
+
if (op.type === "delete" || body !== null) return;
|
|
231
|
+
console.warn(
|
|
232
|
+
`${PREFIX} Contract rule 4 (canonical response body): the endpoint for "${op.collection}" answered a ${op.type} with no JSON document. Server-set fields \u2014 timestamps, defaults, computed columns \u2014 will be missing locally until the next refetch, with no symptom in between. Return the stored document.`
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
function checkClassification(op, status, outcome) {
|
|
236
|
+
if (outcome === "retry" && status >= 400 && status < 500 && status !== 408 && status !== 429) {
|
|
237
|
+
console.warn(
|
|
238
|
+
`${PREFIX} Contract rule 3 (error classes): classify() returned "retry" for status ${status} on a ${op.type} to "${op.collection}". A 4xx will fail identically forever, so this write will be retried until the tab closes. Return "terminal" for a client error, or "applied" for a 409.`
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
if (outcome === "terminal" && status >= 500) {
|
|
242
|
+
console.warn(
|
|
243
|
+
`${PREFIX} Contract rule 3 (error classes): classify() returned "terminal" for status ${status}. A 5xx may recover; parking it as failed needs a human to retry what a back-off would have fixed.`
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function checkMutationFilter(where) {
|
|
248
|
+
if (where === null || typeof where !== "object") return;
|
|
249
|
+
const fields = Object.keys(where);
|
|
250
|
+
if (fields.length === 1 && fields[0] === "_id") return;
|
|
251
|
+
console.warn(
|
|
252
|
+
`${PREFIX} A mutation filter must be { _id }, not { ${fields.join(", ")} }. On a secondary browser tab a filter-based write is forwarded to the primary and re-evaluated there against authoritative data, so it can affect a different set of documents than the UI just showed.`
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
function warnDrainNotPrimary() {
|
|
256
|
+
console.warn(
|
|
257
|
+
`${PREFIX} A drain cycle was requested on a secondary tab and skipped. This is expected and harmless. Running it there would re-send writes every cycle, because a secondary reads a pending set that lags, and its own bookkeeping lands at the primary later.`
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// src/query/send.ts
|
|
262
|
+
async function sendWrite(backend, op, signal, attempt = 0, keepalive = false) {
|
|
263
|
+
const doFetch = backend.fetch ?? globalThis.fetch;
|
|
264
|
+
const headers = new Headers(backend.headers ? await backend.headers() : void 0);
|
|
265
|
+
const body = op.type === "delete" ? void 0 : JSON.stringify(op.document);
|
|
266
|
+
if (body !== void 0 && !headers.has("content-type")) {
|
|
267
|
+
headers.set("content-type", "application/json");
|
|
268
|
+
}
|
|
269
|
+
const response = await doFetch(backend.url(op), {
|
|
270
|
+
keepalive,
|
|
271
|
+
method: backend.method[op.type],
|
|
272
|
+
headers,
|
|
273
|
+
body,
|
|
274
|
+
signal
|
|
275
|
+
});
|
|
276
|
+
const outcome = backend.classify(response, op);
|
|
277
|
+
const document2 = await readDocument(response, outcome);
|
|
278
|
+
if (isDev()) {
|
|
279
|
+
checkClassification(op, response.status, outcome);
|
|
280
|
+
if (outcome === "ok") {
|
|
281
|
+
checkResponseId(op, document2, attempt);
|
|
282
|
+
checkResponseBody(op, document2);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return { outcome, document: document2, status: response.status };
|
|
286
|
+
}
|
|
287
|
+
async function readDocument(response, outcome) {
|
|
288
|
+
if (outcome !== "ok") return null;
|
|
289
|
+
try {
|
|
290
|
+
const parsed = await response.json();
|
|
291
|
+
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
292
|
+
} catch {
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
function toPendingWrite(collection, id, type, document2) {
|
|
297
|
+
return { collection, id, type, document: type === "delete" ? null : document2 };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// src/query/drain.ts
|
|
301
|
+
var EMPTY = {
|
|
302
|
+
sent: 0,
|
|
303
|
+
synced: 0,
|
|
304
|
+
deferred: 0,
|
|
305
|
+
failed: 0,
|
|
306
|
+
superseded: 0,
|
|
307
|
+
skippedNotPrimary: false
|
|
308
|
+
};
|
|
309
|
+
function backoffMs(attempt, random = Math.random) {
|
|
310
|
+
const base = Math.min(1e3 * 2 ** Math.max(0, attempt - 1), 3e5);
|
|
311
|
+
const jitter = 1 + (random() - 0.5) / 2;
|
|
312
|
+
return Math.round(base * jitter);
|
|
313
|
+
}
|
|
314
|
+
async function drainOnce(deps) {
|
|
315
|
+
const { db, backend, collections } = deps;
|
|
316
|
+
const now = deps.now ?? Date.now;
|
|
317
|
+
const random = deps.random ?? Math.random;
|
|
318
|
+
const batch = deps.batch ?? 25;
|
|
319
|
+
if (!(await db.isPrimary?.() ?? true)) {
|
|
320
|
+
if (isDev()) warnDrainNotPrimary();
|
|
321
|
+
return { ...EMPTY, skippedNotPrimary: true };
|
|
322
|
+
}
|
|
323
|
+
const stats = { ...EMPTY };
|
|
324
|
+
for (const name of collections()) {
|
|
325
|
+
const collection = db.collection(name);
|
|
326
|
+
const due = await collection.aggregate([
|
|
327
|
+
{ $match: { _sync: "pending", _retry_at: { $lte: now() } } },
|
|
328
|
+
{ $sort: { _changed_at: 1 } },
|
|
329
|
+
{ $limit: batch }
|
|
330
|
+
]);
|
|
331
|
+
await Promise.all(due.map((doc) => sendOne(collection, name, doc, stats, deps, now, random)));
|
|
332
|
+
}
|
|
333
|
+
return stats;
|
|
334
|
+
}
|
|
335
|
+
async function sendOne(collection, name, doc, stats, deps, now, random) {
|
|
336
|
+
const id = doc._id;
|
|
337
|
+
const op = doc._op;
|
|
338
|
+
const sentRevision = doc._revision;
|
|
339
|
+
const backend = routeFor(doc, name, id, op, deps.backend);
|
|
340
|
+
if (backend === null) {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
stats.sent++;
|
|
344
|
+
let result;
|
|
345
|
+
try {
|
|
346
|
+
const attempt = doc._attempt ?? 0;
|
|
347
|
+
result = await sendWrite(
|
|
348
|
+
backend,
|
|
349
|
+
toPendingWrite(name, id, op, withoutEnvelope(doc)),
|
|
350
|
+
void 0,
|
|
351
|
+
attempt,
|
|
352
|
+
deps.keepalive ?? false
|
|
353
|
+
);
|
|
354
|
+
} catch {
|
|
355
|
+
await defer(collection, id, doc, stats, now, random);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
const current = await collection.findOne({ _id: id });
|
|
359
|
+
if (current === null) return;
|
|
360
|
+
if (current._revision !== sentRevision) {
|
|
361
|
+
stats.superseded++;
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
switch (result.outcome) {
|
|
365
|
+
case "ok":
|
|
366
|
+
case "applied": {
|
|
367
|
+
if (op === "delete") {
|
|
368
|
+
await collection.deleteOne({ _id: id });
|
|
369
|
+
} else {
|
|
370
|
+
const responseDocument = result.document;
|
|
371
|
+
const canonical = {
|
|
372
|
+
...responseDocument && Object.keys(responseDocument).length > 0 ? responseDocument : withoutEnvelope(current),
|
|
373
|
+
_id: id
|
|
374
|
+
};
|
|
375
|
+
await applyCanonical(collection, current, canonical, now());
|
|
376
|
+
}
|
|
377
|
+
stats.synced++;
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
case "retry":
|
|
381
|
+
await defer(collection, id, doc, stats, now, random);
|
|
382
|
+
return;
|
|
383
|
+
case "terminal":
|
|
384
|
+
await collection.updateOne(
|
|
385
|
+
{ _id: id },
|
|
386
|
+
{
|
|
387
|
+
$set: {
|
|
388
|
+
_sync: "failed",
|
|
389
|
+
_error: `The server rejected this ${op} with status ${result.status}.`
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
);
|
|
393
|
+
stats.failed++;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
async function defer(collection, id, doc, stats, now, random) {
|
|
397
|
+
const attempt = (doc._attempt ?? 0) + 1;
|
|
398
|
+
await collection.updateOne(
|
|
399
|
+
{ _id: id },
|
|
400
|
+
{ $set: { _attempt: attempt, _retry_at: now() + backoffMs(attempt, random) } }
|
|
401
|
+
);
|
|
402
|
+
stats.deferred++;
|
|
403
|
+
}
|
|
404
|
+
function routeFor(doc, collection, id, op, fallback) {
|
|
405
|
+
const template = doc._endpoint;
|
|
406
|
+
if (template === void 0) {
|
|
407
|
+
if (fallback === null) return null;
|
|
408
|
+
const candidate = fallback.url(toPendingWrite(collection, id, op, withoutEnvelope(doc)));
|
|
409
|
+
return candidate === "" ? null : { ...fallback, url: () => candidate };
|
|
410
|
+
}
|
|
411
|
+
const url = resolveUrl(template, collection, id, op);
|
|
412
|
+
const method = doc._method ?? fallback?.method[op] ?? "POST";
|
|
413
|
+
return {
|
|
414
|
+
url: () => url,
|
|
415
|
+
method: { insert: method, update: method, delete: method },
|
|
416
|
+
headers: fallback?.headers,
|
|
417
|
+
classify: fallback?.classify ?? defaultClassify,
|
|
418
|
+
fetch: fallback?.fetch
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// src/query/keys.ts
|
|
423
|
+
var import_taladb = require("taladb");
|
|
424
|
+
var QUERY_COLLECTION = "taladb_queries";
|
|
425
|
+
function hashQueryKey(key) {
|
|
426
|
+
return JSON.stringify(key, (_field, value) => normalise(value));
|
|
427
|
+
}
|
|
428
|
+
function normalise(value) {
|
|
429
|
+
if (value === void 0) return UNDEFINED_MARKER;
|
|
430
|
+
if (value === null || typeof value !== "object") {
|
|
431
|
+
if (typeof value === "function" || typeof value === "symbol") {
|
|
432
|
+
throw new TypeError(
|
|
433
|
+
`A query key may not contain a ${typeof value}. Keys must be JSON-shaped (primitives, arrays, plain objects) so that equal keys hash identically.`
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
return value;
|
|
437
|
+
}
|
|
438
|
+
if (!isPlainObject(value)) return value;
|
|
439
|
+
const sorted = {};
|
|
440
|
+
for (const field of Object.keys(value).sort()) {
|
|
441
|
+
sorted[field] = value[field];
|
|
442
|
+
}
|
|
443
|
+
return sorted;
|
|
444
|
+
}
|
|
445
|
+
var UNDEFINED_MARKER = "\0undefined";
|
|
446
|
+
function isPlainObject(value) {
|
|
447
|
+
const proto = Object.getPrototypeOf(value);
|
|
448
|
+
return proto === Object.prototype || proto === null;
|
|
449
|
+
}
|
|
450
|
+
function queryRecordId(collection, key, hash = hashQueryKey) {
|
|
451
|
+
return (0, import_taladb.deriveDocId)(QUERY_COLLECTION, `${collection}\0${hash(key)}`);
|
|
452
|
+
}
|
|
453
|
+
function newDocId(collection) {
|
|
454
|
+
return (0, import_taladb.deriveDocId)(collection, globalThis.crypto.randomUUID());
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// src/query/queries.ts
|
|
458
|
+
async function ensureEnvelopeIndexes(collection) {
|
|
459
|
+
for (const field of ENVELOPE_INDEXES) {
|
|
460
|
+
await collection.createIndex(field);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
async function openQueryCollection(db) {
|
|
464
|
+
const collection = db.collection(QUERY_COLLECTION);
|
|
465
|
+
await collection.createIndex("collection");
|
|
466
|
+
return collection;
|
|
467
|
+
}
|
|
468
|
+
async function readQueryRecord(queries, collection, key, hash) {
|
|
469
|
+
return queries.findOne({ _id: queryRecordId(collection, key, hash) });
|
|
470
|
+
}
|
|
471
|
+
async function deleteQueryRecord(queries, collection, key, hash) {
|
|
472
|
+
await queries.deleteOne({ _id: queryRecordId(collection, key, hash) });
|
|
473
|
+
}
|
|
474
|
+
async function writeQueryRecord(queries, collection, key, ids, now, ttl, shape2 = "array", payload, hash = hashQueryKey) {
|
|
475
|
+
const record = {
|
|
476
|
+
_id: queryRecordId(collection, key, hash),
|
|
477
|
+
collection,
|
|
478
|
+
key: hash(key),
|
|
479
|
+
ids,
|
|
480
|
+
fetchedAt: now,
|
|
481
|
+
ttl,
|
|
482
|
+
shape: shape2,
|
|
483
|
+
...payload === void 0 ? {} : { payload }
|
|
484
|
+
};
|
|
485
|
+
const replaced = await queries.updateOne(
|
|
486
|
+
{ _id: record._id },
|
|
487
|
+
{
|
|
488
|
+
$set: {
|
|
489
|
+
ids,
|
|
490
|
+
fetchedAt: now,
|
|
491
|
+
ttl,
|
|
492
|
+
collection,
|
|
493
|
+
key: record.key,
|
|
494
|
+
shape: shape2,
|
|
495
|
+
// Written unconditionally so a query that stops returning an envelope
|
|
496
|
+
// does not keep serving the previous response's `total` forever.
|
|
497
|
+
payload: payload ?? null
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
);
|
|
501
|
+
if (!replaced) await queries.insert(record);
|
|
502
|
+
return record;
|
|
503
|
+
}
|
|
504
|
+
function isStale(record, now) {
|
|
505
|
+
return now >= record.fetchedAt + record.ttl;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// src/query/context.tsx
|
|
509
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
510
|
+
var QueryContext = (0, import_react2.createContext)(null);
|
|
511
|
+
function useQueryContext() {
|
|
512
|
+
const value = (0, import_react2.useContext)(QueryContext);
|
|
513
|
+
if (value === null) {
|
|
514
|
+
throw new Error(
|
|
515
|
+
"A @taladb/react/query hook was used outside <QueryProvider>. Wrap the tree in <TalaDBProvider><QueryProvider \u2026>\u2026</QueryProvider></TalaDBProvider>."
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
return value;
|
|
519
|
+
}
|
|
520
|
+
function QueryProvider({
|
|
521
|
+
children,
|
|
522
|
+
headers,
|
|
523
|
+
classify,
|
|
524
|
+
fetch,
|
|
525
|
+
drain,
|
|
526
|
+
resolveCollection,
|
|
527
|
+
defaultOptions
|
|
528
|
+
}) {
|
|
529
|
+
const db = useTalaDB();
|
|
530
|
+
const [queries, setQueries] = (0, import_react2.useState)(null);
|
|
531
|
+
const liveRef = (0, import_react2.useRef)(true);
|
|
532
|
+
(0, import_react2.useEffect)(() => {
|
|
533
|
+
liveRef.current = true;
|
|
534
|
+
let cancelled = false;
|
|
535
|
+
void openQueryCollection(db).then((collection) => {
|
|
536
|
+
if (!cancelled) setQueries(collection);
|
|
537
|
+
}).catch((error) => {
|
|
538
|
+
console.error("[@taladb/react/query] Failed to open the query-record collection.", error);
|
|
539
|
+
});
|
|
540
|
+
return () => {
|
|
541
|
+
cancelled = true;
|
|
542
|
+
liveRef.current = false;
|
|
543
|
+
};
|
|
544
|
+
}, [db]);
|
|
545
|
+
const backend = (0, import_react2.useMemo)(
|
|
546
|
+
() => ({
|
|
547
|
+
url: () => "",
|
|
548
|
+
method: DEFAULT_METHODS,
|
|
549
|
+
headers,
|
|
550
|
+
classify: classify ?? defaultClassify,
|
|
551
|
+
fetch
|
|
552
|
+
}),
|
|
553
|
+
[headers, classify, fetch]
|
|
554
|
+
);
|
|
555
|
+
const defaults = defaultOptions?.queries;
|
|
556
|
+
const policy = drain?.policy ?? "auto";
|
|
557
|
+
const intervalMs = drain?.intervalMs;
|
|
558
|
+
const batch = drain?.batch;
|
|
559
|
+
const flushAt = drain?.flushAt ?? 100;
|
|
560
|
+
const flushOnHide = drain?.flushOnHide ?? true;
|
|
561
|
+
const managedRef = (0, import_react2.useRef)(/* @__PURE__ */ new Set());
|
|
562
|
+
const [collections, setCollections] = (0, import_react2.useState)([]);
|
|
563
|
+
const register = (0, import_react2.useCallback)((collection) => {
|
|
564
|
+
if (managedRef.current.has(collection)) return;
|
|
565
|
+
managedRef.current.add(collection);
|
|
566
|
+
setCollections([...managedRef.current]);
|
|
567
|
+
}, []);
|
|
568
|
+
const runningRef = (0, import_react2.useRef)(false);
|
|
569
|
+
const queuedSinceDrainRef = (0, import_react2.useRef)(0);
|
|
570
|
+
const [draining, setDraining] = (0, import_react2.useState)(false);
|
|
571
|
+
const runDrain = (0, import_react2.useCallback)(async (keepalive = false) => {
|
|
572
|
+
if (runningRef.current) return;
|
|
573
|
+
if (managedRef.current.size === 0) return;
|
|
574
|
+
runningRef.current = true;
|
|
575
|
+
queuedSinceDrainRef.current = 0;
|
|
576
|
+
setDraining(true);
|
|
577
|
+
try {
|
|
578
|
+
await drainOnce({
|
|
579
|
+
db,
|
|
580
|
+
backend,
|
|
581
|
+
collections: () => [...managedRef.current],
|
|
582
|
+
batch,
|
|
583
|
+
keepalive
|
|
584
|
+
});
|
|
585
|
+
} catch (error) {
|
|
586
|
+
console.error("[@taladb/react/query] Drain cycle failed.", error);
|
|
587
|
+
} finally {
|
|
588
|
+
runningRef.current = false;
|
|
589
|
+
setDraining(false);
|
|
590
|
+
}
|
|
591
|
+
}, [db, backend, batch]);
|
|
592
|
+
const requestDrain = (0, import_react2.useCallback)(
|
|
593
|
+
(force = false) => {
|
|
594
|
+
queuedSinceDrainRef.current += 1;
|
|
595
|
+
const overThreshold = flushAt !== false && queuedSinceDrainRef.current >= flushAt;
|
|
596
|
+
if (!force && !overThreshold) {
|
|
597
|
+
if (policy === "manual") return;
|
|
598
|
+
if (policy === "interval") return;
|
|
599
|
+
if (policy === "online-only" && typeof navigator !== "undefined" && !navigator.onLine) {
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
if (overThreshold && !force && policy === "manual") return;
|
|
604
|
+
void runDrain();
|
|
605
|
+
},
|
|
606
|
+
[policy, flushAt, runDrain]
|
|
607
|
+
);
|
|
608
|
+
const noteWrite = (0, import_react2.useCallback)(() => {
|
|
609
|
+
queuedSinceDrainRef.current += 1;
|
|
610
|
+
if (flushAt === false || queuedSinceDrainRef.current < flushAt) return;
|
|
611
|
+
if (policy === "manual") return;
|
|
612
|
+
if (typeof navigator !== "undefined" && navigator.onLine === false) return;
|
|
613
|
+
void runDrain();
|
|
614
|
+
}, [flushAt, policy, runDrain]);
|
|
615
|
+
(0, import_react2.useEffect)(() => {
|
|
616
|
+
if (policy === "manual" || typeof window === "undefined") return;
|
|
617
|
+
const onOnline = () => void runDrain();
|
|
618
|
+
window.addEventListener("online", onOnline);
|
|
619
|
+
return () => window.removeEventListener("online", onOnline);
|
|
620
|
+
}, [policy, runDrain]);
|
|
621
|
+
(0, import_react2.useEffect)(() => {
|
|
622
|
+
if (policy !== "interval") return;
|
|
623
|
+
const id = setInterval(() => void runDrain(), intervalMs ?? 3e4);
|
|
624
|
+
return () => clearInterval(id);
|
|
625
|
+
}, [policy, intervalMs, runDrain]);
|
|
626
|
+
(0, import_react2.useEffect)(() => {
|
|
627
|
+
if (!flushOnHide || policy === "manual" || typeof window === "undefined") return;
|
|
628
|
+
const onHidden = () => {
|
|
629
|
+
if (typeof document !== "undefined" && document.visibilityState !== "hidden") return;
|
|
630
|
+
void runDrain();
|
|
631
|
+
};
|
|
632
|
+
const onPageHide = () => void runDrain(true);
|
|
633
|
+
document.addEventListener("visibilitychange", onHidden);
|
|
634
|
+
window.addEventListener("pagehide", onPageHide);
|
|
635
|
+
return () => {
|
|
636
|
+
document.removeEventListener("visibilitychange", onHidden);
|
|
637
|
+
window.removeEventListener("pagehide", onPageHide);
|
|
638
|
+
};
|
|
639
|
+
}, [flushOnHide, policy, runDrain]);
|
|
640
|
+
const value = (0, import_react2.useMemo)(
|
|
641
|
+
() => ({
|
|
642
|
+
queries,
|
|
643
|
+
backend,
|
|
644
|
+
drain: { policy, intervalMs, batch, flushAt, flushOnHide },
|
|
645
|
+
resolveCollection,
|
|
646
|
+
defaults,
|
|
647
|
+
register,
|
|
648
|
+
collections,
|
|
649
|
+
draining,
|
|
650
|
+
requestDrain,
|
|
651
|
+
noteWrite
|
|
652
|
+
}),
|
|
653
|
+
// Spread by field: an inline `drain={{…}}` object would otherwise produce a
|
|
654
|
+
// new context value on every render and re-run every consumer's effects.
|
|
655
|
+
[
|
|
656
|
+
queries,
|
|
657
|
+
backend,
|
|
658
|
+
policy,
|
|
659
|
+
intervalMs,
|
|
660
|
+
batch,
|
|
661
|
+
flushAt,
|
|
662
|
+
flushOnHide,
|
|
663
|
+
resolveCollection,
|
|
664
|
+
defaults,
|
|
665
|
+
register,
|
|
666
|
+
collections,
|
|
667
|
+
draining,
|
|
668
|
+
requestDrain,
|
|
669
|
+
noteWrite
|
|
670
|
+
]
|
|
671
|
+
);
|
|
672
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(QueryContext.Provider, { value, children });
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// src/query/useQuery.ts
|
|
676
|
+
var import_react5 = require("react");
|
|
677
|
+
|
|
678
|
+
// src/useCollection.ts
|
|
679
|
+
var import_react3 = require("react");
|
|
680
|
+
function useCollection(name, options) {
|
|
681
|
+
const db = useTalaDB();
|
|
682
|
+
const registry = useCollectionOptions();
|
|
683
|
+
const explicit = (0, import_react3.useRef)(options);
|
|
684
|
+
explicit.current = options;
|
|
685
|
+
return (0, import_react3.useMemo)(
|
|
686
|
+
() => db.collection(name, explicit.current ?? registry.get(name)),
|
|
687
|
+
[db, name, registry]
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// src/useFind.ts
|
|
692
|
+
var import_react4 = require("react");
|
|
693
|
+
function useFind(collection, filter) {
|
|
694
|
+
const snapshotRef = (0, import_react4.useRef)({ data: [], loading: true, error: null });
|
|
695
|
+
const filterKey = JSON.stringify(filter ?? null);
|
|
696
|
+
const subscribe = (0, import_react4.useCallback)(
|
|
697
|
+
(notify) => {
|
|
698
|
+
snapshotRef.current = { data: snapshotRef.current.data, loading: true, error: null };
|
|
699
|
+
return collection.subscribe(filter ?? {}, (docs) => {
|
|
700
|
+
snapshotRef.current = { data: docs, loading: false, error: null };
|
|
701
|
+
notify();
|
|
702
|
+
}, (error) => {
|
|
703
|
+
snapshotRef.current = { ...snapshotRef.current, loading: false, error };
|
|
704
|
+
notify();
|
|
705
|
+
});
|
|
706
|
+
},
|
|
707
|
+
// filterKey captures the serialised filter; collection is the identity dep.
|
|
708
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
709
|
+
[collection, filterKey]
|
|
710
|
+
);
|
|
711
|
+
const getSnapshot = (0, import_react4.useCallback)(() => snapshotRef.current, []);
|
|
712
|
+
return (0, import_react4.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// src/query/dev.ts
|
|
716
|
+
var seen = /* @__PURE__ */ new Set();
|
|
717
|
+
function isDev2() {
|
|
718
|
+
return typeof process !== "undefined" && process.env?.NODE_ENV !== "production";
|
|
719
|
+
}
|
|
720
|
+
function warnOnce(key, message) {
|
|
721
|
+
if (!isDev2() || seen.has(key)) return;
|
|
722
|
+
seen.add(key);
|
|
723
|
+
console.warn(`[@taladb/react/query] ${message}`);
|
|
724
|
+
}
|
|
725
|
+
function resetWarnings() {
|
|
726
|
+
seen.clear();
|
|
727
|
+
}
|
|
728
|
+
var IGNORED = {
|
|
729
|
+
notifyOnChangeProps: "is not implemented. It is a render optimisation with no effect on correctness; every result field is computed either way.",
|
|
730
|
+
structuralSharing: "is always on and cannot be disabled. Turning it off would make `data` a new reference on every database snapshot, and a live query produces those far more often than a polling cache does."
|
|
731
|
+
};
|
|
732
|
+
function noteIgnoredOptions(options) {
|
|
733
|
+
if (!isDev2()) return;
|
|
734
|
+
for (const [option, explanation] of Object.entries(IGNORED)) {
|
|
735
|
+
if (options[option] === void 0) continue;
|
|
736
|
+
warnOnce(`ignored:${option}`, `\`${option}\` ${explanation}`);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// src/query/collection.ts
|
|
741
|
+
function resolveCollectionName({
|
|
742
|
+
collection,
|
|
743
|
+
queryKey,
|
|
744
|
+
resolve,
|
|
745
|
+
registered
|
|
746
|
+
}) {
|
|
747
|
+
if (collection !== void 0) return collection;
|
|
748
|
+
const resolved = resolve?.(queryKey);
|
|
749
|
+
const inferred = resolved ?? (typeof queryKey[0] === "string" ? queryKey[0] : void 0);
|
|
750
|
+
if (inferred === void 0 || inferred === "") {
|
|
751
|
+
throw new Error(
|
|
752
|
+
`${describe(queryKey)} has no collection: its first key segment is not a string, so there is nothing to infer from. Pass \`collection: '\u2026'\` on the query, or a \`resolveCollection\` function to <QueryProvider>.`
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
if (registered.length === 0) {
|
|
756
|
+
warnOnce(
|
|
757
|
+
inferred,
|
|
758
|
+
`${describe(queryKey)} is storing documents in the '${inferred}' collection, inferred from its first key segment. No collections are registered on <TalaDBProvider>, so this could not be checked \u2014 register them to get schema validation and a real error on a typo.`
|
|
759
|
+
);
|
|
760
|
+
return inferred;
|
|
761
|
+
}
|
|
762
|
+
if (!registered.includes(inferred)) {
|
|
763
|
+
throw new Error(
|
|
764
|
+
`${describe(queryKey)} would store documents in a '${inferred}' collection, inferred from its first key segment, but the registered collections are: ${registered.join(", ")}. Either pass \`collection\` explicitly, or give <QueryProvider> a \`resolveCollection\` function if your query keys do not start with the collection name.`
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
return inferred;
|
|
768
|
+
}
|
|
769
|
+
function describe(queryKey) {
|
|
770
|
+
let rendered;
|
|
771
|
+
try {
|
|
772
|
+
rendered = JSON.stringify(queryKey);
|
|
773
|
+
} catch {
|
|
774
|
+
rendered = "[unserialisable key]";
|
|
775
|
+
}
|
|
776
|
+
return `useQuery(${rendered})`;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// src/query/hydrate.ts
|
|
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) {
|
|
790
|
+
if (docs.length === 0) return { inserted: 0, updated: 0, skipped: 0 };
|
|
791
|
+
const unique = /* @__PURE__ */ new Map();
|
|
792
|
+
for (const doc of docs) {
|
|
793
|
+
if (!(0, import_taladb2.isDocId)(doc._id)) throw badDocId(doc._id, collectionName);
|
|
794
|
+
if (process.env.NODE_ENV !== "production") warnOnReservedFields(doc);
|
|
795
|
+
unique.set(doc._id, doc);
|
|
796
|
+
}
|
|
797
|
+
const deduped = [...unique.values()];
|
|
798
|
+
const ids = [...unique.keys()];
|
|
799
|
+
const existing = await collection.find({ _id: { $in: ids } });
|
|
800
|
+
const state = /* @__PURE__ */ new Map();
|
|
801
|
+
for (const doc of existing) {
|
|
802
|
+
state.set(doc._id, doc);
|
|
803
|
+
}
|
|
804
|
+
const fresh = [];
|
|
805
|
+
let updated = 0;
|
|
806
|
+
let skipped = 0;
|
|
807
|
+
for (const doc of deduped) {
|
|
808
|
+
const id = doc._id;
|
|
809
|
+
if (!state.has(id)) {
|
|
810
|
+
fresh.push(stampSynced(doc, now));
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
const current = state.get(id);
|
|
814
|
+
const syncState = current._sync;
|
|
815
|
+
if (syncState !== void 0 && syncState !== "synced") {
|
|
816
|
+
skipped++;
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
819
|
+
await applyCanonical(collection, current, doc, now);
|
|
820
|
+
updated++;
|
|
821
|
+
}
|
|
822
|
+
if (fresh.length > 0) await collection.insertMany(fresh);
|
|
823
|
+
return { inserted: fresh.length, updated, skipped };
|
|
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
|
+
}
|
|
844
|
+
function warnOnReservedFields(doc) {
|
|
845
|
+
const reserved = reservedFieldsIn(doc);
|
|
846
|
+
if (reserved.length === 0) return;
|
|
847
|
+
console.warn(
|
|
848
|
+
`[@taladb/react/query] A server response carried reserved field(s) ${reserved.join(", ")} on document ` + String(doc._id) + ". These belong to the sync envelope; the response will overwrite them and the document can fall out of the pending queue. Rename the field(s) on the server."
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// src/query/shape.ts
|
|
853
|
+
function extractDocuments({
|
|
854
|
+
raw,
|
|
855
|
+
collection,
|
|
856
|
+
queryKey,
|
|
857
|
+
documents
|
|
858
|
+
}) {
|
|
859
|
+
if (documents !== void 0) {
|
|
860
|
+
const extracted = documents(raw);
|
|
861
|
+
if (!Array.isArray(extracted)) {
|
|
862
|
+
throw new Error(
|
|
863
|
+
`${describe2(queryKey)}: \`documents\` must return an array of documents, but returned ${typeName(extracted)}. It selects the part of the response that is stored in the '${collection}' collection.`
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
return { documents: extracted, shape: "envelope" };
|
|
867
|
+
}
|
|
868
|
+
if (Array.isArray(raw)) return { documents: raw, shape: "array" };
|
|
869
|
+
if (isDocumentLike(raw)) return { documents: [raw], shape: "document" };
|
|
870
|
+
throw new Error(
|
|
871
|
+
`${describe2(queryKey)}: \`queryFn\` returned ${typeName(raw)}, which is neither a document nor a list of documents.
|
|
872
|
+
|
|
873
|
+
Unlike TanStack Query, \`data\` here is resolved from the '` + collection + "' collection rather than from the response, so the response has to say which part of itself is documents. For an envelope like `{ items, total }`, add:\n\n documents: (raw) => raw.items,\n assemble: (docs, raw) => ({ ...raw, items: docs }),\n\nA document is an object with a string `_id`. If yours uses a different identifier, map it in `queryFn` before returning."
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
function isDocumentLike(raw) {
|
|
877
|
+
return typeof raw === "object" && raw !== null && !Array.isArray(raw) && typeof raw._id === "string";
|
|
878
|
+
}
|
|
879
|
+
function typeName(value) {
|
|
880
|
+
if (value === null) return "null";
|
|
881
|
+
if (Array.isArray(value)) return "an array";
|
|
882
|
+
if (typeof value === "object") {
|
|
883
|
+
const keys = Object.keys(value);
|
|
884
|
+
const shown = keys.slice(0, 4).join(", ");
|
|
885
|
+
return keys.length === 0 ? "an empty object" : `an object with keys { ${shown}${keys.length > 4 ? ", \u2026" : ""} }`;
|
|
886
|
+
}
|
|
887
|
+
return `a ${typeof value}`;
|
|
888
|
+
}
|
|
889
|
+
function describe2(queryKey) {
|
|
890
|
+
let rendered;
|
|
891
|
+
try {
|
|
892
|
+
rendered = JSON.stringify(queryKey);
|
|
893
|
+
} catch {
|
|
894
|
+
rendered = "[unserialisable key]";
|
|
895
|
+
}
|
|
896
|
+
return `useQuery(${rendered})`;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// src/query/inflight.ts
|
|
900
|
+
var inflight = /* @__PURE__ */ new Map();
|
|
901
|
+
function inflightKey(collection, keyHash) {
|
|
902
|
+
return `${collection}\0${keyHash}`;
|
|
903
|
+
}
|
|
904
|
+
function runShared(key, work, callerSignal) {
|
|
905
|
+
let entry = inflight.get(key);
|
|
906
|
+
if (entry === void 0) {
|
|
907
|
+
const controller = new AbortController();
|
|
908
|
+
const created = { controller, waiters: 0, promise: void 0 };
|
|
909
|
+
created.promise = work(controller.signal).finally(() => {
|
|
910
|
+
if (inflight.get(key) === created) inflight.delete(key);
|
|
911
|
+
});
|
|
912
|
+
inflight.set(key, created);
|
|
913
|
+
entry = created;
|
|
914
|
+
}
|
|
915
|
+
const joined = entry;
|
|
916
|
+
joined.waiters += 1;
|
|
917
|
+
let left = false;
|
|
918
|
+
const leave = () => {
|
|
919
|
+
if (left) return;
|
|
920
|
+
left = true;
|
|
921
|
+
joined.waiters -= 1;
|
|
922
|
+
if (joined.waiters === 0) joined.controller.abort();
|
|
923
|
+
};
|
|
924
|
+
callerSignal.addEventListener("abort", leave, { once: true });
|
|
925
|
+
return joined.promise.finally(() => {
|
|
926
|
+
if (!left) {
|
|
927
|
+
left = true;
|
|
928
|
+
joined.waiters -= 1;
|
|
929
|
+
}
|
|
930
|
+
callerSignal.removeEventListener("abort", leave);
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
function cancelShared(key) {
|
|
934
|
+
const entry = inflight.get(key);
|
|
935
|
+
if (entry === void 0) return;
|
|
936
|
+
inflight.delete(key);
|
|
937
|
+
entry.controller.abort();
|
|
938
|
+
}
|
|
939
|
+
function isShared(key) {
|
|
940
|
+
return inflight.has(key);
|
|
941
|
+
}
|
|
942
|
+
function resetInflight() {
|
|
943
|
+
for (const entry of inflight.values()) entry.controller.abort();
|
|
944
|
+
inflight.clear();
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// src/query/params.ts
|
|
948
|
+
var predicate = (field, op) => ({
|
|
949
|
+
kind: "predicate",
|
|
950
|
+
field,
|
|
951
|
+
op
|
|
952
|
+
});
|
|
953
|
+
var eq = (field) => predicate(field, "$eq");
|
|
954
|
+
var ne = (field) => predicate(field, "$ne");
|
|
955
|
+
var gte = (field) => predicate(field, "$gte");
|
|
956
|
+
var lte = (field) => predicate(field, "$lte");
|
|
957
|
+
var gt = (field) => predicate(field, "$gt");
|
|
958
|
+
var lt = (field) => predicate(field, "$lt");
|
|
959
|
+
var contains = (field) => predicate(field, "$contains");
|
|
960
|
+
var oneOf = (field) => predicate(field, "$in");
|
|
961
|
+
var shape = () => ({ kind: "shape" });
|
|
962
|
+
function defineParams(spec) {
|
|
963
|
+
return (values) => bindParams(spec, values);
|
|
964
|
+
}
|
|
965
|
+
function isBoundParams(value) {
|
|
966
|
+
return typeof value === "object" && value !== null && typeof value.toFilter === "function";
|
|
967
|
+
}
|
|
968
|
+
function normalizeParams(params) {
|
|
969
|
+
if (params === void 0) return void 0;
|
|
970
|
+
if (isBoundParams(params)) return params;
|
|
971
|
+
return bindParams({}, params);
|
|
972
|
+
}
|
|
973
|
+
function bindParams(spec, values) {
|
|
974
|
+
return {
|
|
975
|
+
values,
|
|
976
|
+
toSearchParams: () => {
|
|
977
|
+
const search = new URLSearchParams();
|
|
978
|
+
for (const [name, value] of Object.entries(values)) {
|
|
979
|
+
if (value === void 0 || value === null) continue;
|
|
980
|
+
if (Array.isArray(value)) {
|
|
981
|
+
if (value.length > 0) search.set(name, value.join(","));
|
|
982
|
+
continue;
|
|
983
|
+
}
|
|
984
|
+
search.set(name, String(value));
|
|
985
|
+
}
|
|
986
|
+
return search;
|
|
987
|
+
},
|
|
988
|
+
toFilter: () => buildFilter(spec, values),
|
|
989
|
+
shapeParams: () => Object.keys(values).filter(
|
|
990
|
+
(name) => spec[name]?.kind === "shape" && values[name] !== void 0
|
|
991
|
+
)
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
function buildFilter(spec, values) {
|
|
995
|
+
const clauses = [];
|
|
996
|
+
for (const [name, value] of Object.entries(values)) {
|
|
997
|
+
if (value === void 0 || value === null) continue;
|
|
998
|
+
const declared = spec[name];
|
|
999
|
+
if (declared === void 0 || declared.kind !== "predicate") continue;
|
|
1000
|
+
if (declared.field === void 0 || declared.op === void 0) continue;
|
|
1001
|
+
if (Array.isArray(value) && value.length === 0) continue;
|
|
1002
|
+
clauses.push({
|
|
1003
|
+
[declared.field]: declared.op === "$eq" ? value : { [declared.op]: value }
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
if (clauses.length === 0) return void 0;
|
|
1007
|
+
if (clauses.length === 1) return clauses[0];
|
|
1008
|
+
return { $and: clauses };
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// src/query/retry.ts
|
|
1012
|
+
var DEFAULT_RETRY = 3;
|
|
1013
|
+
function shouldRetry(retry, failureCount, error) {
|
|
1014
|
+
const setting = retry ?? DEFAULT_RETRY;
|
|
1015
|
+
if (typeof setting === "function") return setting(failureCount, error);
|
|
1016
|
+
if (typeof setting === "boolean") return setting;
|
|
1017
|
+
return failureCount <= setting;
|
|
1018
|
+
}
|
|
1019
|
+
function retryDelayMs(retryDelay, attemptIndex, error) {
|
|
1020
|
+
if (typeof retryDelay === "number") return retryDelay;
|
|
1021
|
+
if (typeof retryDelay === "function") return retryDelay(attemptIndex, error);
|
|
1022
|
+
return Math.min(1e3 * 2 ** attemptIndex, 3e4);
|
|
1023
|
+
}
|
|
1024
|
+
function sleep(ms, signal) {
|
|
1025
|
+
if (signal.aborted) return Promise.resolve();
|
|
1026
|
+
return new Promise((resolve) => {
|
|
1027
|
+
const timer = setTimeout(() => {
|
|
1028
|
+
signal.removeEventListener("abort", onAbort);
|
|
1029
|
+
resolve();
|
|
1030
|
+
}, ms);
|
|
1031
|
+
function onAbort() {
|
|
1032
|
+
clearTimeout(timer);
|
|
1033
|
+
resolve();
|
|
1034
|
+
}
|
|
1035
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
function isOffline() {
|
|
1039
|
+
return typeof navigator !== "undefined" && navigator.onLine === false;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// src/query/structural.ts
|
|
1043
|
+
function replaceEqualDeep(previous, next) {
|
|
1044
|
+
if (previous === next) return next;
|
|
1045
|
+
const bothArrays = Array.isArray(previous) && Array.isArray(next);
|
|
1046
|
+
if (bothArrays || isPlainObject2(previous) && isPlainObject2(next)) {
|
|
1047
|
+
const previousItems = previous;
|
|
1048
|
+
const nextItems = next;
|
|
1049
|
+
const previousKeys = Object.keys(previousItems);
|
|
1050
|
+
const nextKeys = Object.keys(nextItems);
|
|
1051
|
+
let equal = previousKeys.length === nextKeys.length;
|
|
1052
|
+
const merged = bothArrays ? [] : {};
|
|
1053
|
+
for (const key of nextKeys) {
|
|
1054
|
+
const child = replaceEqualDeep(previousItems[key], nextItems[key]);
|
|
1055
|
+
if (equal && child !== previousItems[key]) equal = false;
|
|
1056
|
+
merged[key] = child;
|
|
1057
|
+
}
|
|
1058
|
+
return equal ? previous : merged;
|
|
1059
|
+
}
|
|
1060
|
+
return next;
|
|
1061
|
+
}
|
|
1062
|
+
function isPlainObject2(value) {
|
|
1063
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1064
|
+
const proto = Object.getPrototypeOf(value);
|
|
1065
|
+
return proto === Object.prototype || proto === null;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// src/query/useQuery.ts
|
|
1069
|
+
var MATCH_NONE = { _id: { $in: [] } };
|
|
1070
|
+
var DEFAULT_STALE_TIME = 60 * 6e4;
|
|
1071
|
+
var INITIAL = {
|
|
1072
|
+
ids: null,
|
|
1073
|
+
shape: "array",
|
|
1074
|
+
payload: void 0,
|
|
1075
|
+
error: null,
|
|
1076
|
+
fetchStatus: "idle",
|
|
1077
|
+
dataUpdatedAt: 0,
|
|
1078
|
+
errorUpdatedAt: 0,
|
|
1079
|
+
failureCount: 0,
|
|
1080
|
+
failureReason: null,
|
|
1081
|
+
errorUpdateCount: 0,
|
|
1082
|
+
isFetched: false,
|
|
1083
|
+
isFetchedAfterMount: false,
|
|
1084
|
+
fromCollection: false
|
|
1085
|
+
};
|
|
1086
|
+
function isAbort(cause) {
|
|
1087
|
+
return cause instanceof Error && cause.name === "AbortError";
|
|
1088
|
+
}
|
|
1089
|
+
function toError(cause) {
|
|
1090
|
+
return cause instanceof Error ? cause : new Error(String(cause));
|
|
1091
|
+
}
|
|
1092
|
+
function keepPreviousData(previous) {
|
|
1093
|
+
return previous;
|
|
1094
|
+
}
|
|
1095
|
+
function resolvePlaceholder(options, previous) {
|
|
1096
|
+
const placeholder = options.placeholderData;
|
|
1097
|
+
if (placeholder === void 0) return void 0;
|
|
1098
|
+
return typeof placeholder === "function" ? placeholder(previous) : placeholder;
|
|
1099
|
+
}
|
|
1100
|
+
function buildResult(common, data, error) {
|
|
1101
|
+
if (error !== null) {
|
|
1102
|
+
return data === void 0 ? {
|
|
1103
|
+
...common,
|
|
1104
|
+
status: "error",
|
|
1105
|
+
data: void 0,
|
|
1106
|
+
error,
|
|
1107
|
+
isPending: false,
|
|
1108
|
+
isSuccess: false,
|
|
1109
|
+
isError: true,
|
|
1110
|
+
isLoadingError: true,
|
|
1111
|
+
isRefetchError: false
|
|
1112
|
+
} : {
|
|
1113
|
+
...common,
|
|
1114
|
+
status: "error",
|
|
1115
|
+
data,
|
|
1116
|
+
error,
|
|
1117
|
+
isPending: false,
|
|
1118
|
+
isSuccess: false,
|
|
1119
|
+
isError: true,
|
|
1120
|
+
isLoadingError: false,
|
|
1121
|
+
isRefetchError: true
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
if (data === void 0) {
|
|
1125
|
+
return {
|
|
1126
|
+
...common,
|
|
1127
|
+
status: "pending",
|
|
1128
|
+
data: void 0,
|
|
1129
|
+
error: null,
|
|
1130
|
+
isPending: true,
|
|
1131
|
+
isSuccess: false,
|
|
1132
|
+
isError: false,
|
|
1133
|
+
isLoadingError: false,
|
|
1134
|
+
isRefetchError: false
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
return {
|
|
1138
|
+
...common,
|
|
1139
|
+
status: "success",
|
|
1140
|
+
data,
|
|
1141
|
+
error: null,
|
|
1142
|
+
isPending: false,
|
|
1143
|
+
isSuccess: true,
|
|
1144
|
+
isError: false,
|
|
1145
|
+
isLoadingError: false,
|
|
1146
|
+
isRefetchError: false
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
function useQuery(options) {
|
|
1150
|
+
const { queryKey, meta } = options;
|
|
1151
|
+
const enabled = typeof options.enabled === "function" ? options.enabled() : options.enabled ?? true;
|
|
1152
|
+
const { queries, register, resolveCollection, defaults } = useQueryContext();
|
|
1153
|
+
const staleTime = options.staleTime ?? defaults?.staleTime ?? DEFAULT_STALE_TIME;
|
|
1154
|
+
const retry = options.retry ?? defaults?.retry;
|
|
1155
|
+
const retryDelay = options.retryDelay ?? defaults?.retryDelay;
|
|
1156
|
+
const networkMode = options.networkMode ?? defaults?.networkMode;
|
|
1157
|
+
const refetchOnMount = options.refetchOnMount ?? defaults?.refetchOnMount;
|
|
1158
|
+
const refetchOnWindowFocus = options.refetchOnWindowFocus ?? defaults?.refetchOnWindowFocus;
|
|
1159
|
+
const refetchOnReconnect = options.refetchOnReconnect ?? defaults?.refetchOnReconnect;
|
|
1160
|
+
const refetchIntervalOption = options.refetchInterval ?? defaults?.refetchInterval;
|
|
1161
|
+
const refetchInterval = typeof refetchIntervalOption === "function" ? refetchIntervalOption() : refetchIntervalOption;
|
|
1162
|
+
const refetchIntervalInBackground = options.refetchIntervalInBackground ?? defaults?.refetchIntervalInBackground ?? false;
|
|
1163
|
+
const throwOnError = options.throwOnError ?? defaults?.throwOnError ?? false;
|
|
1164
|
+
const forgetAfter = options.forgetAfter ?? defaults?.forgetAfter ?? Number.POSITIVE_INFINITY;
|
|
1165
|
+
const hashFn = options.queryKeyHashFn ?? hashQueryKey;
|
|
1166
|
+
noteIgnoredOptions(options);
|
|
1167
|
+
const registry = useCollectionOptions();
|
|
1168
|
+
const params = (0, import_react5.useMemo)(
|
|
1169
|
+
() => normalizeParams(options.params),
|
|
1170
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1171
|
+
[JSON.stringify(options.params === void 0 ? null : options.params.values ?? options.params)]
|
|
1172
|
+
);
|
|
1173
|
+
const effectiveKey = (0, import_react5.useMemo)(
|
|
1174
|
+
() => params === void 0 ? queryKey : [...queryKey, params.values],
|
|
1175
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1176
|
+
[hashFn(queryKey), params]
|
|
1177
|
+
);
|
|
1178
|
+
const keyHash = hashFn(effectiveKey);
|
|
1179
|
+
const name = (0, import_react5.useMemo)(
|
|
1180
|
+
() => resolveCollectionName({
|
|
1181
|
+
collection: options.collection,
|
|
1182
|
+
// The caller's key, not the parameter-extended one: the collection is
|
|
1183
|
+
// named by what they wrote, and appending values must not change it.
|
|
1184
|
+
queryKey,
|
|
1185
|
+
resolve: resolveCollection,
|
|
1186
|
+
registered: registry.names()
|
|
1187
|
+
}),
|
|
1188
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1189
|
+
[options.collection, keyHash, resolveCollection, registry]
|
|
1190
|
+
);
|
|
1191
|
+
const collection = useCollection(name);
|
|
1192
|
+
const [state, setState] = (0, import_react5.useState)(() => ({
|
|
1193
|
+
...INITIAL,
|
|
1194
|
+
fetchStatus: enabled ? "fetching" : "idle"
|
|
1195
|
+
}));
|
|
1196
|
+
const offline = options.offline ?? "strict";
|
|
1197
|
+
const [renderedKey, setRenderedKey] = (0, import_react5.useState)(keyHash);
|
|
1198
|
+
const previousDataRef = (0, import_react5.useRef)(void 0);
|
|
1199
|
+
if (renderedKey !== keyHash) {
|
|
1200
|
+
setRenderedKey(keyHash);
|
|
1201
|
+
setState({ ...INITIAL, fetchStatus: enabled ? "fetching" : "idle" });
|
|
1202
|
+
}
|
|
1203
|
+
const queryFnRef = (0, import_react5.useRef)(options.queryFn);
|
|
1204
|
+
queryFnRef.current = options.queryFn;
|
|
1205
|
+
const staleTimeRef = (0, import_react5.useRef)(staleTime);
|
|
1206
|
+
staleTimeRef.current = staleTime;
|
|
1207
|
+
const metaRef = (0, import_react5.useRef)(meta);
|
|
1208
|
+
metaRef.current = meta;
|
|
1209
|
+
const documentsRef = (0, import_react5.useRef)(options.documents);
|
|
1210
|
+
documentsRef.current = options.documents;
|
|
1211
|
+
const assembleRef = (0, import_react5.useRef)(options.assemble);
|
|
1212
|
+
assembleRef.current = options.assemble;
|
|
1213
|
+
const initialDataRef = (0, import_react5.useRef)(options.initialData);
|
|
1214
|
+
initialDataRef.current = options.initialData;
|
|
1215
|
+
const initialDataUpdatedAtRef = (0, import_react5.useRef)(options.initialDataUpdatedAt);
|
|
1216
|
+
initialDataUpdatedAtRef.current = options.initialDataUpdatedAt;
|
|
1217
|
+
const retryRef = (0, import_react5.useRef)(retry);
|
|
1218
|
+
retryRef.current = retry;
|
|
1219
|
+
const retryDelayRef = (0, import_react5.useRef)(retryDelay);
|
|
1220
|
+
retryDelayRef.current = retryDelay;
|
|
1221
|
+
const networkModeRef = (0, import_react5.useRef)(networkMode);
|
|
1222
|
+
networkModeRef.current = networkMode;
|
|
1223
|
+
const enabledRef = (0, import_react5.useRef)(enabled);
|
|
1224
|
+
enabledRef.current = enabled;
|
|
1225
|
+
const hashRef = (0, import_react5.useRef)(hashFn);
|
|
1226
|
+
hashRef.current = hashFn;
|
|
1227
|
+
const forgetAfterRef = (0, import_react5.useRef)(forgetAfter);
|
|
1228
|
+
forgetAfterRef.current = forgetAfter;
|
|
1229
|
+
const paramsRef = (0, import_react5.useRef)(params);
|
|
1230
|
+
paramsRef.current = params;
|
|
1231
|
+
const stateRef = (0, import_react5.useRef)(state);
|
|
1232
|
+
stateRef.current = state;
|
|
1233
|
+
(0, import_react5.useEffect)(() => {
|
|
1234
|
+
register(name);
|
|
1235
|
+
}, [register, name]);
|
|
1236
|
+
const refetchControllersRef = (0, import_react5.useRef)(/* @__PURE__ */ new Set());
|
|
1237
|
+
const resolversRef = (0, import_react5.useRef)([]);
|
|
1238
|
+
const foundRef = (0, import_react5.useRef)(void 0);
|
|
1239
|
+
const mountedRef = (0, import_react5.useRef)(true);
|
|
1240
|
+
(0, import_react5.useEffect)(() => {
|
|
1241
|
+
mountedRef.current = true;
|
|
1242
|
+
return () => {
|
|
1243
|
+
mountedRef.current = false;
|
|
1244
|
+
for (const controller of refetchControllersRef.current) controller.abort();
|
|
1245
|
+
refetchControllersRef.current.clear();
|
|
1246
|
+
const waiting = resolversRef.current;
|
|
1247
|
+
resolversRef.current = [];
|
|
1248
|
+
for (const { resolve } of waiting) resolve(resultRef.current);
|
|
1249
|
+
};
|
|
1250
|
+
}, []);
|
|
1251
|
+
const nextResult = (0, import_react5.useCallback)(() => {
|
|
1252
|
+
if (!mountedRef.current) return Promise.resolve(resultRef.current);
|
|
1253
|
+
return new Promise((resolve) => {
|
|
1254
|
+
const entry = { resolve, snapshot: foundRef.current };
|
|
1255
|
+
resolversRef.current.push(entry);
|
|
1256
|
+
setTimeout(() => {
|
|
1257
|
+
const index = resolversRef.current.indexOf(entry);
|
|
1258
|
+
if (index === -1) return;
|
|
1259
|
+
resolversRef.current.splice(index, 1);
|
|
1260
|
+
resolve(resultRef.current);
|
|
1261
|
+
}, 50);
|
|
1262
|
+
});
|
|
1263
|
+
}, []);
|
|
1264
|
+
(0, import_react5.useEffect)(() => {
|
|
1265
|
+
cancelForget(name, keyHash);
|
|
1266
|
+
return () => {
|
|
1267
|
+
const after = forgetAfterRef.current;
|
|
1268
|
+
if (!Number.isFinite(after) || queries === null) return;
|
|
1269
|
+
scheduleForget(name, keyHash, after, () => {
|
|
1270
|
+
void deleteQueryRecord(queries, name, effectiveKey, hashRef.current).catch(() => {
|
|
1271
|
+
});
|
|
1272
|
+
});
|
|
1273
|
+
};
|
|
1274
|
+
}, [queries, name, keyHash]);
|
|
1275
|
+
const patch = (0, import_react5.useCallback)((next) => {
|
|
1276
|
+
if (mountedRef.current) setState((current) => ({ ...current, ...next }));
|
|
1277
|
+
}, []);
|
|
1278
|
+
const commit = (0, import_react5.useCallback)(
|
|
1279
|
+
async (raw, fetchedAt) => {
|
|
1280
|
+
const { documents: docs, shape: shape3 } = extractDocuments({
|
|
1281
|
+
raw,
|
|
1282
|
+
collection: name,
|
|
1283
|
+
queryKey: effectiveKey,
|
|
1284
|
+
documents: documentsRef.current
|
|
1285
|
+
});
|
|
1286
|
+
await hydrate(collection, docs, fetchedAt, name);
|
|
1287
|
+
const nextIds = docs.map((doc) => doc._id);
|
|
1288
|
+
const payload2 = shape3 === "envelope" && assembleRef.current !== void 0 ? raw : void 0;
|
|
1289
|
+
await writeQueryRecord(
|
|
1290
|
+
queries,
|
|
1291
|
+
name,
|
|
1292
|
+
effectiveKey,
|
|
1293
|
+
nextIds,
|
|
1294
|
+
fetchedAt,
|
|
1295
|
+
staleTimeRef.current,
|
|
1296
|
+
shape3,
|
|
1297
|
+
payload2,
|
|
1298
|
+
hashRef.current
|
|
1299
|
+
);
|
|
1300
|
+
return {
|
|
1301
|
+
ids: nextIds,
|
|
1302
|
+
shape: shape3,
|
|
1303
|
+
payload: payload2,
|
|
1304
|
+
error: null,
|
|
1305
|
+
fetchStatus: "idle",
|
|
1306
|
+
dataUpdatedAt: fetchedAt,
|
|
1307
|
+
failureCount: 0,
|
|
1308
|
+
failureReason: null,
|
|
1309
|
+
isFetched: true,
|
|
1310
|
+
fromCollection: false
|
|
1311
|
+
};
|
|
1312
|
+
},
|
|
1313
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1314
|
+
[queries, collection, name, keyHash]
|
|
1315
|
+
);
|
|
1316
|
+
const attemptFetch = (0, import_react5.useCallback)(
|
|
1317
|
+
async (signal) => {
|
|
1318
|
+
let failures = 0;
|
|
1319
|
+
for (; ; ) {
|
|
1320
|
+
try {
|
|
1321
|
+
return await queryFnRef.current({
|
|
1322
|
+
queryKey: effectiveKey,
|
|
1323
|
+
signal,
|
|
1324
|
+
meta: metaRef.current,
|
|
1325
|
+
params: paramsRef.current
|
|
1326
|
+
});
|
|
1327
|
+
} catch (cause) {
|
|
1328
|
+
if (signal.aborted) throw cause;
|
|
1329
|
+
const error = toError(cause);
|
|
1330
|
+
failures += 1;
|
|
1331
|
+
patch({ failureCount: failures, failureReason: error });
|
|
1332
|
+
if (!shouldRetry(retryRef.current, failures, error)) throw error;
|
|
1333
|
+
await sleep(retryDelayMs(retryDelayRef.current, failures - 1, error), signal);
|
|
1334
|
+
if (signal.aborted) throw error;
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
},
|
|
1338
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1339
|
+
[keyHash, patch]
|
|
1340
|
+
);
|
|
1341
|
+
const revalidate = (0, import_react5.useCallback)(
|
|
1342
|
+
async (signal) => {
|
|
1343
|
+
if (queries === null) return;
|
|
1344
|
+
if (networkModeRef.current !== "always" && isOffline()) {
|
|
1345
|
+
patch({ fetchStatus: "paused" });
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
patch({ fetchStatus: "fetching" });
|
|
1349
|
+
try {
|
|
1350
|
+
const committed = await runShared(
|
|
1351
|
+
inflightKey(name, keyHash),
|
|
1352
|
+
async (sharedSignal) => {
|
|
1353
|
+
const raw = await attemptFetch(sharedSignal);
|
|
1354
|
+
return commit(raw, Date.now());
|
|
1355
|
+
},
|
|
1356
|
+
signal
|
|
1357
|
+
);
|
|
1358
|
+
if (signal.aborted) return;
|
|
1359
|
+
patch({ ...committed, isFetchedAfterMount: true });
|
|
1360
|
+
} catch (cause) {
|
|
1361
|
+
if (signal.aborted) return;
|
|
1362
|
+
if (isAbort(cause)) {
|
|
1363
|
+
patch({ fetchStatus: "idle" });
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
const error = toError(cause);
|
|
1367
|
+
setState((current) => ({
|
|
1368
|
+
...current,
|
|
1369
|
+
error,
|
|
1370
|
+
fetchStatus: "idle",
|
|
1371
|
+
errorUpdatedAt: Date.now(),
|
|
1372
|
+
// `attemptFetch` already counted each network failure. A shape error
|
|
1373
|
+
// never reached it, so this floors the count at one.
|
|
1374
|
+
failureCount: Math.max(current.failureCount, 1),
|
|
1375
|
+
failureReason: error,
|
|
1376
|
+
errorUpdateCount: current.errorUpdateCount + 1,
|
|
1377
|
+
isFetched: true
|
|
1378
|
+
}));
|
|
1379
|
+
throw error;
|
|
1380
|
+
}
|
|
1381
|
+
},
|
|
1382
|
+
// `queryKey` is covered by keyHash; including the array itself would defeat it.
|
|
1383
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1384
|
+
[queries, collection, name, keyHash, attemptFetch, commit, patch]
|
|
1385
|
+
);
|
|
1386
|
+
const refetchIfAllowed = (0, import_react5.useCallback)(
|
|
1387
|
+
(trigger) => {
|
|
1388
|
+
const setting = trigger ?? true;
|
|
1389
|
+
if (setting === false || !enabledRef.current) return;
|
|
1390
|
+
const { dataUpdatedAt } = stateRef.current;
|
|
1391
|
+
const stale = dataUpdatedAt === 0 || Date.now() >= dataUpdatedAt + staleTimeRef.current;
|
|
1392
|
+
if (setting !== "always" && !stale) return;
|
|
1393
|
+
const controller = new AbortController();
|
|
1394
|
+
void revalidate(controller.signal).catch(() => {
|
|
1395
|
+
});
|
|
1396
|
+
},
|
|
1397
|
+
[revalidate]
|
|
1398
|
+
);
|
|
1399
|
+
(0, import_react5.useEffect)(() => {
|
|
1400
|
+
if (queries === null) return;
|
|
1401
|
+
const controller = new AbortController();
|
|
1402
|
+
void (async () => {
|
|
1403
|
+
try {
|
|
1404
|
+
await ensureEnvelopeIndexes(collection);
|
|
1405
|
+
const record = await readQueryRecord(queries, name, effectiveKey, hashRef.current);
|
|
1406
|
+
if (controller.signal.aborted) return;
|
|
1407
|
+
if (record !== null) {
|
|
1408
|
+
patch({
|
|
1409
|
+
ids: record.ids,
|
|
1410
|
+
// Restored, not re-derived: a detail view whose response was one
|
|
1411
|
+
// document must not come back as a one-element array on reload.
|
|
1412
|
+
shape: record.shape ?? "array",
|
|
1413
|
+
payload: record.payload ?? void 0,
|
|
1414
|
+
dataUpdatedAt: record.fetchedAt,
|
|
1415
|
+
isFetched: true
|
|
1416
|
+
});
|
|
1417
|
+
const onMount = refetchOnMount ?? true;
|
|
1418
|
+
const staleEnough = onMount === "always" || record.fetchedAt === 0 || Date.now() >= record.fetchedAt + staleTimeRef.current;
|
|
1419
|
+
if (!enabled || onMount === false || !staleEnough) {
|
|
1420
|
+
patch({ fetchStatus: "idle" });
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
} else {
|
|
1424
|
+
const seed = initialDataRef.current;
|
|
1425
|
+
if (seed !== void 0) {
|
|
1426
|
+
const raw = typeof seed === "function" ? seed() : seed;
|
|
1427
|
+
const seededAt = initialDataUpdatedAtRef.current ?? Date.now();
|
|
1428
|
+
const committed = await commit(raw, seededAt);
|
|
1429
|
+
if (controller.signal.aborted) return;
|
|
1430
|
+
patch(committed);
|
|
1431
|
+
if (!enabled || Date.now() < seededAt + staleTimeRef.current) {
|
|
1432
|
+
patch({ fetchStatus: "idle" });
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
} else if (!enabled) {
|
|
1436
|
+
patch({ fetchStatus: "idle" });
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
await revalidate(controller.signal);
|
|
1441
|
+
} catch {
|
|
1442
|
+
if (offline === "collection" && stateRef.current.ids === null) {
|
|
1443
|
+
const filter2 = paramsRef.current?.toFilter();
|
|
1444
|
+
if (filter2 !== void 0 && !controller.signal.aborted) {
|
|
1445
|
+
const shapeParams = paramsRef.current?.shapeParams() ?? [];
|
|
1446
|
+
if (shapeParams.length > 0) {
|
|
1447
|
+
warnOnce(
|
|
1448
|
+
`offline-shape:${name}`,
|
|
1449
|
+
`useQuery on '${name}' fell back to the whole collection while offline, but its parameters include ${shapeParams.join(", ")}, which only the server can honour. There is no page 2 of a local query \u2014 expect more results than the request would have returned.`
|
|
1450
|
+
);
|
|
1451
|
+
}
|
|
1452
|
+
patch({ fromCollection: true, isFetched: true });
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
patch({ fetchStatus: "idle" });
|
|
1456
|
+
}
|
|
1457
|
+
})();
|
|
1458
|
+
return () => controller.abort();
|
|
1459
|
+
}, [enabled, refetchOnMount, offline, queries, collection, name, keyHash, revalidate, commit, patch]);
|
|
1460
|
+
(0, import_react5.useEffect)(() => {
|
|
1461
|
+
if (typeof window === "undefined" || refetchOnWindowFocus === false) return;
|
|
1462
|
+
const onFocus = () => {
|
|
1463
|
+
if (typeof document !== "undefined" && document.visibilityState === "hidden") return;
|
|
1464
|
+
refetchIfAllowed(refetchOnWindowFocus);
|
|
1465
|
+
};
|
|
1466
|
+
window.addEventListener("visibilitychange", onFocus);
|
|
1467
|
+
window.addEventListener("focus", onFocus);
|
|
1468
|
+
return () => {
|
|
1469
|
+
window.removeEventListener("visibilitychange", onFocus);
|
|
1470
|
+
window.removeEventListener("focus", onFocus);
|
|
1471
|
+
};
|
|
1472
|
+
}, [refetchOnWindowFocus, refetchIfAllowed]);
|
|
1473
|
+
(0, import_react5.useEffect)(() => {
|
|
1474
|
+
if (typeof window === "undefined" || refetchOnReconnect === false) return;
|
|
1475
|
+
const onOnline = () => refetchIfAllowed(refetchOnReconnect);
|
|
1476
|
+
window.addEventListener("online", onOnline);
|
|
1477
|
+
return () => window.removeEventListener("online", onOnline);
|
|
1478
|
+
}, [refetchOnReconnect, refetchIfAllowed]);
|
|
1479
|
+
(0, import_react5.useEffect)(() => {
|
|
1480
|
+
if (!refetchInterval || !enabled) return;
|
|
1481
|
+
const timer = setInterval(() => {
|
|
1482
|
+
if (!refetchIntervalInBackground && typeof document !== "undefined" && document.visibilityState === "hidden") {
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
refetchIfAllowed("always");
|
|
1486
|
+
}, refetchInterval);
|
|
1487
|
+
return () => clearInterval(timer);
|
|
1488
|
+
}, [refetchInterval, refetchIntervalInBackground, enabled, refetchIfAllowed]);
|
|
1489
|
+
const [, forceStaleCheck] = (0, import_react5.useState)(0);
|
|
1490
|
+
(0, import_react5.useEffect)(() => {
|
|
1491
|
+
if (state.dataUpdatedAt === 0 || !Number.isFinite(staleTime)) return;
|
|
1492
|
+
const staleAt = state.dataUpdatedAt + staleTime;
|
|
1493
|
+
if (Date.now() >= staleAt) return;
|
|
1494
|
+
let timer;
|
|
1495
|
+
const check = () => {
|
|
1496
|
+
const remaining = staleAt - Date.now();
|
|
1497
|
+
if (remaining > 0) {
|
|
1498
|
+
timer = setTimeout(check, remaining);
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1501
|
+
forceStaleCheck((n) => n + 1);
|
|
1502
|
+
};
|
|
1503
|
+
timer = setTimeout(check, staleAt - Date.now());
|
|
1504
|
+
return () => clearTimeout(timer);
|
|
1505
|
+
}, [state.dataUpdatedAt, staleTime]);
|
|
1506
|
+
const ids = state.ids;
|
|
1507
|
+
const { where } = options;
|
|
1508
|
+
const whereHash = where === void 0 ? "" : JSON.stringify(where);
|
|
1509
|
+
const { fromCollection } = state;
|
|
1510
|
+
const filter = (0, import_react5.useMemo)(() => {
|
|
1511
|
+
const clauses = [];
|
|
1512
|
+
if (fromCollection) {
|
|
1513
|
+
const params2 = paramsRef.current?.toFilter();
|
|
1514
|
+
if (params2 !== void 0) clauses.push(params2);
|
|
1515
|
+
} else {
|
|
1516
|
+
if (ids === null || ids.length === 0) return MATCH_NONE;
|
|
1517
|
+
clauses.push({ _id: { $in: ids } });
|
|
1518
|
+
const params2 = paramsRef.current?.toFilter();
|
|
1519
|
+
if (params2 !== void 0) clauses.push(params2);
|
|
1520
|
+
}
|
|
1521
|
+
if (where !== void 0) clauses.push(where);
|
|
1522
|
+
if (clauses.length === 0) return MATCH_NONE;
|
|
1523
|
+
if (clauses.length === 1) return clauses[0];
|
|
1524
|
+
return { $and: clauses };
|
|
1525
|
+
}, [ids, whereHash, fromCollection, keyHash]);
|
|
1526
|
+
const { data: found } = useFind(collection, filter);
|
|
1527
|
+
const documents = (0, import_react5.useMemo)(() => {
|
|
1528
|
+
if (fromCollection) {
|
|
1529
|
+
return found.filter((doc) => doc._op !== "delete");
|
|
1530
|
+
}
|
|
1531
|
+
if (ids === null) return void 0;
|
|
1532
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1533
|
+
for (const doc of found) byId.set(doc._id, doc);
|
|
1534
|
+
const ordered = [];
|
|
1535
|
+
for (const id of ids) {
|
|
1536
|
+
const doc = byId.get(id);
|
|
1537
|
+
if (doc !== void 0 && doc._op !== "delete") ordered.push(doc);
|
|
1538
|
+
}
|
|
1539
|
+
return ordered;
|
|
1540
|
+
}, [found, ids, fromCollection]);
|
|
1541
|
+
const { shape: shape2, payload } = state;
|
|
1542
|
+
const assembled = (0, import_react5.useMemo)(() => {
|
|
1543
|
+
if (documents === void 0) return void 0;
|
|
1544
|
+
if (shape2 === "document") {
|
|
1545
|
+
return documents[0];
|
|
1546
|
+
}
|
|
1547
|
+
if (shape2 === "envelope" && assembleRef.current !== void 0) {
|
|
1548
|
+
const assemble = assembleRef.current;
|
|
1549
|
+
return assemble(documents, payload);
|
|
1550
|
+
}
|
|
1551
|
+
return documents;
|
|
1552
|
+
}, [documents, shape2, payload]);
|
|
1553
|
+
const { select } = options;
|
|
1554
|
+
const selected = (0, import_react5.useMemo)(() => {
|
|
1555
|
+
if (select === void 0 || assembled === void 0) return assembled;
|
|
1556
|
+
return select(assembled);
|
|
1557
|
+
}, [assembled, select]);
|
|
1558
|
+
const sharedRef = (0, import_react5.useRef)(void 0);
|
|
1559
|
+
const shared = replaceEqualDeep(sharedRef.current, selected);
|
|
1560
|
+
sharedRef.current = shared;
|
|
1561
|
+
if (shared !== void 0) previousDataRef.current = shared;
|
|
1562
|
+
const placeholder = shared === void 0 && state.error === null ? resolvePlaceholder(options, previousDataRef.current) : void 0;
|
|
1563
|
+
const isPlaceholderData = placeholder !== void 0;
|
|
1564
|
+
const data = isPlaceholderData ? placeholder : shared;
|
|
1565
|
+
const resultRef = (0, import_react5.useRef)(void 0);
|
|
1566
|
+
const refetch = (0, import_react5.useCallback)(
|
|
1567
|
+
async (opts) => {
|
|
1568
|
+
if (opts?.cancelRefetch !== false) cancelShared(inflightKey(name, keyHash));
|
|
1569
|
+
const controller = new AbortController();
|
|
1570
|
+
refetchControllersRef.current.add(controller);
|
|
1571
|
+
try {
|
|
1572
|
+
await revalidate(controller.signal);
|
|
1573
|
+
} catch (cause) {
|
|
1574
|
+
if (opts?.throwOnError) throw cause;
|
|
1575
|
+
} finally {
|
|
1576
|
+
refetchControllersRef.current.delete(controller);
|
|
1577
|
+
}
|
|
1578
|
+
return nextResult();
|
|
1579
|
+
},
|
|
1580
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1581
|
+
[revalidate, name, keyHash]
|
|
1582
|
+
);
|
|
1583
|
+
const isFetching = state.fetchStatus === "fetching";
|
|
1584
|
+
const isPending = ids === null;
|
|
1585
|
+
const isStale2 = state.dataUpdatedAt === 0 || Date.now() >= state.dataUpdatedAt + staleTime;
|
|
1586
|
+
const common = {
|
|
1587
|
+
fetchStatus: state.fetchStatus,
|
|
1588
|
+
isLoading: isPending && isFetching,
|
|
1589
|
+
isFetching,
|
|
1590
|
+
isPaused: state.fetchStatus === "paused",
|
|
1591
|
+
isRefetching: isFetching && !isPending,
|
|
1592
|
+
isStale: isStale2,
|
|
1593
|
+
isFetched: state.isFetched,
|
|
1594
|
+
isFetchedAfterMount: state.isFetchedAfterMount,
|
|
1595
|
+
isPlaceholderData,
|
|
1596
|
+
fromCollection: state.fromCollection,
|
|
1597
|
+
dataUpdatedAt: state.dataUpdatedAt,
|
|
1598
|
+
errorUpdatedAt: state.errorUpdatedAt,
|
|
1599
|
+
failureCount: state.failureCount,
|
|
1600
|
+
failureReason: state.failureReason,
|
|
1601
|
+
errorUpdateCount: state.errorUpdateCount,
|
|
1602
|
+
refetch
|
|
1603
|
+
};
|
|
1604
|
+
const result = buildResult(common, data, state.error);
|
|
1605
|
+
resultRef.current = result;
|
|
1606
|
+
(0, import_react5.useEffect)(() => {
|
|
1607
|
+
foundRef.current = found;
|
|
1608
|
+
if (resolversRef.current.length === 0) return;
|
|
1609
|
+
const ready = resolversRef.current.filter((entry) => entry.snapshot !== found);
|
|
1610
|
+
if (ready.length === 0) return;
|
|
1611
|
+
resolversRef.current = resolversRef.current.filter((entry) => entry.snapshot === found);
|
|
1612
|
+
for (const { resolve } of ready) resolve(resultRef.current);
|
|
1613
|
+
});
|
|
1614
|
+
if (state.error !== null && shouldThrow(throwOnError, state.error)) throw state.error;
|
|
1615
|
+
return result;
|
|
1616
|
+
}
|
|
1617
|
+
function shouldThrow(setting, error) {
|
|
1618
|
+
return typeof setting === "function" ? setting(error) : setting;
|
|
1619
|
+
}
|
|
1620
|
+
var forgetTimers = /* @__PURE__ */ new Map();
|
|
1621
|
+
function forgetKey(collection, keyHash) {
|
|
1622
|
+
return `${collection}\0${keyHash}`;
|
|
1623
|
+
}
|
|
1624
|
+
function scheduleForget(collection, keyHash, after, run) {
|
|
1625
|
+
const key = forgetKey(collection, keyHash);
|
|
1626
|
+
const timer = setTimeout(() => {
|
|
1627
|
+
forgetTimers.delete(key);
|
|
1628
|
+
run();
|
|
1629
|
+
}, after);
|
|
1630
|
+
timer.unref?.();
|
|
1631
|
+
forgetTimers.set(key, timer);
|
|
1632
|
+
}
|
|
1633
|
+
function cancelForget(collection, keyHash) {
|
|
1634
|
+
const key = forgetKey(collection, keyHash);
|
|
1635
|
+
const timer = forgetTimers.get(key);
|
|
1636
|
+
if (timer === void 0) return;
|
|
1637
|
+
clearTimeout(timer);
|
|
1638
|
+
forgetTimers.delete(key);
|
|
1639
|
+
}
|
|
1640
|
+
function resetForgetTimers() {
|
|
1641
|
+
for (const timer of forgetTimers.values()) clearTimeout(timer);
|
|
1642
|
+
forgetTimers.clear();
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
// src/query/useMutation.ts
|
|
1646
|
+
var import_react6 = require("react");
|
|
1647
|
+
|
|
1648
|
+
// src/query/mutate.ts
|
|
1649
|
+
function stampRoute(route, op) {
|
|
1650
|
+
if (route?.url === void 0) return {};
|
|
1651
|
+
return {
|
|
1652
|
+
_endpoint: route.url,
|
|
1653
|
+
_method: route.method?.[op] ?? DEFAULT_METHODS[op]
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1656
|
+
var OP_FOR = {
|
|
1657
|
+
insert: "insert",
|
|
1658
|
+
update: "update",
|
|
1659
|
+
delete: "delete"
|
|
1660
|
+
};
|
|
1661
|
+
async function writeLocal(collection, op, now, route) {
|
|
1662
|
+
if (op.type === "insert") {
|
|
1663
|
+
const doc = { ...stampPending(op.doc, "insert"), ...stampRoute(route, "insert") };
|
|
1664
|
+
const id2 = await collection.insert(doc);
|
|
1665
|
+
return { id: id2, op: "insert", cancelled: false };
|
|
1666
|
+
}
|
|
1667
|
+
const id = op.where._id;
|
|
1668
|
+
const existing = await collection.findOne({ _id: id });
|
|
1669
|
+
if (existing === null) {
|
|
1670
|
+
throw new Error(
|
|
1671
|
+
`Cannot ${op.type} document ${id}: it is not in the local collection. Writes apply locally first, so the document must already be here.`
|
|
1672
|
+
);
|
|
1673
|
+
}
|
|
1674
|
+
const next = coalesce(pendingOp(existing), OP_FOR[op.type]);
|
|
1675
|
+
if (next === null) {
|
|
1676
|
+
await collection.deleteOne({ _id: id });
|
|
1677
|
+
return { id, op: null, cancelled: true };
|
|
1678
|
+
}
|
|
1679
|
+
const fields = {
|
|
1680
|
+
...op.type === "update" ? op.set : {},
|
|
1681
|
+
_sync: "pending",
|
|
1682
|
+
_op: next,
|
|
1683
|
+
_attempt: 0,
|
|
1684
|
+
_error: null,
|
|
1685
|
+
_retry_at: 0,
|
|
1686
|
+
_revision: nextRevision(),
|
|
1687
|
+
// The verb follows the coalesced operation, not the one just requested:
|
|
1688
|
+
// editing an unsent insert still POSTs.
|
|
1689
|
+
...stampRoute(route, next)
|
|
1690
|
+
};
|
|
1691
|
+
await collection.updateOne(
|
|
1692
|
+
{ _id: id },
|
|
1693
|
+
{
|
|
1694
|
+
$set: fields,
|
|
1695
|
+
// A hook without a route means "use the provider", not "reuse whatever
|
|
1696
|
+
// endpoint a previous write happened to stamp on this document".
|
|
1697
|
+
...route?.url === void 0 ? { $unset: { _endpoint: true, _method: true } } : {}
|
|
1698
|
+
}
|
|
1699
|
+
);
|
|
1700
|
+
return { id, op: next, cancelled: false };
|
|
1701
|
+
}
|
|
1702
|
+
function pendingOp(doc) {
|
|
1703
|
+
if (doc._sync !== "pending" && doc._sync !== "failed") return void 0;
|
|
1704
|
+
return doc._op;
|
|
1705
|
+
}
|
|
1706
|
+
async function writeConfirmed(collection, doc, now) {
|
|
1707
|
+
const id = doc._id;
|
|
1708
|
+
const current = await collection.findOne({ _id: id });
|
|
1709
|
+
await applyCanonical(collection, current, doc, now);
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
// src/query/useMutation.ts
|
|
1713
|
+
var INITIAL2 = {
|
|
1714
|
+
status: "idle",
|
|
1715
|
+
data: void 0,
|
|
1716
|
+
error: null,
|
|
1717
|
+
variables: void 0,
|
|
1718
|
+
submittedAt: 0,
|
|
1719
|
+
failureCount: 0,
|
|
1720
|
+
failureReason: null,
|
|
1721
|
+
paused: false
|
|
1722
|
+
};
|
|
1723
|
+
function toError2(cause) {
|
|
1724
|
+
return cause instanceof Error ? cause : new Error(String(cause));
|
|
1725
|
+
}
|
|
1726
|
+
function useMutation(options) {
|
|
1727
|
+
const {
|
|
1728
|
+
collection: name,
|
|
1729
|
+
url,
|
|
1730
|
+
method,
|
|
1731
|
+
mode = "optimistic",
|
|
1732
|
+
operation = "insert"
|
|
1733
|
+
} = options;
|
|
1734
|
+
if (options.mutationFn !== void 0 && mode !== "immediate") {
|
|
1735
|
+
throw new Error(
|
|
1736
|
+
`useMutation({ collection: '${name}' }) was given a \`mutationFn\` in '${mode}' mode. Under '${mode}' the request is sent by the drain loop, long after this component is gone \u2014 after a route change, a reload, a week \u2014 so there is no closure left to call and the write would be silently dropped.
|
|
1737
|
+
|
|
1738
|
+
Either route it by template, which is data and survives:
|
|
1739
|
+
useMutation({ collection: '${name}', url: '/api/${name}/:id' })
|
|
1740
|
+
|
|
1741
|
+
or send it from here and store only what the server agreed to:
|
|
1742
|
+
useMutation({ collection: '${name}', mode: 'immediate', mutationFn })`
|
|
1743
|
+
);
|
|
1744
|
+
}
|
|
1745
|
+
const { backend, register, requestDrain, noteWrite } = useQueryContext();
|
|
1746
|
+
const collection = useCollection(name);
|
|
1747
|
+
const [state, setState] = (0, import_react6.useState)(INITIAL2);
|
|
1748
|
+
const callbacksRef = (0, import_react6.useRef)(options);
|
|
1749
|
+
callbacksRef.current = options;
|
|
1750
|
+
(0, import_react6.useEffect)(() => {
|
|
1751
|
+
register(name);
|
|
1752
|
+
}, [register, name]);
|
|
1753
|
+
const watchersRef = (0, import_react6.useRef)(/* @__PURE__ */ new Set());
|
|
1754
|
+
const mountedRef = (0, import_react6.useRef)(true);
|
|
1755
|
+
(0, import_react6.useEffect)(() => {
|
|
1756
|
+
mountedRef.current = true;
|
|
1757
|
+
return () => {
|
|
1758
|
+
mountedRef.current = false;
|
|
1759
|
+
for (const stop of watchersRef.current) stop();
|
|
1760
|
+
watchersRef.current.clear();
|
|
1761
|
+
};
|
|
1762
|
+
}, []);
|
|
1763
|
+
const patch = (0, import_react6.useCallback)((next) => {
|
|
1764
|
+
if (mountedRef.current) setState((current) => ({ ...current, ...next }));
|
|
1765
|
+
}, []);
|
|
1766
|
+
const reset = (0, import_react6.useCallback)(() => {
|
|
1767
|
+
if (mountedRef.current) setState(INITIAL2);
|
|
1768
|
+
}, []);
|
|
1769
|
+
const mutateAsync = (0, import_react6.useCallback)(
|
|
1770
|
+
async (variables, perCall) => {
|
|
1771
|
+
const hooks = callbacksRef.current;
|
|
1772
|
+
const op = toOperation(operation, variables);
|
|
1773
|
+
if (isDev() && op.type !== "insert") checkMutationFilter(op.where);
|
|
1774
|
+
patch({
|
|
1775
|
+
status: "pending",
|
|
1776
|
+
variables,
|
|
1777
|
+
error: null,
|
|
1778
|
+
data: void 0,
|
|
1779
|
+
submittedAt: Date.now()
|
|
1780
|
+
});
|
|
1781
|
+
let context;
|
|
1782
|
+
try {
|
|
1783
|
+
context = await hooks.onMutate?.(variables);
|
|
1784
|
+
const data = mode === "immediate" ? await runScoped(
|
|
1785
|
+
hooks.scope?.id,
|
|
1786
|
+
() => attemptWrite({
|
|
1787
|
+
retry: hooks.retry,
|
|
1788
|
+
retryDelay: hooks.retryDelay,
|
|
1789
|
+
networkMode: hooks.networkMode,
|
|
1790
|
+
onPaused: (paused) => patch({ paused }),
|
|
1791
|
+
onFailure: (count, reason) => patch({ failureCount: count, failureReason: reason }),
|
|
1792
|
+
send: () => sendThenStore({
|
|
1793
|
+
collection,
|
|
1794
|
+
name,
|
|
1795
|
+
op,
|
|
1796
|
+
url,
|
|
1797
|
+
method,
|
|
1798
|
+
backend,
|
|
1799
|
+
mutationFn: hooks.mutationFn,
|
|
1800
|
+
variables
|
|
1801
|
+
})
|
|
1802
|
+
})
|
|
1803
|
+
) : await storeThenSend({ collection, op, url, method, mode, requestDrain, noteWrite });
|
|
1804
|
+
patch({
|
|
1805
|
+
status: "success",
|
|
1806
|
+
data,
|
|
1807
|
+
error: null,
|
|
1808
|
+
failureCount: 0,
|
|
1809
|
+
failureReason: null,
|
|
1810
|
+
paused: false
|
|
1811
|
+
});
|
|
1812
|
+
await hooks.onSuccess?.(data, variables, context);
|
|
1813
|
+
await perCall?.onSuccess?.(data, variables, context);
|
|
1814
|
+
await hooks.onSettled?.(data, null, variables, context);
|
|
1815
|
+
await perCall?.onSettled?.(data, null, variables, context);
|
|
1816
|
+
if (mode === "immediate") {
|
|
1817
|
+
await hooks.onSynced?.(data, variables);
|
|
1818
|
+
await perCall?.onSynced?.(data, variables);
|
|
1819
|
+
} else if (hooks.onSynced !== void 0 || perCall?.onSynced !== void 0) {
|
|
1820
|
+
watchUntilSynced(collection, data._id, watchersRef, () => {
|
|
1821
|
+
void hooks.onSynced?.(data, variables);
|
|
1822
|
+
void perCall?.onSynced?.(data, variables);
|
|
1823
|
+
});
|
|
1824
|
+
}
|
|
1825
|
+
return data;
|
|
1826
|
+
} catch (cause) {
|
|
1827
|
+
const error = toError2(cause);
|
|
1828
|
+
setState((current) => ({
|
|
1829
|
+
...current,
|
|
1830
|
+
status: "error",
|
|
1831
|
+
data: void 0,
|
|
1832
|
+
error,
|
|
1833
|
+
failureCount: Math.max(current.failureCount, 1),
|
|
1834
|
+
failureReason: error,
|
|
1835
|
+
paused: false
|
|
1836
|
+
}));
|
|
1837
|
+
await hooks.onError?.(error, variables, context);
|
|
1838
|
+
await perCall?.onError?.(error, variables, context);
|
|
1839
|
+
await hooks.onSettled?.(void 0, error, variables, context);
|
|
1840
|
+
await perCall?.onSettled?.(void 0, error, variables, context);
|
|
1841
|
+
throw error;
|
|
1842
|
+
}
|
|
1843
|
+
},
|
|
1844
|
+
[collection, name, mode, operation, url, method, backend, requestDrain, noteWrite, patch]
|
|
1845
|
+
);
|
|
1846
|
+
const mutate = (0, import_react6.useCallback)(
|
|
1847
|
+
(variables, perCall) => {
|
|
1848
|
+
void mutateAsync(variables, perCall).catch(() => {
|
|
1849
|
+
});
|
|
1850
|
+
},
|
|
1851
|
+
[mutateAsync]
|
|
1852
|
+
);
|
|
1853
|
+
const common = {
|
|
1854
|
+
mutate,
|
|
1855
|
+
mutateAsync,
|
|
1856
|
+
reset,
|
|
1857
|
+
// `immediate` is the only mode that can be blocked by connectivity; the
|
|
1858
|
+
// others commit locally and let the drain worry about the network.
|
|
1859
|
+
isPaused: state.paused,
|
|
1860
|
+
failureCount: state.failureCount,
|
|
1861
|
+
failureReason: state.failureReason,
|
|
1862
|
+
submittedAt: state.submittedAt
|
|
1863
|
+
};
|
|
1864
|
+
const result = buildResult2(common, state);
|
|
1865
|
+
const throwOnError = options.throwOnError ?? false;
|
|
1866
|
+
if (state.error !== null && (typeof throwOnError === "function" ? throwOnError(state.error) : throwOnError)) {
|
|
1867
|
+
throw state.error;
|
|
1868
|
+
}
|
|
1869
|
+
return result;
|
|
1870
|
+
}
|
|
1871
|
+
async function attemptWrite(args) {
|
|
1872
|
+
const { retry, retryDelay, networkMode, onPaused, onFailure, send } = args;
|
|
1873
|
+
const controller = new AbortController();
|
|
1874
|
+
let failures = 0;
|
|
1875
|
+
for (; ; ) {
|
|
1876
|
+
if ((networkMode ?? "online") === "online" && isOffline()) {
|
|
1877
|
+
onPaused(true);
|
|
1878
|
+
await waitForOnline();
|
|
1879
|
+
onPaused(false);
|
|
1880
|
+
}
|
|
1881
|
+
try {
|
|
1882
|
+
return await send();
|
|
1883
|
+
} catch (cause) {
|
|
1884
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
1885
|
+
failures += 1;
|
|
1886
|
+
onFailure(failures, error);
|
|
1887
|
+
if (!shouldRetry(retry ?? 0, failures, error)) throw error;
|
|
1888
|
+
await sleep(retryDelayMs(retryDelay, failures - 1, error), controller.signal);
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
function waitForOnline() {
|
|
1893
|
+
if (typeof window === "undefined") return Promise.resolve();
|
|
1894
|
+
return new Promise((resolve) => {
|
|
1895
|
+
const onOnline = () => {
|
|
1896
|
+
window.removeEventListener("online", onOnline);
|
|
1897
|
+
resolve();
|
|
1898
|
+
};
|
|
1899
|
+
window.addEventListener("online", onOnline);
|
|
1900
|
+
});
|
|
1901
|
+
}
|
|
1902
|
+
var scopeChains = /* @__PURE__ */ new Map();
|
|
1903
|
+
function runScoped(id, work) {
|
|
1904
|
+
if (id === void 0) return work();
|
|
1905
|
+
const previous = scopeChains.get(id) ?? Promise.resolve();
|
|
1906
|
+
const next = previous.then(work, work);
|
|
1907
|
+
const settled = next.then(
|
|
1908
|
+
() => void 0,
|
|
1909
|
+
() => void 0
|
|
1910
|
+
);
|
|
1911
|
+
scopeChains.set(id, settled);
|
|
1912
|
+
void settled.then(() => {
|
|
1913
|
+
if (scopeChains.get(id) === settled) scopeChains.delete(id);
|
|
1914
|
+
});
|
|
1915
|
+
return next;
|
|
1916
|
+
}
|
|
1917
|
+
function resetMutationScopes() {
|
|
1918
|
+
scopeChains.clear();
|
|
1919
|
+
}
|
|
1920
|
+
function watchUntilSynced(collection, id, watchers, fire) {
|
|
1921
|
+
if (id === void 0) return;
|
|
1922
|
+
let stop;
|
|
1923
|
+
let done = false;
|
|
1924
|
+
const finish = () => {
|
|
1925
|
+
if (done) return;
|
|
1926
|
+
done = true;
|
|
1927
|
+
if (stop !== void 0) {
|
|
1928
|
+
watchers.current.delete(stop);
|
|
1929
|
+
stop();
|
|
1930
|
+
}
|
|
1931
|
+
};
|
|
1932
|
+
stop = collection.subscribe({ _id: id }, (docs) => {
|
|
1933
|
+
const doc = docs[0];
|
|
1934
|
+
if (doc === void 0) {
|
|
1935
|
+
finish();
|
|
1936
|
+
return;
|
|
1937
|
+
}
|
|
1938
|
+
if (doc._sync === "synced") {
|
|
1939
|
+
finish();
|
|
1940
|
+
fire();
|
|
1941
|
+
}
|
|
1942
|
+
});
|
|
1943
|
+
watchers.current.add(stop);
|
|
1944
|
+
}
|
|
1945
|
+
function toOperation(operation, variables) {
|
|
1946
|
+
const values = variables ?? {};
|
|
1947
|
+
if (operation === "insert") return { type: "insert", doc: values };
|
|
1948
|
+
const id = values._id;
|
|
1949
|
+
if (typeof id !== "string" || id === "") {
|
|
1950
|
+
throw new Error(
|
|
1951
|
+
`useMutation({ operation: '${operation}' }) needs an \`_id\` on the variables \u2014 mutate({ _id, \u2026 }). Writes are addressed by id, never by filter: a filter is re-evaluated at the primary tab against data the calling tab has not seen.`
|
|
1952
|
+
);
|
|
1953
|
+
}
|
|
1954
|
+
if (operation === "delete") return { type: "delete", where: { _id: id } };
|
|
1955
|
+
const { _id: _ignored, ...set } = values;
|
|
1956
|
+
return { type: "update", where: { _id: id }, set };
|
|
1957
|
+
}
|
|
1958
|
+
async function storeThenSend(args) {
|
|
1959
|
+
const { collection, op, url, method, mode, requestDrain, noteWrite } = args;
|
|
1960
|
+
const written = await writeLocal(collection, op, Date.now(), { url, method });
|
|
1961
|
+
if (mode === "optimistic") requestDrain();
|
|
1962
|
+
else noteWrite();
|
|
1963
|
+
const stored = await collection.findOne({ _id: written.id });
|
|
1964
|
+
return stored === null ? { _id: written.id } : withoutEnvelope(stored);
|
|
1965
|
+
}
|
|
1966
|
+
async function sendThenStore(args) {
|
|
1967
|
+
const { collection, name, op, url, method, backend, mutationFn, variables } = args;
|
|
1968
|
+
if (mutationFn !== void 0) {
|
|
1969
|
+
const returned = await mutationFn(variables);
|
|
1970
|
+
const id2 = returned?._id !== void 0 ? String(returned._id) : op.type === "insert" ? op.doc._id ?? newDocId(name) : op.where._id;
|
|
1971
|
+
if (op.type === "delete") {
|
|
1972
|
+
await collection.deleteOne({ _id: id2 });
|
|
1973
|
+
return { _id: id2 };
|
|
1974
|
+
}
|
|
1975
|
+
const stored2 = { ...returned, _id: id2 };
|
|
1976
|
+
await writeConfirmed(collection, stored2, Date.now());
|
|
1977
|
+
return stored2;
|
|
1978
|
+
}
|
|
1979
|
+
if (url === void 0) {
|
|
1980
|
+
throw new Error(
|
|
1981
|
+
"useMutation({ mode: 'immediate' }) needs a `url` \u2014 the request is sent from here, so there is nothing to route it by otherwise."
|
|
1982
|
+
);
|
|
1983
|
+
}
|
|
1984
|
+
const id = op.type === "insert" ? op.doc._id ?? newDocId(name) : op.where._id;
|
|
1985
|
+
const body = op.type === "delete" ? null : op.type === "insert" ? { ...op.doc, _id: id } : { ...op.set, _id: id };
|
|
1986
|
+
const verb = method?.[op.type] ?? backend?.method[op.type] ?? DEFAULT_METHODS[op.type];
|
|
1987
|
+
const headers = new Headers(backend?.headers ? await backend.headers() : void 0);
|
|
1988
|
+
if (body !== null && !headers.has("content-type")) {
|
|
1989
|
+
headers.set("content-type", "application/json");
|
|
1990
|
+
}
|
|
1991
|
+
const doFetch = backend?.fetch ?? globalThis.fetch;
|
|
1992
|
+
const response = await doFetch(resolveUrl(url, name, id, op.type), {
|
|
1993
|
+
method: verb,
|
|
1994
|
+
headers,
|
|
1995
|
+
body: body === null ? void 0 : JSON.stringify(body)
|
|
1996
|
+
});
|
|
1997
|
+
const outcome = backend?.classify ? backend.classify(response, { collection: name, id, type: op.type, document: body }) : defaultClassify(response);
|
|
1998
|
+
if (outcome === "retry" || outcome === "terminal") {
|
|
1999
|
+
throw new Error(
|
|
2000
|
+
`The server rejected this ${op.type} with status ${response.status}. Nothing was written locally.`
|
|
2001
|
+
);
|
|
2002
|
+
}
|
|
2003
|
+
if (op.type === "delete") {
|
|
2004
|
+
await collection.deleteOne({ _id: id });
|
|
2005
|
+
return { _id: id };
|
|
2006
|
+
}
|
|
2007
|
+
const canonical = await readJson(response);
|
|
2008
|
+
const fallback = op.type === "update" ? { ...withoutEnvelope(await collection.findOne({ _id: id }) ?? {}), ...body } : body;
|
|
2009
|
+
const stored = {
|
|
2010
|
+
...canonical && Object.keys(canonical).length > 0 ? canonical : fallback,
|
|
2011
|
+
_id: id
|
|
2012
|
+
};
|
|
2013
|
+
await writeConfirmed(collection, stored, Date.now());
|
|
2014
|
+
return stored;
|
|
2015
|
+
}
|
|
2016
|
+
function buildResult2(common, state) {
|
|
2017
|
+
if (state.status === "error") {
|
|
2018
|
+
return {
|
|
2019
|
+
...common,
|
|
2020
|
+
status: "error",
|
|
2021
|
+
data: void 0,
|
|
2022
|
+
error: state.error,
|
|
2023
|
+
variables: state.variables,
|
|
2024
|
+
isIdle: false,
|
|
2025
|
+
isPending: false,
|
|
2026
|
+
isSuccess: false,
|
|
2027
|
+
isError: true
|
|
2028
|
+
};
|
|
2029
|
+
}
|
|
2030
|
+
if (state.status === "success") {
|
|
2031
|
+
return {
|
|
2032
|
+
...common,
|
|
2033
|
+
status: "success",
|
|
2034
|
+
data: state.data,
|
|
2035
|
+
error: null,
|
|
2036
|
+
variables: state.variables,
|
|
2037
|
+
isIdle: false,
|
|
2038
|
+
isPending: false,
|
|
2039
|
+
isSuccess: true,
|
|
2040
|
+
isError: false
|
|
2041
|
+
};
|
|
2042
|
+
}
|
|
2043
|
+
if (state.status === "pending") {
|
|
2044
|
+
return {
|
|
2045
|
+
...common,
|
|
2046
|
+
status: "pending",
|
|
2047
|
+
data: void 0,
|
|
2048
|
+
error: null,
|
|
2049
|
+
variables: state.variables,
|
|
2050
|
+
isIdle: false,
|
|
2051
|
+
isPending: true,
|
|
2052
|
+
isSuccess: false,
|
|
2053
|
+
isError: false
|
|
2054
|
+
};
|
|
2055
|
+
}
|
|
2056
|
+
return {
|
|
2057
|
+
...common,
|
|
2058
|
+
status: "idle",
|
|
2059
|
+
data: void 0,
|
|
2060
|
+
error: null,
|
|
2061
|
+
variables: void 0,
|
|
2062
|
+
isIdle: true,
|
|
2063
|
+
isPending: false,
|
|
2064
|
+
isSuccess: false,
|
|
2065
|
+
isError: false
|
|
2066
|
+
};
|
|
2067
|
+
}
|
|
2068
|
+
async function readJson(response) {
|
|
2069
|
+
try {
|
|
2070
|
+
const parsed = await response.json();
|
|
2071
|
+
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
2072
|
+
} catch {
|
|
2073
|
+
return null;
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
// src/query/useSyncStatus.ts
|
|
2078
|
+
var import_react7 = require("react");
|
|
2079
|
+
var QUEUED = { _sync: { $in: ["pending", "failed"] } };
|
|
2080
|
+
function useSyncStatus() {
|
|
2081
|
+
const db = useTalaDB();
|
|
2082
|
+
const { collections, draining, requestDrain } = useQueryContext();
|
|
2083
|
+
const slicesRef = (0, import_react7.useRef)(/* @__PURE__ */ new Map());
|
|
2084
|
+
const [version, bump] = (0, import_react7.useState)(0);
|
|
2085
|
+
const [online, setOnline] = (0, import_react7.useState)(
|
|
2086
|
+
() => typeof navigator === "undefined" ? true : navigator.onLine
|
|
2087
|
+
);
|
|
2088
|
+
(0, import_react7.useEffect)(() => {
|
|
2089
|
+
if (typeof window === "undefined") return;
|
|
2090
|
+
const update = () => setOnline(navigator.onLine);
|
|
2091
|
+
window.addEventListener("online", update);
|
|
2092
|
+
window.addEventListener("offline", update);
|
|
2093
|
+
return () => {
|
|
2094
|
+
window.removeEventListener("online", update);
|
|
2095
|
+
window.removeEventListener("offline", update);
|
|
2096
|
+
};
|
|
2097
|
+
}, []);
|
|
2098
|
+
(0, import_react7.useEffect)(() => {
|
|
2099
|
+
const unsubscribes = collections.map((name) => {
|
|
2100
|
+
const collection = db.collection(name);
|
|
2101
|
+
return collection.subscribe(QUEUED, (docs) => {
|
|
2102
|
+
slicesRef.current.set(name, summarise(name, docs));
|
|
2103
|
+
bump((n) => n + 1);
|
|
2104
|
+
});
|
|
2105
|
+
});
|
|
2106
|
+
return () => {
|
|
2107
|
+
for (const unsubscribe of unsubscribes) unsubscribe();
|
|
2108
|
+
};
|
|
2109
|
+
}, [db, collections]);
|
|
2110
|
+
const { pending, failed } = (0, import_react7.useMemo)(() => {
|
|
2111
|
+
let total = 0;
|
|
2112
|
+
const failures = [];
|
|
2113
|
+
for (const slice of slicesRef.current.values()) {
|
|
2114
|
+
total += slice.pending;
|
|
2115
|
+
failures.push(...slice.failed);
|
|
2116
|
+
}
|
|
2117
|
+
return { pending: total, failed: failures };
|
|
2118
|
+
}, [version]);
|
|
2119
|
+
const findCollection = (0, import_react7.useCallback)(
|
|
2120
|
+
async (id) => {
|
|
2121
|
+
for (const name of collections) {
|
|
2122
|
+
const collection = db.collection(name);
|
|
2123
|
+
if (await collection.findOne({ _id: id }) !== null) return collection;
|
|
2124
|
+
}
|
|
2125
|
+
return null;
|
|
2126
|
+
},
|
|
2127
|
+
[db, collections]
|
|
2128
|
+
);
|
|
2129
|
+
const retry = (0, import_react7.useCallback)(
|
|
2130
|
+
(id) => {
|
|
2131
|
+
void (async () => {
|
|
2132
|
+
const collection = await findCollection(id);
|
|
2133
|
+
if (collection === null) return;
|
|
2134
|
+
await collection.updateOne(
|
|
2135
|
+
{ _id: id },
|
|
2136
|
+
{ $set: { _sync: "pending", _attempt: 0, _error: null, _retry_at: 0 } }
|
|
2137
|
+
);
|
|
2138
|
+
requestDrain(true);
|
|
2139
|
+
})();
|
|
2140
|
+
},
|
|
2141
|
+
[findCollection, requestDrain]
|
|
2142
|
+
);
|
|
2143
|
+
const discard = (0, import_react7.useCallback)(
|
|
2144
|
+
(id) => {
|
|
2145
|
+
void (async () => {
|
|
2146
|
+
const collection = await findCollection(id);
|
|
2147
|
+
if (collection === null) return;
|
|
2148
|
+
const doc = await collection.findOne({ _id: id });
|
|
2149
|
+
if (doc === null) return;
|
|
2150
|
+
if (doc._op === "insert") {
|
|
2151
|
+
await collection.deleteOne({ _id: id });
|
|
2152
|
+
} else {
|
|
2153
|
+
await collection.updateOne(
|
|
2154
|
+
{ _id: id },
|
|
2155
|
+
{ $set: { _sync: "synced", _op: null, _attempt: 0, _error: null, _retry_at: 0 } }
|
|
2156
|
+
);
|
|
2157
|
+
}
|
|
2158
|
+
})();
|
|
2159
|
+
},
|
|
2160
|
+
[findCollection]
|
|
2161
|
+
);
|
|
2162
|
+
return { pending, failed, draining, online, retry, discard };
|
|
2163
|
+
}
|
|
2164
|
+
function summarise(collection, docs) {
|
|
2165
|
+
let pending = 0;
|
|
2166
|
+
const failed = [];
|
|
2167
|
+
for (const doc of docs) {
|
|
2168
|
+
if (doc._sync === "pending") {
|
|
2169
|
+
pending++;
|
|
2170
|
+
} else if (doc._sync === "failed") {
|
|
2171
|
+
failed.push({
|
|
2172
|
+
collection,
|
|
2173
|
+
id: doc._id,
|
|
2174
|
+
op: doc._op ?? "update",
|
|
2175
|
+
error: doc._error ?? "Unknown error",
|
|
2176
|
+
attempts: doc._attempt ?? 0
|
|
2177
|
+
});
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
return { pending, failed };
|
|
2181
|
+
}
|
|
2182
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2183
|
+
0 && (module.exports = {
|
|
2184
|
+
DEFAULT_METHODS,
|
|
2185
|
+
DEFAULT_RETRY,
|
|
2186
|
+
DEFAULT_STALE_TIME,
|
|
2187
|
+
ENVELOPE_FIELDS,
|
|
2188
|
+
ENVELOPE_INDEXES,
|
|
2189
|
+
QUERY_COLLECTION,
|
|
2190
|
+
QueryProvider,
|
|
2191
|
+
backoffMs,
|
|
2192
|
+
cancelShared,
|
|
2193
|
+
checkClassification,
|
|
2194
|
+
checkMutationFilter,
|
|
2195
|
+
checkResponseBody,
|
|
2196
|
+
checkResponseId,
|
|
2197
|
+
coalesce,
|
|
2198
|
+
contains,
|
|
2199
|
+
defaultClassify,
|
|
2200
|
+
defineParams,
|
|
2201
|
+
deleteQueryRecord,
|
|
2202
|
+
drainOnce,
|
|
2203
|
+
ensureEnvelopeIndexes,
|
|
2204
|
+
eq,
|
|
2205
|
+
extractDocuments,
|
|
2206
|
+
gt,
|
|
2207
|
+
gte,
|
|
2208
|
+
hashQueryKey,
|
|
2209
|
+
hydrate,
|
|
2210
|
+
inflightKey,
|
|
2211
|
+
isBoundParams,
|
|
2212
|
+
isDev,
|
|
2213
|
+
isOffline,
|
|
2214
|
+
isShared,
|
|
2215
|
+
isStale,
|
|
2216
|
+
keepPreviousData,
|
|
2217
|
+
lt,
|
|
2218
|
+
lte,
|
|
2219
|
+
ne,
|
|
2220
|
+
newDocId,
|
|
2221
|
+
normalizeParams,
|
|
2222
|
+
noteIgnoredOptions,
|
|
2223
|
+
oneOf,
|
|
2224
|
+
openQueryCollection,
|
|
2225
|
+
queryRecordId,
|
|
2226
|
+
readQueryRecord,
|
|
2227
|
+
replaceEqualDeep,
|
|
2228
|
+
reservedFieldsIn,
|
|
2229
|
+
resetCollectionWarnings,
|
|
2230
|
+
resetForgetTimers,
|
|
2231
|
+
resetInflight,
|
|
2232
|
+
resetMutationScopes,
|
|
2233
|
+
resetWarnings,
|
|
2234
|
+
resolveCollectionName,
|
|
2235
|
+
resolveUrl,
|
|
2236
|
+
retryDelayMs,
|
|
2237
|
+
runShared,
|
|
2238
|
+
sendWrite,
|
|
2239
|
+
shape,
|
|
2240
|
+
shouldRetry,
|
|
2241
|
+
stampPending,
|
|
2242
|
+
stampSynced,
|
|
2243
|
+
toPendingWrite,
|
|
2244
|
+
useMutation,
|
|
2245
|
+
useQuery,
|
|
2246
|
+
useQueryContext,
|
|
2247
|
+
useSyncStatus,
|
|
2248
|
+
warnOnce,
|
|
2249
|
+
writeConfirmed,
|
|
2250
|
+
writeLocal,
|
|
2251
|
+
writeQueryRecord
|
|
2252
|
+
});
|