pbtsdb 0.8.0 → 0.9.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.
@@ -1,57 +1,109 @@
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
+ var BACK_RELATION_EXPAND_CAP = 1e3;
24
+ function parseViaKey(key) {
25
+ const index = key.indexOf("_via_");
26
+ if (index < 0) return void 0;
27
+ const field = key.slice(index + "_via_".length);
28
+ return field ? { field } : void 0;
29
+ }
30
+ function markFiledSubset(target, key, values, parentId) {
31
+ const via = parseViaKey(key);
32
+ if (!via || values.length >= BACK_RELATION_EXPAND_CAP) return;
33
+ target.markSubsetLoaded?.(via.field, parentId);
34
+ }
35
+ function validateExpandPath(collectionName, targets, path) {
36
+ if (!targets) {
37
+ throw new Error(
38
+ `Cannot expand "${path}" on collection "${collectionName}": no relations declared`
39
+ );
40
+ }
41
+ let current = targets;
42
+ for (const segment of path.split(".")) {
43
+ const next = current?.[segment];
44
+ if (!next) {
45
+ throw new Error(
46
+ `Cannot expand "${path}" on collection "${collectionName}": segment "${segment}" is not a declared relation`
27
47
  );
28
- if (!storedIsNewer(entry, match)) return entry;
29
- changed = true;
30
- return match;
31
- });
32
- return changed ? merged : incoming;
48
+ }
49
+ current = next.relationTargets;
50
+ }
51
+ }
52
+
53
+ // src/keyed-where.ts
54
+ function fieldRef(node) {
55
+ return node?.type === "ref" && node.path?.length === 1 ? node.path[0] : void 0;
56
+ }
57
+ function stringValue(node) {
58
+ return node?.type === "val" && typeof node.value === "string" ? node.value : void 0;
59
+ }
60
+ function collectEq(a, b) {
61
+ const field = fieldRef(a) ?? fieldRef(b);
62
+ if (field === void 0) return void 0;
63
+ const value = stringValue(fieldRef(a) !== void 0 ? b : a);
64
+ return value === void 0 ? void 0 : { field, values: [value] };
65
+ }
66
+ function collectIn(a, b) {
67
+ const field = fieldRef(a);
68
+ if (field === void 0 || b?.type !== "val" || !Array.isArray(b.value)) return void 0;
69
+ return b.value.every((v) => typeof v === "string") ? { field, values: b.value } : void 0;
70
+ }
71
+ function collectOr(args) {
72
+ let field;
73
+ const values = [];
74
+ for (const arg of args) {
75
+ const sub = collect(arg);
76
+ if (!sub || field !== void 0 && sub.field !== field) return void 0;
77
+ field = sub.field;
78
+ values.push(...sub.values);
79
+ }
80
+ return field === void 0 ? void 0 : { field, values };
81
+ }
82
+ function collect(node) {
83
+ if (node.type !== "func" || !node.args) return void 0;
84
+ const [a, b] = node.args;
85
+ switch (node.name) {
86
+ case "eq":
87
+ return collectEq(a, b);
88
+ case "in":
89
+ return collectIn(a, b);
90
+ case "or":
91
+ return collectOr(node.args);
92
+ default:
93
+ return void 0;
33
94
  }
34
- return storedIsNewer(incoming, stored) ? stored : incoming;
35
95
  }
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;
53
- }
54
- return carried ? { ...row, expand: merged } : incoming;
96
+ function subsetFromWhere(where) {
97
+ if (!where) return void 0;
98
+ const subset = collect(where);
99
+ return subset ? { field: subset.field, values: [...new Set(subset.values)].sort() } : void 0;
100
+ }
101
+ function matchesSubset(row, field, wanted) {
102
+ const value = row[field];
103
+ if (typeof value === "string") return wanted.has(value);
104
+ if (Array.isArray(value))
105
+ return value.some((item) => typeof item === "string" && wanted.has(item));
106
+ return false;
55
107
  }
56
108
 
57
109
  // src/logger.ts
@@ -84,112 +136,6 @@ function setLogger(customLogger) {
84
136
  function resetLogger() {
85
137
  currentLogger = defaultLogger;
86
138
  }
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
139
  function escapeValue(value) {
194
140
  if (value === null) {
195
141
  return "null";
@@ -289,22 +235,55 @@ function convertToPocketBaseSort(orderBy) {
289
235
 
290
236
  // src/build-collection.ts
291
237
  var viewPaths = /* @__PURE__ */ new WeakMap();
238
+ var pendingFilings = /* @__PURE__ */ new WeakMap();
239
+ function registerPendingFiling(target, field, settles) {
240
+ let byField = pendingFilings.get(target);
241
+ if (!byField) {
242
+ byField = /* @__PURE__ */ new Map();
243
+ pendingFilings.set(target, byField);
244
+ }
245
+ let pending = byField.get(field);
246
+ if (!pending) {
247
+ pending = [];
248
+ byField.set(field, pending);
249
+ }
250
+ pending.push(settles);
251
+ return () => {
252
+ const index = pending.indexOf(settles);
253
+ if (index !== -1) pending.splice(index, 1);
254
+ if (pending.length === 0) byField.delete(field);
255
+ if (byField.size === 0) pendingFilings.delete(target);
256
+ };
257
+ }
258
+ async function awaitPendingFiling(target, field) {
259
+ const pending = pendingFilings.get(target)?.get(field);
260
+ if (!pending || pending.length === 0) return;
261
+ await Promise.all(pending);
262
+ }
263
+ function registerMarkInvalidationEvents(collection, clearMarks) {
264
+ collection.onStatusChange((event) => {
265
+ if (event.status === "cleaned-up") clearMarks();
266
+ });
267
+ collection.onTruncate(() => clearMarks());
268
+ }
292
269
  function buildCollection(input) {
293
270
  const { pb, queryClient, factoryOptions, collectionName, options } = input;
294
271
  const relationTargets = options?.relations;
295
- const alwaysExpand = normalizePaths(options?.alwaysExpand ?? []);
296
- for (const path of alwaysExpand) validateExpandPath(collectionName, relationTargets, path);
272
+ const alwaysFetch = normalizePaths(options?.alwaysFetchRelations ?? []);
273
+ for (const path of alwaysFetch) validateExpandPath(collectionName, relationTargets, path);
297
274
  const syncMode = options?.syncMode ?? "eager";
298
275
  const requestedExpand = /* @__PURE__ */ new Set();
299
276
  function toRequest(opts) {
300
277
  const request = {};
301
- const filter = convertToPocketBaseFilter(opts?.where);
278
+ const subset = subsetFromWhere(opts?.where);
279
+ const filter = subset ? void 0 : convertToPocketBaseFilter(opts?.where);
302
280
  const sort = convertToPocketBaseSort(opts?.orderBy);
303
- const expand2 = joinPaths(opts?.expand ?? []);
281
+ const expand = joinPaths(opts?.expand ?? []);
282
+ if (subset) request.subset = subset;
304
283
  if (filter) request.filter = filter;
305
284
  if (sort) request.sort = sort;
306
285
  if (opts?.limit) request.limit = opts.limit;
307
- if (expand2) request.expand = expand2;
286
+ if (expand) request.expand = expand;
308
287
  return request;
309
288
  }
310
289
  function queryKeyFor(opts) {
@@ -313,18 +292,46 @@ function buildCollection(input) {
313
292
  }
314
293
  function activeExpand(request) {
315
294
  return joinPaths([
316
- ...alwaysExpand,
295
+ ...alwaysFetch,
317
296
  ...splitPaths(request.expand),
318
297
  ...syncMode === "eager" ? requestedExpand : []
319
298
  ]);
320
299
  }
300
+ function backRelationTargetsFor(request) {
301
+ if (!relationTargets) return [];
302
+ const results = [];
303
+ for (const key of splitPaths(activeExpand(request)).map((path) => path.split(".")[0])) {
304
+ const target = relationTargets[key];
305
+ const via = target && parseViaKey(key);
306
+ if (target && via) results.push({ target, field: via.field });
307
+ }
308
+ return results;
309
+ }
310
+ function stripFetchedRelations(items, expandString) {
311
+ const heads = new Set(splitPaths(expandString).map((path) => path.split(".")[0]));
312
+ if (heads.size === 0) return items;
313
+ return items.map((item) => {
314
+ const { expand, ...plain } = item;
315
+ if (!expand) return item;
316
+ const kept = Object.fromEntries(
317
+ Object.entries(expand).filter(([key]) => !heads.has(key))
318
+ );
319
+ return Object.keys(kept).length > 0 ? { ...plain, expand: kept } : plain;
320
+ });
321
+ }
321
322
  const ignoreAutoCancellation = options?.ignoreAutoCancellation ?? true;
322
323
  const refetchOnMutation = options?.refetchOnMutation ?? false;
323
324
  function eagerSyncInFlight(target) {
324
325
  return target.status !== void 0 && target.status !== "idle" && target.status !== "cleaned-up";
325
326
  }
327
+ function withoutExpand(values) {
328
+ return values.map((value) => {
329
+ const { expand: _expand, ...plain } = value;
330
+ return plain;
331
+ });
332
+ }
326
333
  async function upsertInto(key, target, values) {
327
- if (!target.utils) return;
334
+ if (!target.utils) return false;
328
335
  if (!target.isReady()) {
329
336
  if (target.config?.syncMode === "on-demand") {
330
337
  await target._sync.startSync();
@@ -334,48 +341,148 @@ function buildCollection(input) {
334
341
  logger.warn(
335
342
  `not syncing ${key} on ${collectionName} because store is not yet ready`
336
343
  );
337
- return;
344
+ return false;
338
345
  }
339
346
  }
340
- target.utils.writeUpsert(values);
347
+ target.utils.writeUpsert(withoutExpand(values));
348
+ return true;
341
349
  }
342
- async function upsertExpandedField(key, value, targets) {
350
+ async function upsertExpandedField(key, value, targets, parentId) {
343
351
  const target = targets[key];
344
352
  if (!target) {
345
353
  logger.debug("No relation target for expanded field", { collectionName, key });
346
354
  return;
347
355
  }
348
356
  const values = Array.isArray(value) ? value : [value];
349
- await upsertInto(key, target, values);
357
+ const filed = await upsertInto(key, target, values);
350
358
  await upsertExpanded(values, target.relationTargets);
359
+ if (filed && parentId !== void 0) markFiledSubset(target, key, values, parentId);
351
360
  }
352
361
  async function upsertExpanded(records, targets) {
353
362
  if (!targets) return;
354
363
  for (const record of records) {
355
364
  const expandData = record.expand;
356
365
  if (!expandData) continue;
366
+ const id = record.id;
367
+ const parentId = typeof id === "string" ? id : void 0;
357
368
  for (const [key, value] of Object.entries(expandData)) {
358
- await upsertExpandedField(key, value, targets);
369
+ await upsertExpandedField(key, value, targets, parentId);
359
370
  }
360
371
  }
361
372
  }
373
+ function subsetFilter({ field, values }) {
374
+ return values.map((value) => `${field} = "${value.replace(/"/g, '\\"')}"`).join(" || ");
375
+ }
376
+ const loadedSubsets = /* @__PURE__ */ new Map();
377
+ function markSubsetLoaded(field, value) {
378
+ let values = loadedSubsets.get(field);
379
+ if (!values) {
380
+ values = /* @__PURE__ */ new Set();
381
+ loadedSubsets.set(field, values);
382
+ }
383
+ values.add(value);
384
+ }
385
+ function loadedSubsetCount() {
386
+ let count = 0;
387
+ for (const values of loadedSubsets.values()) count += values.size;
388
+ return count;
389
+ }
390
+ function subsetIsLoaded({ field, values }) {
391
+ const marked = loadedSubsets.get(field);
392
+ return marked !== void 0 && values.every((value) => marked.has(value));
393
+ }
394
+ function forgetMarkedValue(marked, value) {
395
+ if (typeof value === "string") return marked.delete(value) ? [value] : [];
396
+ if (!Array.isArray(value)) return [];
397
+ return value.filter((item) => typeof item === "string" && marked.delete(item));
398
+ }
399
+ function matchesMarkedSubset(query, isMarked) {
400
+ if (query.queryKey[0] !== collectionName) return false;
401
+ const request = query.queryKey[1];
402
+ if (!request?.subset) return false;
403
+ const { field, values } = request.subset;
404
+ return values.some((value) => isMarked(field, value));
405
+ }
406
+ function invalidateMarkedQueries(isMarked) {
407
+ void queryClient.invalidateQueries({ predicate: (query) => matchesMarkedSubset(query, isMarked) }).catch(
408
+ (error) => logger.error("Failed to invalidate marked subset queries", {
409
+ collectionName,
410
+ error
411
+ })
412
+ );
413
+ }
414
+ function invalidateForgottenSubset(field, values) {
415
+ invalidateMarkedQueries(
416
+ (queryField, value) => queryField === field && values.includes(value)
417
+ );
418
+ }
419
+ function invalidateAllMarkedSubsets() {
420
+ invalidateMarkedQueries((field, value) => loadedSubsets.get(field)?.has(value) ?? false);
421
+ }
422
+ function forgetMarksFor(row) {
423
+ if (!row || typeof row !== "object") return;
424
+ for (const [field, marked] of loadedSubsets) {
425
+ const forgotten = forgetMarkedValue(marked, row[field]);
426
+ if (forgotten.length > 0) {
427
+ queueMicrotask(() => invalidateForgottenSubset(field, forgotten));
428
+ }
429
+ }
430
+ }
431
+ function rowsFromStore(ids) {
432
+ const rows = [];
433
+ for (const id of ids) {
434
+ const row = collection._state.syncedData.get(id);
435
+ if (!row) return void 0;
436
+ rows.push(row);
437
+ }
438
+ return rows;
439
+ }
440
+ function idSubsetFromStore(subset, limit) {
441
+ const present = rowsFromStore(subset.values);
442
+ if (!present) return void 0;
443
+ return limit ? present.slice(0, limit) : present;
444
+ }
445
+ function fieldSubsetFromStore(subset, limit) {
446
+ if (!subsetIsLoaded(subset)) return void 0;
447
+ const wanted = new Set(subset.values);
448
+ const rows = [...collection._state.syncedData.values()].filter(
449
+ (row) => matchesSubset(row, subset.field, wanted)
450
+ );
451
+ return limit ? rows.slice(0, limit) : rows;
452
+ }
453
+ function servedFromStore(request) {
454
+ const { subset, sort, limit } = request;
455
+ if (!subset) return void 0;
456
+ if (subset.values.length === 0) return [];
457
+ if (request.expand) return void 0;
458
+ if (sort && limit) return void 0;
459
+ return subset.field === "id" ? idSubsetFromStore(subset, limit) : fieldSubsetFromStore(subset, limit);
460
+ }
362
461
  async function fetchItems(request) {
363
- const { filter, sort, limit } = request;
364
- const expand2 = activeExpand(request);
462
+ const served = servedFromStore(request);
463
+ if (served) return served;
464
+ if (request.subset && request.subset.field !== "id" && backRelationTargetsFor(request).length === 0) {
465
+ await awaitPendingFiling(collection, request.subset.field);
466
+ const servedAfterFiling = servedFromStore(request);
467
+ if (servedAfterFiling) return servedAfterFiling;
468
+ }
469
+ const { sort, limit, subset } = request;
470
+ const filter = subset ? subsetFilter(subset) : request.filter;
471
+ const expand = activeExpand(request);
365
472
  if (limit) {
366
473
  const result = await pb.collection(collectionName).getList(1, limit, {
367
474
  filter,
368
475
  sort,
369
476
  skipTotal: true,
370
477
  // Optimize by skipping total count
371
- expand: expand2
478
+ expand
372
479
  });
373
480
  return result.items;
374
481
  }
375
482
  return await pb.collection(collectionName).getFullList({
376
483
  filter,
377
484
  sort,
378
- expand: expand2
485
+ expand
379
486
  });
380
487
  }
381
488
  const inFlightConfirmedIds = /* @__PURE__ */ new Set();
@@ -408,6 +515,13 @@ function buildCollection(input) {
408
515
  async function fetchRecords(request, queryKey) {
409
516
  const confirmedMidFlight = /* @__PURE__ */ new Set();
410
517
  inFlightConfirmedIds.add(confirmedMidFlight);
518
+ let settleFiling = () => void 0;
519
+ const filed = new Promise((resolve) => {
520
+ settleFiling = resolve;
521
+ });
522
+ const unregisterFilings = backRelationTargetsFor(request).map(
523
+ ({ target, field }) => registerPendingFiling(target, field, filed)
524
+ );
411
525
  try {
412
526
  let items;
413
527
  try {
@@ -422,50 +536,14 @@ function buildCollection(input) {
422
536
  throw error;
423
537
  }
424
538
  await upsertExpanded(items, relationTargets);
425
- if (request.expand) writeExpandedRows(items);
426
- return withRowsConfirmedMidFlight(items, confirmedMidFlight);
539
+ return withRowsConfirmedMidFlight(
540
+ stripFetchedRelations(items, activeExpand(request)),
541
+ confirmedMidFlight
542
+ );
427
543
  } finally {
428
544
  inFlightConfirmedIds.delete(confirmedMidFlight);
429
- }
430
- }
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
- );
545
+ settleFiling();
546
+ for (const unregister of unregisterFilings) unregister();
469
547
  }
470
548
  }
471
549
  const queryCollectionConfig = queryCollectionOptions({
@@ -575,22 +653,14 @@ function buildCollection(input) {
575
653
  const paths = (opts.subscription && viewPaths.get(opts.subscription)) ?? subscribingViewPaths;
576
654
  return paths ? { ...opts, expand: paths } : opts;
577
655
  }
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
656
  const innerSync = collectionOptions.sync.sync;
584
657
  collectionOptions.sync = {
585
658
  ...collectionOptions.sync,
586
659
  sync: (params) => {
587
660
  const guardedWrite = (message) => {
588
661
  const op = message;
662
+ if (op.type === "delete" && !applyingOwnWrite) forgetMarksFor(op.value);
589
663
  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
664
  return params.write(message);
595
665
  };
596
666
  const res = innerSync({ ...params, write: guardedWrite });
@@ -604,14 +674,6 @@ function buildCollection(input) {
604
674
  }
605
675
  };
606
676
  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
677
  const views = /* @__PURE__ */ new Map();
616
678
  function createView(paths) {
617
679
  const view = Object.create(collection);
@@ -630,18 +692,18 @@ function buildCollection(input) {
630
692
  }
631
693
  }
632
694
  },
633
- expand: {
695
+ fetchRelations: {
634
696
  value: () => {
635
- throw new Error(`A view of "${collectionName}" cannot be expanded further`);
697
+ throw new Error(`A view of "${collectionName}" cannot fetch further relations`);
636
698
  }
637
699
  }
638
700
  });
639
701
  return view;
640
702
  }
641
- function expand(...paths) {
703
+ function fetchRelations(...paths) {
642
704
  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;
705
+ const all = normalizePaths([...alwaysFetch, ...paths]);
706
+ if (all.every((path) => alwaysFetch.includes(path))) return collection;
645
707
  const key = all.join(",");
646
708
  let view = views.get(key);
647
709
  if (!view) {
@@ -698,15 +760,16 @@ function buildCollection(input) {
698
760
  if (!collection.utils) return;
699
761
  markEchoPresence(event);
700
762
  if (isStaleEcho(event)) return;
763
+ const [stored] = stripFetchedRelations([event.record], pendingSubscribeExpand());
701
764
  try {
702
765
  writeOwn(
703
766
  () => collection.utils.writeBatch(() => {
704
767
  switch (event.action) {
705
768
  case "create":
706
- collection.utils.writeInsert(event.record);
769
+ collection.utils.writeInsert(stored);
707
770
  break;
708
771
  case "update":
709
- collection.utils.writeUpsert(event.record);
772
+ collection.utils.writeUpsert(stored);
710
773
  break;
711
774
  case "delete":
712
775
  if (event.record && "id" in event.record) {
@@ -734,15 +797,9 @@ function buildCollection(input) {
734
797
  })
735
798
  );
736
799
  }
737
- propagateRelatedChange(
738
- collection,
739
- event.action,
740
- event.record,
741
- /* @__PURE__ */ new Set()
742
- );
743
800
  };
744
801
  function pendingSubscribeExpand() {
745
- return joinPaths([...alwaysExpand, ...requestedExpand]);
802
+ return joinPaths([...alwaysFetch, ...requestedExpand]);
746
803
  }
747
804
  function activeExpandTargets() {
748
805
  const targets = /* @__PURE__ */ new Set();
@@ -789,11 +846,11 @@ function buildCollection(input) {
789
846
  }
790
847
  function realtimeSubscribeOptions() {
791
848
  const base = factoryOptions?.subscribeOptions?.();
792
- const expand2 = joinPaths([
849
+ const expand = joinPaths([
793
850
  ...splitPaths(pendingSubscribeExpand()),
794
851
  ...splitPaths(base?.expand)
795
852
  ]);
796
- return expand2 ? { ...base, expand: expand2 } : base;
853
+ return expand ? { ...base, expand } : base;
797
854
  }
798
855
  const doStartSubscription = async () => {
799
856
  if (isSubscribed) return;
@@ -821,7 +878,11 @@ function buildCollection(input) {
821
878
  }
822
879
  };
823
880
  const doStopSubscription = async (releaseTargets = true) => {
824
- if (releaseTargets) syncHeldSubscriptions(/* @__PURE__ */ new Set());
881
+ if (releaseTargets) {
882
+ syncHeldSubscriptions(/* @__PURE__ */ new Set());
883
+ invalidateAllMarkedSubsets();
884
+ loadedSubsets.clear();
885
+ }
825
886
  if (!isSubscribed || !unsubscribeFn) return;
826
887
  try {
827
888
  await unsubscribeFn();
@@ -909,15 +970,22 @@ function buildCollection(input) {
909
970
  }
910
971
  }
911
972
  );
973
+ registerMarkInvalidationEvents(
974
+ {
975
+ onStatusChange: (callback) => collection.on("status:change", callback),
976
+ onTruncate: (callback) => collection.on("truncate", callback)
977
+ },
978
+ () => loadedSubsets.clear()
979
+ );
912
980
  Object.assign(collection, {
913
981
  collectionName,
914
982
  relationTargets,
915
983
  waitForSubscription,
916
984
  isSubscribed: () => isSubscribed,
917
985
  heldRelationTargetCount: () => heldTargetSubscriptions.size,
918
- expand,
919
- relationDependents,
920
- applyRelatedChange
986
+ markSubsetLoaded,
987
+ loadedSubsetCount,
988
+ fetchRelations
921
989
  });
922
990
  return collection;
923
991
  }
@@ -946,5 +1014,5 @@ function newRecordId() {
946
1014
  }
947
1015
 
948
1016
  export { createCollection, newRecordId, resetLogger, setLogger };
949
- //# sourceMappingURL=chunk-KMR6I4ZC.js.map
950
- //# sourceMappingURL=chunk-KMR6I4ZC.js.map
1017
+ //# sourceMappingURL=chunk-LT7BDI37.js.map
1018
+ //# sourceMappingURL=chunk-LT7BDI37.js.map