@rpcbase/client 0.390.0 → 0.392.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +2 -2
- package/dist/rts/index.js +1 -1
- package/dist/rts/populateCache.d.ts +21 -0
- package/dist/rts/populateCache.d.ts.map +1 -0
- package/dist/rts/pouchStore.d.ts +21 -0
- package/dist/rts/pouchStore.d.ts.map +1 -1
- package/dist/rts/useQuery.d.ts.map +1 -1
- package/dist/rts/wsClient.d.ts.map +1 -1
- package/dist/{useQuery-ClKeTB8S.js → useQuery-CUjwBdkK.js} +644 -49
- package/dist/useQuery-CUjwBdkK.js.map +1 -0
- package/package.json +1 -1
- package/dist/useQuery-ClKeTB8S.js.map +0 -1
|
@@ -298,6 +298,140 @@ const satisfiesProjection = (doc, projection) => {
|
|
|
298
298
|
}
|
|
299
299
|
return true;
|
|
300
300
|
};
|
|
301
|
+
const getValueAtPath = (doc, path) => {
|
|
302
|
+
const parts = path.split(".").map((part) => part.trim()).filter(Boolean);
|
|
303
|
+
if (!parts.length) return void 0;
|
|
304
|
+
let current = doc;
|
|
305
|
+
for (const part of parts) {
|
|
306
|
+
if (!current || typeof current !== "object" || Array.isArray(current)) return void 0;
|
|
307
|
+
current = current[part];
|
|
308
|
+
}
|
|
309
|
+
return current;
|
|
310
|
+
};
|
|
311
|
+
const setValueAtPath = (doc, path, value) => {
|
|
312
|
+
const parts = path.split(".").map((part) => part.trim()).filter(Boolean);
|
|
313
|
+
if (!parts.length) return;
|
|
314
|
+
let current = doc;
|
|
315
|
+
for (let i = 0; i < parts.length - 1; i += 1) {
|
|
316
|
+
const key = parts[i];
|
|
317
|
+
const existing = current[key];
|
|
318
|
+
if (!existing || typeof existing !== "object" || Array.isArray(existing)) {
|
|
319
|
+
current[key] = {};
|
|
320
|
+
}
|
|
321
|
+
current = current[key];
|
|
322
|
+
}
|
|
323
|
+
current[parts[parts.length - 1]] = value;
|
|
324
|
+
};
|
|
325
|
+
const unsetValueAtPath = (doc, path) => {
|
|
326
|
+
const parts = path.split(".").map((part) => part.trim()).filter(Boolean);
|
|
327
|
+
if (!parts.length) return;
|
|
328
|
+
let current = doc;
|
|
329
|
+
for (let i = 0; i < parts.length - 1; i += 1) {
|
|
330
|
+
const key = parts[i];
|
|
331
|
+
const next = current[key];
|
|
332
|
+
if (!next || typeof next !== "object" || Array.isArray(next)) return;
|
|
333
|
+
current = next;
|
|
334
|
+
}
|
|
335
|
+
delete current[parts[parts.length - 1]];
|
|
336
|
+
};
|
|
337
|
+
const cloneDoc = (doc) => {
|
|
338
|
+
try {
|
|
339
|
+
return structuredClone(doc);
|
|
340
|
+
} catch {
|
|
341
|
+
return JSON.parse(JSON.stringify(doc));
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
const toProjectionSpec = (projection) => {
|
|
345
|
+
const spec = {};
|
|
346
|
+
for (const [key, value] of Object.entries(projection)) {
|
|
347
|
+
const path = key.trim();
|
|
348
|
+
if (!path) continue;
|
|
349
|
+
if (value === 1 || value === 0) {
|
|
350
|
+
spec[path] = value;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return spec;
|
|
354
|
+
};
|
|
355
|
+
const applyProjection = (doc, projection) => {
|
|
356
|
+
const spec = toProjectionSpec(projection);
|
|
357
|
+
const includeKeys = Object.keys(spec).filter((key) => spec[key] === 1);
|
|
358
|
+
const excludeKeys = Object.keys(spec).filter((key) => spec[key] === 0);
|
|
359
|
+
if (includeKeys.length > 0) {
|
|
360
|
+
const projected2 = {};
|
|
361
|
+
for (const key of includeKeys) {
|
|
362
|
+
const value = getValueAtPath(doc, key);
|
|
363
|
+
if (value !== void 0) setValueAtPath(projected2, key, value);
|
|
364
|
+
}
|
|
365
|
+
if (spec._id !== 0 && Object.hasOwn(doc, "_id")) {
|
|
366
|
+
projected2._id = doc._id;
|
|
367
|
+
}
|
|
368
|
+
return projected2;
|
|
369
|
+
}
|
|
370
|
+
const projected = cloneDoc(doc);
|
|
371
|
+
for (const key of excludeKeys) {
|
|
372
|
+
unsetValueAtPath(projected, key);
|
|
373
|
+
}
|
|
374
|
+
return projected;
|
|
375
|
+
};
|
|
376
|
+
const compareSort = (a, b, sort) => {
|
|
377
|
+
for (const key of Object.keys(sort)) {
|
|
378
|
+
const dir = sort[key];
|
|
379
|
+
const aVal = getValueAtPath(a, key);
|
|
380
|
+
const bVal = getValueAtPath(b, key);
|
|
381
|
+
if (typeof aVal === "number" && typeof bVal === "number") {
|
|
382
|
+
if (aVal < bVal) return -1 * dir;
|
|
383
|
+
if (aVal > bVal) return 1 * dir;
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
if (typeof aVal === "string" && typeof bVal === "string") {
|
|
387
|
+
if (aVal < bVal) return -1 * dir;
|
|
388
|
+
if (aVal > bVal) return 1 * dir;
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return 0;
|
|
393
|
+
};
|
|
394
|
+
const extractDocId = (value) => {
|
|
395
|
+
if (typeof value === "string") return value;
|
|
396
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return "";
|
|
397
|
+
const id = value._id;
|
|
398
|
+
return typeof id === "string" ? id : "";
|
|
399
|
+
};
|
|
400
|
+
const matchValue = (docValue, matchValueRaw) => {
|
|
401
|
+
if (Array.isArray(matchValueRaw)) {
|
|
402
|
+
if (!Array.isArray(docValue)) return false;
|
|
403
|
+
if (docValue.length !== matchValueRaw.length) return false;
|
|
404
|
+
for (let i = 0; i < matchValueRaw.length; i += 1) {
|
|
405
|
+
const matched = matchValue(docValue[i], matchValueRaw[i]);
|
|
406
|
+
if (matched === null) return null;
|
|
407
|
+
if (!matched) return false;
|
|
408
|
+
}
|
|
409
|
+
return true;
|
|
410
|
+
}
|
|
411
|
+
if (matchValueRaw && typeof matchValueRaw === "object") {
|
|
412
|
+
const matchObj = matchValueRaw;
|
|
413
|
+
if (Object.keys(matchObj).some((key) => key.startsWith("$"))) return null;
|
|
414
|
+
if (!docValue || typeof docValue !== "object" || Array.isArray(docValue)) return false;
|
|
415
|
+
const docObj = docValue;
|
|
416
|
+
for (const key of Object.keys(matchObj)) {
|
|
417
|
+
const matched = matchValue(docObj[key], matchObj[key]);
|
|
418
|
+
if (matched === null) return null;
|
|
419
|
+
if (!matched) return false;
|
|
420
|
+
}
|
|
421
|
+
return true;
|
|
422
|
+
}
|
|
423
|
+
return Object.is(docValue, matchValueRaw);
|
|
424
|
+
};
|
|
425
|
+
const matchesSimpleQuery = (doc, query) => {
|
|
426
|
+
for (const [key, expected] of Object.entries(query)) {
|
|
427
|
+
if (key.startsWith("$")) return null;
|
|
428
|
+
const actual = getValueAtPath(doc, key);
|
|
429
|
+
const matched = matchValue(actual, expected);
|
|
430
|
+
if (matched === null) return null;
|
|
431
|
+
if (!matched) return false;
|
|
432
|
+
}
|
|
433
|
+
return true;
|
|
434
|
+
};
|
|
301
435
|
const remapDocFromStorage = (doc) => {
|
|
302
436
|
const next = {};
|
|
303
437
|
for (const [key, value] of Object.entries(doc)) {
|
|
@@ -306,10 +440,11 @@ const remapDocFromStorage = (doc) => {
|
|
|
306
440
|
}
|
|
307
441
|
return next;
|
|
308
442
|
};
|
|
309
|
-
const
|
|
443
|
+
const runQueryInternal = async ({
|
|
310
444
|
modelName,
|
|
311
445
|
query = {},
|
|
312
|
-
options
|
|
446
|
+
options,
|
|
447
|
+
strictProjection = false
|
|
313
448
|
}) => {
|
|
314
449
|
const collection = await getCollection(modelName, {
|
|
315
450
|
uid: options.uid
|
|
@@ -322,39 +457,291 @@ const runQuery = async ({
|
|
|
322
457
|
selector: replacedQuery,
|
|
323
458
|
limit
|
|
324
459
|
});
|
|
325
|
-
|
|
460
|
+
const mappedDocs = docs.map(({
|
|
326
461
|
_rev: _revIgnored,
|
|
327
462
|
...rest
|
|
328
463
|
}) => remapDocFromStorage(rest));
|
|
464
|
+
let filteredDocs = mappedDocs;
|
|
329
465
|
if (options.projection) {
|
|
330
|
-
|
|
466
|
+
if (strictProjection && mappedDocs.some((entry) => !satisfiesProjection(entry, options.projection))) {
|
|
467
|
+
return {
|
|
468
|
+
data: [],
|
|
469
|
+
context: {
|
|
470
|
+
source: "cache"
|
|
471
|
+
},
|
|
472
|
+
projectionMismatch: true
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
filteredDocs = filteredDocs.filter((entry) => satisfiesProjection(entry, options.projection));
|
|
331
476
|
}
|
|
477
|
+
let result = filteredDocs;
|
|
332
478
|
if (options.sort) {
|
|
333
|
-
|
|
334
|
-
for (const key of Object.keys(options.sort)) {
|
|
335
|
-
if (!Object.hasOwn(a, key) || !Object.hasOwn(b, key)) continue;
|
|
336
|
-
const dir = options.sort[key];
|
|
337
|
-
const aVal = a[key];
|
|
338
|
-
const bVal = b[key];
|
|
339
|
-
if (typeof aVal === "number" && typeof bVal === "number") {
|
|
340
|
-
if (aVal < bVal) return -1 * dir;
|
|
341
|
-
if (aVal > bVal) return 1 * dir;
|
|
342
|
-
continue;
|
|
343
|
-
}
|
|
344
|
-
if (typeof aVal === "string" && typeof bVal === "string") {
|
|
345
|
-
if (aVal < bVal) return -1 * dir;
|
|
346
|
-
if (aVal > bVal) return 1 * dir;
|
|
347
|
-
continue;
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
return 0;
|
|
351
|
-
});
|
|
479
|
+
result = result.sort((a, b) => compareSort(a, b, options.sort));
|
|
352
480
|
}
|
|
353
481
|
return {
|
|
354
|
-
data:
|
|
482
|
+
data: result,
|
|
355
483
|
context: {
|
|
356
484
|
source: "cache"
|
|
485
|
+
},
|
|
486
|
+
projectionMismatch: false
|
|
487
|
+
};
|
|
488
|
+
};
|
|
489
|
+
const runQuery = async ({
|
|
490
|
+
modelName,
|
|
491
|
+
query = {},
|
|
492
|
+
options
|
|
493
|
+
}) => {
|
|
494
|
+
const result = await runQueryInternal({
|
|
495
|
+
modelName,
|
|
496
|
+
query,
|
|
497
|
+
options
|
|
498
|
+
});
|
|
499
|
+
return {
|
|
500
|
+
data: result.data,
|
|
501
|
+
context: result.context
|
|
502
|
+
};
|
|
503
|
+
};
|
|
504
|
+
const addWriteDoc = (writes, modelName, doc) => {
|
|
505
|
+
const docsById = writes.get(modelName) ?? /* @__PURE__ */ new Map();
|
|
506
|
+
docsById.set(doc._id, doc);
|
|
507
|
+
writes.set(modelName, docsById);
|
|
508
|
+
};
|
|
509
|
+
const sanitizePopulatedDoc = (doc, populate, writes) => {
|
|
510
|
+
const next = cloneDoc(doc);
|
|
511
|
+
for (const entry of populate) {
|
|
512
|
+
const value = getValueAtPath(next, entry.path);
|
|
513
|
+
if (value === void 0) continue;
|
|
514
|
+
if (Array.isArray(value)) {
|
|
515
|
+
const ids = [];
|
|
516
|
+
for (const candidate of value) {
|
|
517
|
+
const id2 = extractDocId(candidate);
|
|
518
|
+
if (!id2) continue;
|
|
519
|
+
ids.push(id2);
|
|
520
|
+
if (!entry.model) continue;
|
|
521
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) continue;
|
|
522
|
+
const candidateDoc = candidate;
|
|
523
|
+
const nested2 = entry.populate?.length ? sanitizePopulatedDoc(candidateDoc, entry.populate, writes) : cloneDoc(candidateDoc);
|
|
524
|
+
addWriteDoc(writes, entry.model, nested2);
|
|
525
|
+
}
|
|
526
|
+
setValueAtPath(next, entry.path, ids);
|
|
527
|
+
continue;
|
|
357
528
|
}
|
|
529
|
+
const id = extractDocId(value);
|
|
530
|
+
setValueAtPath(next, entry.path, id || null);
|
|
531
|
+
if (!id) continue;
|
|
532
|
+
if (!entry.model) continue;
|
|
533
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) continue;
|
|
534
|
+
const valueDoc = value;
|
|
535
|
+
const nested = entry.populate?.length ? sanitizePopulatedDoc(valueDoc, entry.populate, writes) : cloneDoc(valueDoc);
|
|
536
|
+
addWriteDoc(writes, entry.model, nested);
|
|
537
|
+
}
|
|
538
|
+
return next;
|
|
539
|
+
};
|
|
540
|
+
const updatePopulatedDocs = async ({
|
|
541
|
+
modelName,
|
|
542
|
+
data,
|
|
543
|
+
uid,
|
|
544
|
+
populate
|
|
545
|
+
}) => {
|
|
546
|
+
if (!data.length) return;
|
|
547
|
+
const writes = /* @__PURE__ */ new Map();
|
|
548
|
+
const roots = data.map((doc) => sanitizePopulatedDoc(doc, populate, writes));
|
|
549
|
+
await updateDocs(modelName, roots, uid);
|
|
550
|
+
for (const [targetModelName, docsById] of writes.entries()) {
|
|
551
|
+
if (!docsById.size) continue;
|
|
552
|
+
await updateDocs(targetModelName, Array.from(docsById.values()), uid);
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
const loadProjectedDocsByIds = async ({
|
|
556
|
+
modelName,
|
|
557
|
+
ids,
|
|
558
|
+
uid,
|
|
559
|
+
projection
|
|
560
|
+
}) => {
|
|
561
|
+
const uniqueIds = Array.from(new Set(ids.filter(Boolean)));
|
|
562
|
+
if (!uniqueIds.length) {
|
|
563
|
+
return {
|
|
564
|
+
rawDocsById: /* @__PURE__ */ new Map(),
|
|
565
|
+
projectedDocsById: /* @__PURE__ */ new Map(),
|
|
566
|
+
projectionMismatch: false
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
const collection = await getCollection(modelName, {
|
|
570
|
+
uid
|
|
571
|
+
});
|
|
572
|
+
const {
|
|
573
|
+
docs
|
|
574
|
+
} = await collection.find({
|
|
575
|
+
selector: {
|
|
576
|
+
_id: {
|
|
577
|
+
$in: uniqueIds
|
|
578
|
+
}
|
|
579
|
+
},
|
|
580
|
+
limit: uniqueIds.length
|
|
581
|
+
});
|
|
582
|
+
const rawDocsById = /* @__PURE__ */ new Map();
|
|
583
|
+
const projectedDocsById = /* @__PURE__ */ new Map();
|
|
584
|
+
let projectionMismatch = false;
|
|
585
|
+
for (const rawDoc of docs) {
|
|
586
|
+
const id = typeof rawDoc._id === "string" ? rawDoc._id : "";
|
|
587
|
+
if (!id) continue;
|
|
588
|
+
const {
|
|
589
|
+
_rev: _revIgnored,
|
|
590
|
+
...rest
|
|
591
|
+
} = rawDoc;
|
|
592
|
+
const remapped = remapDocFromStorage(rest);
|
|
593
|
+
if (!satisfiesProjection(remapped, projection)) {
|
|
594
|
+
projectionMismatch = true;
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
rawDocsById.set(id, remapped);
|
|
598
|
+
projectedDocsById.set(id, applyProjection(remapped, projection));
|
|
599
|
+
}
|
|
600
|
+
if (projectedDocsById.size < uniqueIds.length) {
|
|
601
|
+
projectionMismatch = true;
|
|
602
|
+
}
|
|
603
|
+
return {
|
|
604
|
+
rawDocsById,
|
|
605
|
+
projectedDocsById,
|
|
606
|
+
projectionMismatch
|
|
607
|
+
};
|
|
608
|
+
};
|
|
609
|
+
const hydratePopulateEntries = async ({
|
|
610
|
+
docs,
|
|
611
|
+
populate,
|
|
612
|
+
uid
|
|
613
|
+
}) => {
|
|
614
|
+
const currentDocs = docs;
|
|
615
|
+
for (const entry of populate) {
|
|
616
|
+
if (!entry.model) {
|
|
617
|
+
return {
|
|
618
|
+
hit: false,
|
|
619
|
+
data: []
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
const descriptors = currentDocs.map((doc) => {
|
|
623
|
+
const rawValue = getValueAtPath(doc, entry.path);
|
|
624
|
+
const isArray = Array.isArray(rawValue);
|
|
625
|
+
const ids = (isArray ? rawValue : [rawValue]).map((candidate) => extractDocId(candidate)).filter(Boolean);
|
|
626
|
+
return {
|
|
627
|
+
doc,
|
|
628
|
+
hasValue: rawValue !== void 0,
|
|
629
|
+
isArray,
|
|
630
|
+
ids
|
|
631
|
+
};
|
|
632
|
+
});
|
|
633
|
+
const allIds = descriptors.flatMap((descriptor) => descriptor.ids);
|
|
634
|
+
const loaded = await loadProjectedDocsByIds({
|
|
635
|
+
modelName: entry.model,
|
|
636
|
+
ids: allIds,
|
|
637
|
+
uid,
|
|
638
|
+
projection: entry.select
|
|
639
|
+
});
|
|
640
|
+
if (loaded.projectionMismatch) {
|
|
641
|
+
return {
|
|
642
|
+
hit: false,
|
|
643
|
+
data: []
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
let populatedDocsById = loaded.projectedDocsById;
|
|
647
|
+
if (entry.match) {
|
|
648
|
+
const filtered = /* @__PURE__ */ new Map();
|
|
649
|
+
for (const [id, candidate] of loaded.rawDocsById.entries()) {
|
|
650
|
+
const matched = matchesSimpleQuery(candidate, entry.match);
|
|
651
|
+
if (matched === null) return {
|
|
652
|
+
hit: false,
|
|
653
|
+
data: []
|
|
654
|
+
};
|
|
655
|
+
if (!matched) continue;
|
|
656
|
+
const projected = populatedDocsById.get(id);
|
|
657
|
+
if (projected) filtered.set(id, projected);
|
|
658
|
+
}
|
|
659
|
+
populatedDocsById = filtered;
|
|
660
|
+
}
|
|
661
|
+
if (entry.populate?.length) {
|
|
662
|
+
const nestedResult = await hydratePopulateEntries({
|
|
663
|
+
docs: Array.from(populatedDocsById.values()).map((candidate) => cloneDoc(candidate)),
|
|
664
|
+
populate: entry.populate,
|
|
665
|
+
uid
|
|
666
|
+
});
|
|
667
|
+
if (!nestedResult.hit) {
|
|
668
|
+
return {
|
|
669
|
+
hit: false,
|
|
670
|
+
data: []
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
populatedDocsById = nestedResult.data.reduce((acc, candidate) => {
|
|
674
|
+
const id = extractDocId(candidate);
|
|
675
|
+
if (id) acc.set(id, candidate);
|
|
676
|
+
return acc;
|
|
677
|
+
}, /* @__PURE__ */ new Map());
|
|
678
|
+
}
|
|
679
|
+
for (const descriptor of descriptors) {
|
|
680
|
+
if (!descriptor.hasValue) continue;
|
|
681
|
+
if (!descriptor.ids.length) {
|
|
682
|
+
setValueAtPath(descriptor.doc, entry.path, descriptor.isArray ? [] : null);
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
if (descriptor.isArray) {
|
|
686
|
+
let values = descriptor.ids.map((id) => populatedDocsById.get(id)).filter((candidate) => Boolean(candidate));
|
|
687
|
+
if (entry.options?.sort) {
|
|
688
|
+
values = values.sort((a, b) => compareSort(a, b, entry.options.sort));
|
|
689
|
+
}
|
|
690
|
+
if (typeof entry.options?.limit === "number" && Number.isFinite(entry.options.limit)) {
|
|
691
|
+
values = values.slice(0, Math.max(0, Math.floor(Math.abs(entry.options.limit))));
|
|
692
|
+
}
|
|
693
|
+
setValueAtPath(descriptor.doc, entry.path, values);
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
696
|
+
const value = populatedDocsById.get(descriptor.ids[0]);
|
|
697
|
+
setValueAtPath(descriptor.doc, entry.path, value ?? null);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
return {
|
|
701
|
+
hit: true,
|
|
702
|
+
data: currentDocs
|
|
703
|
+
};
|
|
704
|
+
};
|
|
705
|
+
const runPopulatedQuery = async ({
|
|
706
|
+
modelName,
|
|
707
|
+
query = {},
|
|
708
|
+
options
|
|
709
|
+
}) => {
|
|
710
|
+
const rootResult = await runQueryInternal({
|
|
711
|
+
modelName,
|
|
712
|
+
query,
|
|
713
|
+
options: {
|
|
714
|
+
uid: options.uid,
|
|
715
|
+
projection: options.projection,
|
|
716
|
+
sort: options.sort,
|
|
717
|
+
limit: options.limit
|
|
718
|
+
},
|
|
719
|
+
strictProjection: true
|
|
720
|
+
});
|
|
721
|
+
if (rootResult.projectionMismatch) {
|
|
722
|
+
return {
|
|
723
|
+
hit: false,
|
|
724
|
+
data: [],
|
|
725
|
+
context: rootResult.context
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
const projectedRoots = rootResult.data.map((doc) => applyProjection(doc, options.projection));
|
|
729
|
+
const hydrated = await hydratePopulateEntries({
|
|
730
|
+
docs: projectedRoots.map((doc) => cloneDoc(doc)),
|
|
731
|
+
populate: options.populate,
|
|
732
|
+
uid: options.uid
|
|
733
|
+
});
|
|
734
|
+
if (!hydrated.hit) {
|
|
735
|
+
return {
|
|
736
|
+
hit: false,
|
|
737
|
+
data: [],
|
|
738
|
+
context: rootResult.context
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
return {
|
|
742
|
+
hit: true,
|
|
743
|
+
data: hydrated.data,
|
|
744
|
+
context: rootResult.context
|
|
358
745
|
};
|
|
359
746
|
};
|
|
360
747
|
const updateDocs = async (modelName, data, uid) => {
|
|
@@ -493,6 +880,158 @@ const destroyAllCollections = async () => {
|
|
|
493
880
|
await Promise.all(dbs.map((db) => db.destroy()));
|
|
494
881
|
collections.clear();
|
|
495
882
|
};
|
|
883
|
+
const EXCLUDE_PROJECTION_ERROR = "must be include-only (value 1); exclusion projection is not supported";
|
|
884
|
+
const sortProjectionSpec = (projection) => {
|
|
885
|
+
const sorted = {};
|
|
886
|
+
for (const key of Object.keys(projection).sort()) {
|
|
887
|
+
sorted[key] = projection[key];
|
|
888
|
+
}
|
|
889
|
+
return sorted;
|
|
890
|
+
};
|
|
891
|
+
const normalizeProjectionSpec = (value, source, label) => {
|
|
892
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
893
|
+
const raw = value;
|
|
894
|
+
const normalized = {};
|
|
895
|
+
let hasExclude = false;
|
|
896
|
+
for (const [key, rawValue] of Object.entries(raw)) {
|
|
897
|
+
const path = key.trim();
|
|
898
|
+
if (!path) continue;
|
|
899
|
+
if (rawValue === 1 || rawValue === true) {
|
|
900
|
+
normalized[path] = 1;
|
|
901
|
+
continue;
|
|
902
|
+
}
|
|
903
|
+
if (rawValue === 0 || rawValue === false) {
|
|
904
|
+
hasExclude = true;
|
|
905
|
+
continue;
|
|
906
|
+
}
|
|
907
|
+
if (typeof rawValue === "number" && Number.isFinite(rawValue)) {
|
|
908
|
+
if (rawValue === 1) normalized[path] = 1;
|
|
909
|
+
if (rawValue === 0) hasExclude = true;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
if (hasExclude) {
|
|
913
|
+
throw new Error(`${source}: ${label} ${EXCLUDE_PROJECTION_ERROR}`);
|
|
914
|
+
}
|
|
915
|
+
return Object.keys(normalized).length > 0 ? sortProjectionSpec(normalized) : void 0;
|
|
916
|
+
};
|
|
917
|
+
const normalizeSelectString = (value, source) => {
|
|
918
|
+
const tokens = value.split(/\s+/).map((token) => token.trim()).filter(Boolean);
|
|
919
|
+
if (!tokens.length) return void 0;
|
|
920
|
+
const normalized = {};
|
|
921
|
+
for (const token of tokens) {
|
|
922
|
+
let path = token;
|
|
923
|
+
if (token.startsWith("-")) {
|
|
924
|
+
throw new Error(`${source}: populate select ${EXCLUDE_PROJECTION_ERROR}`);
|
|
925
|
+
} else if (token.startsWith("+")) {
|
|
926
|
+
path = token.slice(1);
|
|
927
|
+
}
|
|
928
|
+
path = path.trim();
|
|
929
|
+
if (!path) continue;
|
|
930
|
+
normalized[path] = 1;
|
|
931
|
+
}
|
|
932
|
+
return Object.keys(normalized).length > 0 ? sortProjectionSpec(normalized) : void 0;
|
|
933
|
+
};
|
|
934
|
+
const normalizePopulateSelect = (value, source) => {
|
|
935
|
+
if (typeof value === "string") {
|
|
936
|
+
return normalizeSelectString(value, source);
|
|
937
|
+
}
|
|
938
|
+
return normalizeProjectionSpec(value, source, "populate select");
|
|
939
|
+
};
|
|
940
|
+
const normalizePopulateOptions = (value) => {
|
|
941
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
942
|
+
const raw = value;
|
|
943
|
+
const normalized = {};
|
|
944
|
+
if (raw.sort && typeof raw.sort === "object" && !Array.isArray(raw.sort)) {
|
|
945
|
+
const sortRaw = raw.sort;
|
|
946
|
+
const sort = {};
|
|
947
|
+
for (const [key, rawDirection] of Object.entries(sortRaw)) {
|
|
948
|
+
const path = key.trim();
|
|
949
|
+
if (!path) continue;
|
|
950
|
+
if (rawDirection === 1 || rawDirection === "asc") {
|
|
951
|
+
sort[path] = 1;
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
954
|
+
if (rawDirection === -1 || rawDirection === "desc") {
|
|
955
|
+
sort[path] = -1;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
if (Object.keys(sort).length > 0) {
|
|
959
|
+
normalized.sort = sort;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
if (typeof raw.limit === "number" && Number.isFinite(raw.limit)) {
|
|
963
|
+
normalized.limit = Math.max(0, Math.floor(Math.abs(raw.limit)));
|
|
964
|
+
}
|
|
965
|
+
return Object.keys(normalized).length > 0 ? normalized : void 0;
|
|
966
|
+
};
|
|
967
|
+
const normalizeString = (value) => {
|
|
968
|
+
if (typeof value !== "string") return void 0;
|
|
969
|
+
const normalized = value.trim();
|
|
970
|
+
return normalized || void 0;
|
|
971
|
+
};
|
|
972
|
+
const normalizeObject = (value) => {
|
|
973
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
974
|
+
return value;
|
|
975
|
+
};
|
|
976
|
+
const normalizePopulateObject = (value, source) => {
|
|
977
|
+
const path = normalizeString(value.path);
|
|
978
|
+
if (!path) {
|
|
979
|
+
throw new Error(`${source}: populate entries must define a non-empty path`);
|
|
980
|
+
}
|
|
981
|
+
const select = normalizePopulateSelect(value.select, source);
|
|
982
|
+
if (!select) {
|
|
983
|
+
throw new Error(`${source}: populate entries must define a select projection`);
|
|
984
|
+
}
|
|
985
|
+
const nested = value.populate !== void 0 ? normalizePopulateOption(value.populate, source) : void 0;
|
|
986
|
+
return {
|
|
987
|
+
path,
|
|
988
|
+
select,
|
|
989
|
+
...normalizeString(value.model) ? {
|
|
990
|
+
model: normalizeString(value.model)
|
|
991
|
+
} : {},
|
|
992
|
+
...normalizeObject(value.match) ? {
|
|
993
|
+
match: normalizeObject(value.match)
|
|
994
|
+
} : {},
|
|
995
|
+
...normalizePopulateOptions(value.options) ? {
|
|
996
|
+
options: normalizePopulateOptions(value.options)
|
|
997
|
+
} : {},
|
|
998
|
+
...nested && nested.length > 0 ? {
|
|
999
|
+
populate: nested
|
|
1000
|
+
} : {}
|
|
1001
|
+
};
|
|
1002
|
+
};
|
|
1003
|
+
const normalizePopulateOption = (value, source) => {
|
|
1004
|
+
if (typeof value === "string") {
|
|
1005
|
+
throw new Error(`${source}: populate string syntax is not supported; use object entries with select`);
|
|
1006
|
+
}
|
|
1007
|
+
if (Array.isArray(value)) {
|
|
1008
|
+
if (value.length === 0) {
|
|
1009
|
+
throw new Error(`${source}: populate must contain at least one entry`);
|
|
1010
|
+
}
|
|
1011
|
+
return value.map((entry) => {
|
|
1012
|
+
if (typeof entry === "string") {
|
|
1013
|
+
throw new Error(`${source}: populate string syntax is not supported; use object entries with select`);
|
|
1014
|
+
}
|
|
1015
|
+
return normalizePopulateObject(entry, source);
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
return [normalizePopulateObject(value, source)];
|
|
1019
|
+
};
|
|
1020
|
+
const preparePopulateCacheOptions = (options, source) => {
|
|
1021
|
+
if (!options.populate) return void 0;
|
|
1022
|
+
const rootProjection = normalizeProjectionSpec(options.projection, source, "projection");
|
|
1023
|
+
if (!rootProjection) {
|
|
1024
|
+
throw new Error(`${source}: projection is required when populate is used`);
|
|
1025
|
+
}
|
|
1026
|
+
const populate = normalizePopulateOption(options.populate, source);
|
|
1027
|
+
if (!populate.length) {
|
|
1028
|
+
throw new Error(`${source}: populate must contain at least one entry`);
|
|
1029
|
+
}
|
|
1030
|
+
return {
|
|
1031
|
+
rootProjection,
|
|
1032
|
+
populate
|
|
1033
|
+
};
|
|
1034
|
+
};
|
|
496
1035
|
const computeRtsQueryKey = (query, options) => {
|
|
497
1036
|
const key = options.key ?? "";
|
|
498
1037
|
const projection = options.projection ? JSON.stringify(options.projection) : "";
|
|
@@ -630,7 +1169,8 @@ const handleQueryPayload = (payload) => {
|
|
|
630
1169
|
if (!callbacks || !callbacks.size) return;
|
|
631
1170
|
const subscription = subscriptions.get(cbKey);
|
|
632
1171
|
const pageInfo = normalizePageInfo$1(payload.pageInfo);
|
|
633
|
-
const
|
|
1172
|
+
const populateCache = subscription?.populateCache;
|
|
1173
|
+
const hasPopulate = Boolean(populateCache);
|
|
634
1174
|
const hasPagination = Boolean(subscription?.options?.pagination || pageInfo);
|
|
635
1175
|
const isLocal = !!(txnId && localTxnBuf.includes(txnId));
|
|
636
1176
|
const context = {
|
|
@@ -648,9 +1188,18 @@ const handleQueryPayload = (payload) => {
|
|
|
648
1188
|
for (const cb of callbacks) cb(null, data, context);
|
|
649
1189
|
if (!currentUid) return;
|
|
650
1190
|
const docs = Array.isArray(data) ? data.filter(isDocWithId) : [];
|
|
651
|
-
if (!docs.length) return;
|
|
652
|
-
if (hasPopulate) return;
|
|
653
1191
|
if (hasPagination) return;
|
|
1192
|
+
if (!docs.length) return;
|
|
1193
|
+
if (hasPopulate && populateCache) {
|
|
1194
|
+
void updatePopulatedDocs({
|
|
1195
|
+
modelName,
|
|
1196
|
+
data: docs,
|
|
1197
|
+
uid: currentUid,
|
|
1198
|
+
populate: populateCache.populate
|
|
1199
|
+
}).catch(() => {
|
|
1200
|
+
});
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
654
1203
|
void updateDocs(modelName, docs, currentUid).catch(() => {
|
|
655
1204
|
});
|
|
656
1205
|
};
|
|
@@ -926,7 +1475,11 @@ const registerQuery = (modelName, query, optionsOrCallback, callbackMaybe, behav
|
|
|
926
1475
|
const cbKey = `${modelName}.${queryKey}`;
|
|
927
1476
|
const runInitialNetworkQuery = behavior?.runInitialNetworkQuery !== false;
|
|
928
1477
|
const runInitialLocalQuery = behavior?.runInitialLocalQuery !== false;
|
|
929
|
-
const
|
|
1478
|
+
const populateCache = preparePopulateCacheOptions({
|
|
1479
|
+
projection: options.projection,
|
|
1480
|
+
populate: options.populate
|
|
1481
|
+
}, "registerQuery");
|
|
1482
|
+
const hasPopulate = Boolean(populateCache);
|
|
930
1483
|
const hasPagination = Boolean(options.pagination);
|
|
931
1484
|
const set = queryCallbacks.get(cbKey) ?? /* @__PURE__ */ new Set();
|
|
932
1485
|
set.add(callback);
|
|
@@ -936,25 +1489,50 @@ const registerQuery = (modelName, query, optionsOrCallback, callbackMaybe, behav
|
|
|
936
1489
|
query,
|
|
937
1490
|
options,
|
|
938
1491
|
queryKey,
|
|
939
|
-
runInitialNetworkQuery
|
|
1492
|
+
runInitialNetworkQuery,
|
|
1493
|
+
...populateCache ? {
|
|
1494
|
+
populateCache
|
|
1495
|
+
} : {}
|
|
940
1496
|
});
|
|
941
|
-
if (currentUid && runInitialLocalQuery && !
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
1497
|
+
if (currentUid && runInitialLocalQuery && !hasPagination) {
|
|
1498
|
+
if (hasPopulate && populateCache) {
|
|
1499
|
+
void runPopulatedQuery({
|
|
1500
|
+
modelName,
|
|
1501
|
+
query,
|
|
1502
|
+
options: {
|
|
1503
|
+
uid: currentUid,
|
|
1504
|
+
projection: populateCache.rootProjection,
|
|
1505
|
+
sort: options.sort,
|
|
1506
|
+
limit: options.limit,
|
|
1507
|
+
populate: populateCache.populate
|
|
1508
|
+
}
|
|
1509
|
+
}).then(({
|
|
1510
|
+
hit,
|
|
1511
|
+
data,
|
|
1512
|
+
context
|
|
1513
|
+
}) => {
|
|
1514
|
+
if (!hit) return;
|
|
1515
|
+
callback?.(null, data, context);
|
|
1516
|
+
}).catch(() => {
|
|
1517
|
+
});
|
|
1518
|
+
} else {
|
|
1519
|
+
void runQuery({
|
|
1520
|
+
modelName,
|
|
1521
|
+
query,
|
|
1522
|
+
options: {
|
|
1523
|
+
uid: currentUid,
|
|
1524
|
+
projection: options.projection,
|
|
1525
|
+
sort: options.sort,
|
|
1526
|
+
limit: options.limit
|
|
1527
|
+
}
|
|
1528
|
+
}).then(({
|
|
1529
|
+
data,
|
|
1530
|
+
context
|
|
1531
|
+
}) => {
|
|
1532
|
+
callback?.(null, data, context);
|
|
1533
|
+
}).catch(() => {
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
958
1536
|
}
|
|
959
1537
|
sendToServer({
|
|
960
1538
|
type: "register-query",
|
|
@@ -988,6 +1566,10 @@ const runNetworkQuery = async ({
|
|
|
988
1566
|
if (typeof modelName !== "string" || modelName.trim().length === 0) {
|
|
989
1567
|
throw new Error("runNetworkQuery: modelName must be a non-empty string");
|
|
990
1568
|
}
|
|
1569
|
+
preparePopulateCacheOptions({
|
|
1570
|
+
projection: options.projection,
|
|
1571
|
+
populate: options.populate
|
|
1572
|
+
}, "runNetworkQuery");
|
|
991
1573
|
const hasTimeout = typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0;
|
|
992
1574
|
const timeoutStartedAt = hasTimeout ? Date.now() : 0;
|
|
993
1575
|
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
|
@@ -1134,10 +1716,20 @@ const flattenLoadedPages = (previousPages, headPage, nextPages) => {
|
|
|
1134
1716
|
for (const page of nextPages) merged.push(...page.nodes);
|
|
1135
1717
|
return dedupeById(merged);
|
|
1136
1718
|
};
|
|
1719
|
+
const assertIncludeOnlyProjection = (projection) => {
|
|
1720
|
+
if (!projection) return;
|
|
1721
|
+
for (const [path, value] of Object.entries(projection)) {
|
|
1722
|
+
if (!path.trim()) continue;
|
|
1723
|
+
if (value !== 1) {
|
|
1724
|
+
throw new Error("useQuery: projection must be include-only (value 1); exclusion projection is not supported");
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
};
|
|
1137
1728
|
const useQuery = (modelName, query = {}, options = {}) => {
|
|
1138
1729
|
if (typeof modelName !== "string" || modelName.trim().length === 0) {
|
|
1139
1730
|
throw new Error("useQuery: modelName must be a non-empty string");
|
|
1140
1731
|
}
|
|
1732
|
+
assertIncludeOnlyProjection(options.projection);
|
|
1141
1733
|
const id = useId();
|
|
1142
1734
|
const enabled = options.enabled ?? true;
|
|
1143
1735
|
const ssrEnabled = options.ssr !== false;
|
|
@@ -1149,7 +1741,10 @@ const useQuery = (modelName, query = {}, options = {}) => {
|
|
|
1149
1741
|
const limitStr = typeof options.limit === "number" ? String(options.limit) : "";
|
|
1150
1742
|
const populateJson = options.populate ? JSON.stringify(options.populate) : "";
|
|
1151
1743
|
const paginationJson = options.pagination ? JSON.stringify(options.pagination) : "";
|
|
1152
|
-
|
|
1744
|
+
preparePopulateCacheOptions({
|
|
1745
|
+
projection: options.projection,
|
|
1746
|
+
populate: options.populate
|
|
1747
|
+
}, "useQuery");
|
|
1153
1748
|
const isPaginated = Boolean(options.pagination);
|
|
1154
1749
|
const queryKey = computeRtsQueryKey(query, {
|
|
1155
1750
|
key,
|
|
@@ -1270,7 +1865,7 @@ const useQuery = (modelName, query = {}, options = {}) => {
|
|
|
1270
1865
|
useEffect(() => {
|
|
1271
1866
|
if (!enabled) return;
|
|
1272
1867
|
const runInitialNetworkQuery = refreshOnMount || !hasSeedData;
|
|
1273
|
-
const runInitialLocalQuery = !hasSeedData && !
|
|
1868
|
+
const runInitialLocalQuery = !hasSeedData && !isPaginated;
|
|
1274
1869
|
const unsubscribe = registerQuery(modelName, query, {
|
|
1275
1870
|
key,
|
|
1276
1871
|
projection: options.projection,
|
|
@@ -1488,4 +2083,4 @@ export {
|
|
|
1488
2083
|
updateDocs as u,
|
|
1489
2084
|
useQuery as v
|
|
1490
2085
|
};
|
|
1491
|
-
//# sourceMappingURL=useQuery-
|
|
2086
|
+
//# sourceMappingURL=useQuery-CUjwBdkK.js.map
|