pbtsdb 0.8.0 → 0.9.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.
@@ -1,57 +1,87 @@
1
- import { BTreeIndex, createCollection as createCollection$1, parseWhereExpression, parseOrderByExpression, deepEquals } from '@tanstack/db';
2
- export { BTreeIndex, BasicIndex, ReverseIndex, createEffect, toArray } from '@tanstack/db';
1
+ import { BTreeIndex, createCollection as createCollection$1, parseWhereExpression, parseOrderByExpression } from '@tanstack/db';
2
+ export { BTreeIndex, BasicIndex, ReverseIndex, createEffect, materialize, toArray } from '@tanstack/db';
3
3
  import { queryCollectionOptions, DeleteOperationItemNotFoundError } from '@tanstack/query-db-collection';
4
4
 
5
5
  // src/core.ts
6
6
 
7
- // src/expand-merge.ts
8
- function sameRelationValue(a, b) {
9
- if (Array.isArray(a) && Array.isArray(b)) {
10
- return a.length === b.length && a.every((value, index) => value === b[index]);
7
+ // src/expand-paths.ts
8
+ function normalizePaths(paths) {
9
+ const set = /* @__PURE__ */ new Set();
10
+ for (const path of paths) {
11
+ const trimmed = path.trim();
12
+ if (trimmed) set.add(trimmed);
11
13
  }
12
- return a === b;
14
+ return [...set].sort();
13
15
  }
14
- function storedIsNewer(incoming, stored) {
15
- const a = incoming;
16
- const b = stored;
17
- if (!a || !b || a.id !== b.id) return false;
18
- if (typeof a.updated !== "string" || typeof b.updated !== "string") return false;
19
- return b.updated > a.updated;
16
+ function splitPaths(csv) {
17
+ return csv ? normalizePaths(csv.split(",")) : [];
20
18
  }
21
- function newerEntry(incoming, stored) {
22
- if (Array.isArray(incoming) && Array.isArray(stored)) {
23
- let changed = false;
24
- const merged = incoming.map((entry) => {
25
- const match = stored.find(
26
- (candidate) => candidate?.id === entry?.id
19
+ function joinPaths(paths) {
20
+ const normalized = normalizePaths(paths);
21
+ return normalized.length > 0 ? normalized.join(",") : void 0;
22
+ }
23
+ function validateExpandPath(collectionName, targets, path) {
24
+ if (!targets) {
25
+ throw new Error(
26
+ `Cannot expand "${path}" on collection "${collectionName}": no relations declared`
27
+ );
28
+ }
29
+ let current = targets;
30
+ for (const segment of path.split(".")) {
31
+ const next = current?.[segment];
32
+ if (!next) {
33
+ throw new Error(
34
+ `Cannot expand "${path}" on collection "${collectionName}": segment "${segment}" is not a declared relation`
27
35
  );
28
- if (!storedIsNewer(entry, match)) return entry;
29
- changed = true;
30
- return match;
31
- });
32
- return changed ? merged : incoming;
36
+ }
37
+ current = next.relationTargets;
33
38
  }
34
- return storedIsNewer(incoming, stored) ? stored : incoming;
35
39
  }
36
- function mergeExpand(incoming, existing) {
37
- const stored = existing?.expand;
38
- if (!stored) return incoming;
39
- const row = incoming;
40
- const merged = { ...row.expand ?? {} };
41
- let carried = false;
42
- for (const [relation, value] of Object.entries(stored)) {
43
- if (relation in merged) {
44
- const kept = newerEntry(merged[relation], value);
45
- if (kept === merged[relation]) continue;
46
- merged[relation] = kept;
47
- carried = true;
48
- continue;
49
- }
50
- if (!sameRelationValue(row[relation], existing[relation])) continue;
51
- merged[relation] = value;
52
- carried = true;
40
+
41
+ // src/keyed-where.ts
42
+ function isIdRef(node) {
43
+ return node?.type === "ref" && node.path?.length === 1 && node.path[0] === "id";
44
+ }
45
+ function stringValue(node) {
46
+ return node?.type === "val" && typeof node.value === "string" ? node.value : void 0;
47
+ }
48
+ function collectEq(a, b) {
49
+ const other = isIdRef(a) ? b : isIdRef(b) ? a : void 0;
50
+ if (!other) return void 0;
51
+ const value = stringValue(other);
52
+ return value === void 0 ? void 0 : [value];
53
+ }
54
+ function collectIn(a, b) {
55
+ if (!isIdRef(a) || b?.type !== "val" || !Array.isArray(b.value)) return void 0;
56
+ return b.value.every((v) => typeof v === "string") ? b.value : void 0;
57
+ }
58
+ function collectOr(args) {
59
+ const ids = [];
60
+ for (const arg of args) {
61
+ const sub = collect(arg);
62
+ if (!sub) return void 0;
63
+ ids.push(...sub);
64
+ }
65
+ return ids;
66
+ }
67
+ function collect(node) {
68
+ if (node.type !== "func" || !node.args) return void 0;
69
+ const [a, b] = node.args;
70
+ switch (node.name) {
71
+ case "eq":
72
+ return collectEq(a, b);
73
+ case "in":
74
+ return collectIn(a, b);
75
+ case "or":
76
+ return collectOr(node.args);
77
+ default:
78
+ return void 0;
53
79
  }
54
- return carried ? { ...row, expand: merged } : incoming;
80
+ }
81
+ function idsFromWhere(where) {
82
+ if (!where) return void 0;
83
+ const ids = collect(where);
84
+ return ids ? [...new Set(ids)].sort() : void 0;
55
85
  }
56
86
 
57
87
  // src/logger.ts
@@ -84,112 +114,6 @@ function setLogger(customLogger) {
84
114
  function resetLogger() {
85
115
  currentLogger = defaultLogger;
86
116
  }
87
-
88
- // src/expand-patch.ts
89
- function embeddedId(value) {
90
- return value?.id;
91
- }
92
- function patchUpdate(row, field, record) {
93
- const embedded = row.expand?.[field];
94
- if (embedded === void 0) return void 0;
95
- let next;
96
- if (Array.isArray(embedded)) {
97
- const index = embedded.findIndex((entry) => embeddedId(entry) === record.id);
98
- if (index === -1) return void 0;
99
- const replaced = mergeExpand(record, embedded[index]);
100
- next = embedded.map((entry, i) => i === index ? replaced : entry);
101
- } else {
102
- if (embeddedId(embedded) !== record.id) return void 0;
103
- next = mergeExpand(record, embedded);
104
- }
105
- if (deepEquals(next, embedded)) return void 0;
106
- return { ...row, expand: { ...row.expand, [field]: next } };
107
- }
108
- function patchDelete(row, field, id) {
109
- const value = row[field];
110
- const embedded = row.expand?.[field];
111
- if (Array.isArray(value)) {
112
- if (!value.includes(id)) return void 0;
113
- const patched2 = { ...row, [field]: value.filter((item) => item !== id) };
114
- if (Array.isArray(embedded)) {
115
- patched2.expand = {
116
- ...row.expand,
117
- [field]: embedded.filter((entry) => embeddedId(entry) !== id)
118
- };
119
- }
120
- return patched2;
121
- }
122
- if (value !== id) return void 0;
123
- const patched = { ...row, [field]: "" };
124
- if (embedded !== void 0) {
125
- const expand = { ...row.expand };
126
- delete expand[field];
127
- patched.expand = expand;
128
- }
129
- return patched;
130
- }
131
- function patchEmbedded(row, field, action, record) {
132
- const current = row;
133
- const patched = action === "delete" ? patchDelete(current, field, record.id) : patchUpdate(current, field, record);
134
- return patched;
135
- }
136
- function fieldsByParent(dependents) {
137
- const grouped = /* @__PURE__ */ new Map();
138
- for (const { field, parent } of dependents) {
139
- const fields = grouped.get(parent);
140
- if (fields) fields.push(field);
141
- else grouped.set(parent, [field]);
142
- }
143
- return grouped;
144
- }
145
- function propagateRelatedChange(source, action, record, visited) {
146
- for (const [parent, fields] of fieldsByParent(source.relationDependents ?? [])) {
147
- try {
148
- parent.applyRelatedChange?.(fields, action, record, visited);
149
- } catch (error) {
150
- logger.error("Failed to patch a relation dependent", {
151
- collectionName: source.collectionName,
152
- dependent: parent.collectionName,
153
- fields,
154
- error
155
- });
156
- }
157
- }
158
- }
159
-
160
- // src/expand-paths.ts
161
- function normalizePaths(paths) {
162
- const set = /* @__PURE__ */ new Set();
163
- for (const path of paths) {
164
- const trimmed = path.trim();
165
- if (trimmed) set.add(trimmed);
166
- }
167
- return [...set].sort();
168
- }
169
- function splitPaths(csv) {
170
- return csv ? normalizePaths(csv.split(",")) : [];
171
- }
172
- function joinPaths(paths) {
173
- const normalized = normalizePaths(paths);
174
- return normalized.length > 0 ? normalized.join(",") : void 0;
175
- }
176
- function validateExpandPath(collectionName, targets, path) {
177
- if (!targets) {
178
- throw new Error(
179
- `Cannot expand "${path}" on collection "${collectionName}": no relations declared`
180
- );
181
- }
182
- let current = targets;
183
- for (const segment of path.split(".")) {
184
- const next = current?.[segment];
185
- if (!next) {
186
- throw new Error(
187
- `Cannot expand "${path}" on collection "${collectionName}": segment "${segment}" is not a declared relation`
188
- );
189
- }
190
- current = next.relationTargets;
191
- }
192
- }
193
117
  function escapeValue(value) {
194
118
  if (value === null) {
195
119
  return "null";
@@ -292,19 +216,21 @@ var viewPaths = /* @__PURE__ */ new WeakMap();
292
216
  function buildCollection(input) {
293
217
  const { pb, queryClient, factoryOptions, collectionName, options } = input;
294
218
  const relationTargets = options?.relations;
295
- const alwaysExpand = normalizePaths(options?.alwaysExpand ?? []);
296
- for (const path of alwaysExpand) validateExpandPath(collectionName, relationTargets, path);
219
+ const alwaysFetch = normalizePaths(options?.alwaysFetchRelations ?? []);
220
+ for (const path of alwaysFetch) validateExpandPath(collectionName, relationTargets, path);
297
221
  const syncMode = options?.syncMode ?? "eager";
298
222
  const requestedExpand = /* @__PURE__ */ new Set();
299
223
  function toRequest(opts) {
300
224
  const request = {};
301
- const filter = convertToPocketBaseFilter(opts?.where);
225
+ const ids = idsFromWhere(opts?.where);
226
+ const filter = ids ? void 0 : convertToPocketBaseFilter(opts?.where);
302
227
  const sort = convertToPocketBaseSort(opts?.orderBy);
303
- const expand2 = joinPaths(opts?.expand ?? []);
228
+ const expand = joinPaths(opts?.expand ?? []);
229
+ if (ids) request.ids = ids;
304
230
  if (filter) request.filter = filter;
305
231
  if (sort) request.sort = sort;
306
232
  if (opts?.limit) request.limit = opts.limit;
307
- if (expand2) request.expand = expand2;
233
+ if (expand) request.expand = expand;
308
234
  return request;
309
235
  }
310
236
  function queryKeyFor(opts) {
@@ -313,16 +239,34 @@ function buildCollection(input) {
313
239
  }
314
240
  function activeExpand(request) {
315
241
  return joinPaths([
316
- ...alwaysExpand,
242
+ ...alwaysFetch,
317
243
  ...splitPaths(request.expand),
318
244
  ...syncMode === "eager" ? requestedExpand : []
319
245
  ]);
320
246
  }
247
+ function stripFetchedRelations(items, expandString) {
248
+ const heads = new Set(splitPaths(expandString).map((path) => path.split(".")[0]));
249
+ if (heads.size === 0) return items;
250
+ return items.map((item) => {
251
+ const { expand, ...plain } = item;
252
+ if (!expand) return item;
253
+ const kept = Object.fromEntries(
254
+ Object.entries(expand).filter(([key]) => !heads.has(key))
255
+ );
256
+ return Object.keys(kept).length > 0 ? { ...plain, expand: kept } : plain;
257
+ });
258
+ }
321
259
  const ignoreAutoCancellation = options?.ignoreAutoCancellation ?? true;
322
260
  const refetchOnMutation = options?.refetchOnMutation ?? false;
323
261
  function eagerSyncInFlight(target) {
324
262
  return target.status !== void 0 && target.status !== "idle" && target.status !== "cleaned-up";
325
263
  }
264
+ function withoutExpand(values) {
265
+ return values.map((value) => {
266
+ const { expand: _expand, ...plain } = value;
267
+ return plain;
268
+ });
269
+ }
326
270
  async function upsertInto(key, target, values) {
327
271
  if (!target.utils) return;
328
272
  if (!target.isReady()) {
@@ -337,7 +281,7 @@ function buildCollection(input) {
337
281
  return;
338
282
  }
339
283
  }
340
- target.utils.writeUpsert(values);
284
+ target.utils.writeUpsert(withoutExpand(values));
341
285
  }
342
286
  async function upsertExpandedField(key, value, targets) {
343
287
  const target = targets[key];
@@ -359,23 +303,48 @@ function buildCollection(input) {
359
303
  }
360
304
  }
361
305
  }
306
+ function idFilter(ids) {
307
+ return ids.map((id) => `id = "${id.replace(/"/g, '\\"')}"`).join(" || ");
308
+ }
309
+ function rowsFromStore(ids) {
310
+ const rows = [];
311
+ for (const id of ids) {
312
+ const row = collection._state.syncedData.get(id);
313
+ if (!row) return void 0;
314
+ rows.push(row);
315
+ }
316
+ return rows;
317
+ }
318
+ function servedFromStore(request) {
319
+ const { ids, sort, limit } = request;
320
+ if (!ids) return void 0;
321
+ if (ids.length === 0) return [];
322
+ if (request.expand) return void 0;
323
+ if (sort && limit) return void 0;
324
+ const present = rowsFromStore(ids);
325
+ if (!present) return void 0;
326
+ return limit ? present.slice(0, limit) : present;
327
+ }
362
328
  async function fetchItems(request) {
363
- const { filter, sort, limit } = request;
364
- const expand2 = activeExpand(request);
329
+ const served = servedFromStore(request);
330
+ if (served) return served;
331
+ const { sort, limit, ids } = request;
332
+ const filter = ids ? idFilter(ids) : request.filter;
333
+ const expand = activeExpand(request);
365
334
  if (limit) {
366
335
  const result = await pb.collection(collectionName).getList(1, limit, {
367
336
  filter,
368
337
  sort,
369
338
  skipTotal: true,
370
339
  // Optimize by skipping total count
371
- expand: expand2
340
+ expand
372
341
  });
373
342
  return result.items;
374
343
  }
375
344
  return await pb.collection(collectionName).getFullList({
376
345
  filter,
377
346
  sort,
378
- expand: expand2
347
+ expand
379
348
  });
380
349
  }
381
350
  const inFlightConfirmedIds = /* @__PURE__ */ new Set();
@@ -422,52 +391,14 @@ function buildCollection(input) {
422
391
  throw error;
423
392
  }
424
393
  await upsertExpanded(items, relationTargets);
425
- if (request.expand) writeExpandedRows(items);
426
- return withRowsConfirmedMidFlight(items, confirmedMidFlight);
394
+ return withRowsConfirmedMidFlight(
395
+ stripFetchedRelations(items, activeExpand(request)),
396
+ confirmedMidFlight
397
+ );
427
398
  } finally {
428
399
  inFlightConfirmedIds.delete(confirmedMidFlight);
429
400
  }
430
401
  }
431
- function writeExpandedRows(items) {
432
- if (!collection.utils || !collection.isReady()) return;
433
- collection.utils.writeUpsert(items);
434
- }
435
- function patchRowFields(row, fields, action, record) {
436
- let current;
437
- for (const field of fields) {
438
- const next = patchEmbedded(current ?? row, field, action, record);
439
- if (next) current = next;
440
- }
441
- return current;
442
- }
443
- function patchedRows(fields, action, record, visited) {
444
- const patched = [];
445
- for (const row of collection._state.syncedData.values()) {
446
- const id = row.id;
447
- if (typeof id !== "string") continue;
448
- const key = `${collectionName}:${id}`;
449
- if (visited.has(key)) continue;
450
- const next = patchRowFields(row, fields, action, record);
451
- if (!next) continue;
452
- visited.add(key);
453
- patched.push(next);
454
- }
455
- return patched;
456
- }
457
- function applyRelatedChange(fields, action, record, visited) {
458
- if (!collection.utils || !collection.isReady()) return;
459
- const patched = patchedRows(fields, action, record, visited);
460
- if (patched.length === 0) return;
461
- writeOwn(() => collection.utils.writeUpsert(patched));
462
- for (const row of patched) {
463
- propagateRelatedChange(
464
- collection,
465
- "update",
466
- row,
467
- visited
468
- );
469
- }
470
- }
471
402
  const queryCollectionConfig = queryCollectionOptions({
472
403
  autoIndex: "eager",
473
404
  defaultIndexType: BTreeIndex,
@@ -575,11 +506,6 @@ function buildCollection(input) {
575
506
  const paths = (opts.subscription && viewPaths.get(opts.subscription)) ?? subscribingViewPaths;
576
507
  return paths ? { ...opts, expand: paths } : opts;
577
508
  }
578
- function mergedWriteValue(op) {
579
- const key = syncedWriteKey(op);
580
- const existing = key === null ? void 0 : collection._state.syncedData.get(key);
581
- return mergeExpand(op.value, existing);
582
- }
583
509
  const innerSync = collectionOptions.sync.sync;
584
510
  collectionOptions.sync = {
585
511
  ...collectionOptions.sync,
@@ -587,10 +513,6 @@ function buildCollection(input) {
587
513
  const guardedWrite = (message) => {
588
514
  const op = message;
589
515
  if (shouldDropSyncedWrite(op)) return;
590
- if ((op.type === "insert" || op.type === "update") && op.value && typeof op.value === "object") {
591
- const merged = mergedWriteValue(op);
592
- if (merged !== op.value) return params.write({ ...message, value: merged });
593
- }
594
516
  return params.write(message);
595
517
  };
596
518
  const res = innerSync({ ...params, write: guardedWrite });
@@ -604,14 +526,6 @@ function buildCollection(input) {
604
526
  }
605
527
  };
606
528
  const collection = createCollection$1(collectionOptions);
607
- const relationDependents = [];
608
- for (const [field, target] of Object.entries(relationTargets ?? {})) {
609
- target.relationDependents ?? (target.relationDependents = []);
610
- target.relationDependents.push({
611
- field,
612
- parent: collection
613
- });
614
- }
615
529
  const views = /* @__PURE__ */ new Map();
616
530
  function createView(paths) {
617
531
  const view = Object.create(collection);
@@ -630,18 +544,18 @@ function buildCollection(input) {
630
544
  }
631
545
  }
632
546
  },
633
- expand: {
547
+ fetchRelations: {
634
548
  value: () => {
635
- throw new Error(`A view of "${collectionName}" cannot be expanded further`);
549
+ throw new Error(`A view of "${collectionName}" cannot fetch further relations`);
636
550
  }
637
551
  }
638
552
  });
639
553
  return view;
640
554
  }
641
- function expand(...paths) {
555
+ function fetchRelations(...paths) {
642
556
  for (const path of paths) validateExpandPath(collectionName, relationTargets, path);
643
- const all = normalizePaths([...alwaysExpand, ...paths]);
644
- if (all.every((path) => alwaysExpand.includes(path))) return collection;
557
+ const all = normalizePaths([...alwaysFetch, ...paths]);
558
+ if (all.every((path) => alwaysFetch.includes(path))) return collection;
645
559
  const key = all.join(",");
646
560
  let view = views.get(key);
647
561
  if (!view) {
@@ -698,15 +612,16 @@ function buildCollection(input) {
698
612
  if (!collection.utils) return;
699
613
  markEchoPresence(event);
700
614
  if (isStaleEcho(event)) return;
615
+ const [stored] = stripFetchedRelations([event.record], pendingSubscribeExpand());
701
616
  try {
702
617
  writeOwn(
703
618
  () => collection.utils.writeBatch(() => {
704
619
  switch (event.action) {
705
620
  case "create":
706
- collection.utils.writeInsert(event.record);
621
+ collection.utils.writeInsert(stored);
707
622
  break;
708
623
  case "update":
709
- collection.utils.writeUpsert(event.record);
624
+ collection.utils.writeUpsert(stored);
710
625
  break;
711
626
  case "delete":
712
627
  if (event.record && "id" in event.record) {
@@ -734,15 +649,9 @@ function buildCollection(input) {
734
649
  })
735
650
  );
736
651
  }
737
- propagateRelatedChange(
738
- collection,
739
- event.action,
740
- event.record,
741
- /* @__PURE__ */ new Set()
742
- );
743
652
  };
744
653
  function pendingSubscribeExpand() {
745
- return joinPaths([...alwaysExpand, ...requestedExpand]);
654
+ return joinPaths([...alwaysFetch, ...requestedExpand]);
746
655
  }
747
656
  function activeExpandTargets() {
748
657
  const targets = /* @__PURE__ */ new Set();
@@ -789,11 +698,11 @@ function buildCollection(input) {
789
698
  }
790
699
  function realtimeSubscribeOptions() {
791
700
  const base = factoryOptions?.subscribeOptions?.();
792
- const expand2 = joinPaths([
701
+ const expand = joinPaths([
793
702
  ...splitPaths(pendingSubscribeExpand()),
794
703
  ...splitPaths(base?.expand)
795
704
  ]);
796
- return expand2 ? { ...base, expand: expand2 } : base;
705
+ return expand ? { ...base, expand } : base;
797
706
  }
798
707
  const doStartSubscription = async () => {
799
708
  if (isSubscribed) return;
@@ -915,9 +824,7 @@ function buildCollection(input) {
915
824
  waitForSubscription,
916
825
  isSubscribed: () => isSubscribed,
917
826
  heldRelationTargetCount: () => heldTargetSubscriptions.size,
918
- expand,
919
- relationDependents,
920
- applyRelatedChange
827
+ fetchRelations
921
828
  });
922
829
  return collection;
923
830
  }
@@ -946,5 +853,5 @@ function newRecordId() {
946
853
  }
947
854
 
948
855
  export { createCollection, newRecordId, resetLogger, setLogger };
949
- //# sourceMappingURL=chunk-KMR6I4ZC.js.map
950
- //# sourceMappingURL=chunk-KMR6I4ZC.js.map
856
+ //# sourceMappingURL=chunk-YWTU6QXM.js.map
857
+ //# sourceMappingURL=chunk-YWTU6QXM.js.map