@rpcbase/client 0.389.0 → 0.391.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.
@@ -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 runQuery = async ({
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
- let mappedDocs = docs.map(({
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
- mappedDocs = mappedDocs.filter((doc) => satisfiesProjection(doc, options.projection));
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
- mappedDocs = mappedDocs.sort((a, b) => {
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: mappedDocs,
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 hasPopulate = Boolean(subscription?.options?.populate);
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 hasPopulate = Boolean(options.populate);
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 && !hasPopulate && !hasPagination) {
942
- void runQuery({
943
- modelName,
944
- query,
945
- options: {
946
- uid: currentUid,
947
- projection: options.projection,
948
- sort: options.sort,
949
- limit: options.limit
950
- }
951
- }).then(({
952
- data,
953
- context
954
- }) => {
955
- callback?.(null, data, context);
956
- }).catch(() => {
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
- const hasPopulate = Boolean(options.populate);
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 && !hasPopulate && !isPaginated;
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-ClKeTB8S.js.map
2086
+ //# sourceMappingURL=useQuery-CUjwBdkK.js.map