@agentshouse/mdruntime 0.1.0-cc294e382554d → 0.1.0-cc56db96f86ae

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.
Files changed (125) hide show
  1. package/AGENTS.md +28 -23
  2. package/README.md +9 -8
  3. package/dist/adapter.d.ts +11 -10
  4. package/dist/adapter.d.ts.map +1 -1
  5. package/dist/bootstrap.d.ts.map +1 -1
  6. package/dist/bootstrap.js +60 -29
  7. package/dist/change-set.d.ts.map +1 -1
  8. package/dist/change-set.js +2 -6
  9. package/dist/change.d.ts +31 -1
  10. package/dist/change.d.ts.map +1 -1
  11. package/dist/change.js +10 -1
  12. package/dist/chat/house.d.ts +25 -0
  13. package/dist/chat/house.d.ts.map +1 -0
  14. package/dist/chat/house.js +38 -0
  15. package/dist/chat/input.d.ts +3 -1
  16. package/dist/chat/input.d.ts.map +1 -1
  17. package/dist/chat/input.js +11 -18
  18. package/dist/chat/message.d.ts.map +1 -1
  19. package/dist/chat/message.js +45 -10
  20. package/dist/chat/messages.d.ts +6 -1
  21. package/dist/chat/messages.d.ts.map +1 -1
  22. package/dist/chat/messages.js +14 -11
  23. package/dist/chat/name.d.ts +1 -0
  24. package/dist/chat/name.d.ts.map +1 -1
  25. package/dist/chat/name.js +1 -1
  26. package/dist/chat/opened.d.ts +1 -0
  27. package/dist/chat/opened.d.ts.map +1 -1
  28. package/dist/chat/opened.js +2 -0
  29. package/dist/chat/overlay.d.ts.map +1 -1
  30. package/dist/chat/overlay.js +1 -5
  31. package/dist/chat/parts.d.ts +1 -0
  32. package/dist/chat/parts.d.ts.map +1 -1
  33. package/dist/chat/parts.js +3 -2
  34. package/dist/chat/provider.d.ts +5 -1
  35. package/dist/chat/provider.d.ts.map +1 -1
  36. package/dist/chat/provider.js +58 -77
  37. package/dist/chat/ready.d.ts +4 -1
  38. package/dist/chat/ready.d.ts.map +1 -1
  39. package/dist/chat/ready.js +7 -5
  40. package/dist/chat/room-file.d.ts +8 -0
  41. package/dist/chat/room-file.d.ts.map +1 -0
  42. package/dist/chat/room-file.js +67 -0
  43. package/dist/chat/style.d.ts.map +1 -1
  44. package/dist/chat/style.js +4 -0
  45. package/dist/chat.d.ts +1 -0
  46. package/dist/chat.d.ts.map +1 -1
  47. package/dist/client.d.ts +4 -1
  48. package/dist/client.d.ts.map +1 -1
  49. package/dist/client.js +2 -0
  50. package/dist/collection.d.ts +2 -2
  51. package/dist/collection.d.ts.map +1 -1
  52. package/dist/collection.js +56 -40
  53. package/dist/connection.d.ts +5 -1
  54. package/dist/connection.d.ts.map +1 -1
  55. package/dist/connection.js +723 -158
  56. package/dist/conversation.d.ts +2 -2
  57. package/dist/conversation.d.ts.map +1 -1
  58. package/dist/conversation.js +18 -29
  59. package/dist/digest.d.ts +2 -0
  60. package/dist/digest.d.ts.map +1 -0
  61. package/dist/digest.js +100 -0
  62. package/dist/durability.d.ts +1 -1
  63. package/dist/file.d.ts +2 -10
  64. package/dist/file.d.ts.map +1 -1
  65. package/dist/file.js +3 -120
  66. package/dist/host.d.ts +1 -1
  67. package/dist/host.d.ts.map +1 -1
  68. package/dist/host.js +7 -3
  69. package/dist/index.d.ts +6 -4
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/index.js +2 -0
  72. package/dist/memory.d.ts.map +1 -1
  73. package/dist/memory.js +9 -5
  74. package/dist/outcome.d.ts +20 -1
  75. package/dist/outcome.d.ts.map +1 -1
  76. package/dist/outcome.js +22 -0
  77. package/dist/overlay.d.ts +1 -1
  78. package/dist/overlay.d.ts.map +1 -1
  79. package/dist/overlay.js +27 -3
  80. package/dist/query.d.ts +2 -1
  81. package/dist/query.d.ts.map +1 -1
  82. package/dist/query.js +1 -0
  83. package/dist/reach.d.ts +5 -1
  84. package/dist/reach.d.ts.map +1 -1
  85. package/dist/reach.js +144 -39
  86. package/dist/read-side.d.ts +4 -4
  87. package/dist/read-side.d.ts.map +1 -1
  88. package/dist/read-side.js +585 -232
  89. package/dist/readiness.d.ts +1 -1
  90. package/dist/readiness.d.ts.map +1 -1
  91. package/dist/recovery.d.ts +2 -2
  92. package/dist/recovery.d.ts.map +1 -1
  93. package/dist/recovery.js +8 -7
  94. package/dist/refresh.js +8 -8
  95. package/dist/replica.d.ts +1 -2
  96. package/dist/replica.d.ts.map +1 -1
  97. package/dist/replica.js +69 -48
  98. package/dist/room-draft.d.ts +3 -1
  99. package/dist/room-draft.d.ts.map +1 -1
  100. package/dist/room-draft.js +21 -13
  101. package/dist/save.d.ts +43 -7
  102. package/dist/save.d.ts.map +1 -1
  103. package/dist/save.js +329 -77
  104. package/dist/search.d.ts +1 -1
  105. package/dist/search.d.ts.map +1 -1
  106. package/dist/search.js +61 -10
  107. package/dist/shared.d.ts +5 -1
  108. package/dist/shared.d.ts.map +1 -1
  109. package/dist/shared.js +72 -9
  110. package/dist/store.d.ts +8 -13
  111. package/dist/store.d.ts.map +1 -1
  112. package/dist/store.js +189 -230
  113. package/dist/tables.d.ts +181 -0
  114. package/dist/tables.d.ts.map +1 -0
  115. package/dist/tables.js +309 -0
  116. package/dist/turn.d.ts +1 -1
  117. package/dist/turn.d.ts.map +1 -1
  118. package/dist/turn.js +9 -6
  119. package/dist/windows.d.ts +0 -5
  120. package/dist/windows.d.ts.map +1 -1
  121. package/dist/windows.js +0 -9
  122. package/dist/wire.d.ts +8 -0
  123. package/dist/wire.d.ts.map +1 -1
  124. package/dist/wire.js +2 -0
  125. package/package.json +6 -2
package/dist/read-side.js CHANGED
@@ -1,8 +1,9 @@
1
- import { and, BasicIndex, configureIndexDevMode, count, createCollection, createLiveQueryCollection, deepEquals, eq, inArray, or, toArray, } from '@tanstack/db';
1
+ import { and, coalesce, configureIndexDevMode, count, createCollection, createLiveQueryCollection, deepEquals, eq, inArray, not, or, } from '@tanstack/db';
2
2
  import { ARRAY_INDEX, parse, pointerSegments } from '@agentshouse/mdmodel';
3
+ import { rowsOf, } from './observation.js';
3
4
  import { documentKey } from './rows.js';
4
- import { after, inUnits, ROW_WEIGHT } from './turn.js';
5
- import { beyond, cursorOf, MAXIMUM_WINDOW, windowed } from './windows.js';
5
+ import { after, inUnits, ROW_WEIGHT, stepped } from './turn.js';
6
+ import { beyond, cursorOf, FROM_START, MAXIMUM_WINDOW } from './windows.js';
6
7
  function every(expressions) {
7
8
  const [first, second, ...rest] = expressions;
8
9
  return second === undefined ? first : and(first, second, ...rest);
@@ -55,18 +56,21 @@ function conditioned(condition, row) {
55
56
  function narrowed(query, where, alias) {
56
57
  return where === null ? query : query.fn.where((row) => conditioned(where, row[alias]));
57
58
  }
58
- function reached(reach, room, document) {
59
- return reach === null
60
- ? []
61
- : [or(inArray(room, [...reach.rooms]), inArray(document, [...reach.documents]))];
59
+ function reached(reach, locked, room, document) {
60
+ return [
61
+ ...(reach === null
62
+ ? []
63
+ : [or(inArray(room, [...reach.rooms]), inArray(document, [...reach.documents]))]),
64
+ ...(locked.length === 0 ? [] : [not(inArray(room, [...locked]))]),
65
+ ];
62
66
  }
63
- function scoped(scope, row, rooms, reach, paths = null, folder = null) {
67
+ function scoped(scope, row, rooms, reach, locked, paths = null, folder = null) {
64
68
  return every([
65
69
  eq(row.kind, 'document'),
66
70
  ...(rooms === null ? [] : [inArray(row.room, [...rooms])]),
67
71
  ...(paths === null ? [] : [inArray(row.path, [...paths])]),
68
72
  ...(folder === null ? [] : [eq(row.folder, folder)]),
69
- ...reached(reach, row.room, row.key),
73
+ ...reached(reach, locked, row.room, row.key),
70
74
  ...(scope.types === null ? [] : [inArray(row.type, [...scope.types])]),
71
75
  ...(scope.pathPrefix === null ? [] : [inArray(scope.pathPrefix, row.directories)]),
72
76
  ]);
@@ -74,33 +78,42 @@ function scoped(scope, row, rooms, reach, paths = null, folder = null) {
74
78
  function keyed(key, row) {
75
79
  return key.of === 'field' ? row.values[key.field] : row[key.of];
76
80
  }
81
+ function readied(live) {
82
+ return new Promise((resolve) => {
83
+ live.onFirstReady(resolve);
84
+ });
85
+ }
86
+ function streamed(value) {
87
+ return coalesce(value);
88
+ }
77
89
  function live(query) {
78
90
  return createLiveQueryCollection({
79
91
  query: query,
80
92
  startSync: true,
93
+ gcTime: 0,
81
94
  });
82
95
  }
83
- function related(q, rows, scope, field, kept, reach) {
96
+ function related(q, rows, scope, field, kept, reach, locked) {
84
97
  const matched = kept === 'source' ? 'target' : 'source';
85
98
  const from = q.from({ r: rows });
86
99
  const joined = from
87
100
  .where(({ r }) => every([eq(r.kind, 'reference'), ...(field === null ? [] : [inArray(field, r.forms)])]))
88
- .innerJoin({ m: rows }, ({ r, m }) => eq(r[matched], m.key))
89
- .where(({ m }) => scoped(scope, m, null, reach));
101
+ .innerJoin({ m: rows }, ({ r, m }) => eq(streamed(r[matched]), streamed(m.key)))
102
+ .where(({ m }) => scoped(scope, m, null, reach, locked));
90
103
  return narrowed(joined, scope.where, 'm')
91
104
  .groupBy(({ r }) => r[kept])
92
105
  .select(({ r }) => ({ key: r[kept] }));
93
106
  }
94
107
  function filtered(q, rows, plan) {
95
108
  const from = q.from({ d: rows });
96
- let query = narrowed(from.where(({ d }) => scoped(plan.root, d, plan.rooms, plan.reach, plan.paths, plan.folder)), plan.root.where, 'd');
109
+ let query = narrowed(from.where(({ d }) => scoped(plan.root, d, plan.rooms, plan.reach, plan.locked, plan.paths, plan.folder)), plan.root.where, 'd');
97
110
  if (plan.hop !== null) {
98
- const hop = related(q, rows, plan.hop, plan.hop.field, 'source', plan.reach);
99
- query = query.innerJoin({ h: hop }, ({ d, h }) => eq(d.key, h.key));
111
+ const hop = related(q, rows, plan.hop, plan.hop.field, 'source', plan.reach, plan.locked);
112
+ query = query.innerJoin({ h: hop }, ({ d, h }) => eq(streamed(d.key), streamed(h.key)));
100
113
  }
101
114
  if (plan.backlinks !== null) {
102
- const backlinks = related(q, rows, plan.backlinks, plan.backlinks.field, 'target', plan.reach);
103
- query = query.innerJoin({ k: backlinks }, ({ d, k }) => eq(d.key, k.key));
115
+ const backlinks = related(q, rows, plan.backlinks, plan.backlinks.field, 'target', plan.reach, plan.locked);
116
+ query = query.innerJoin({ k: backlinks }, ({ d, k }) => eq(streamed(d.key), streamed(k.key)));
104
117
  }
105
118
  return query;
106
119
  }
@@ -139,10 +152,10 @@ function documents(q, rows, plan, shape) {
139
152
  query = query.fn.where(({ d }) => beyond([...shape.orderBy.map(({ key }) => storedValue(key, d)), d.key], cursor, orderings));
140
153
  }
141
154
  for (const { key, options } of shape.orderBy) {
142
- query = query.orderBy(({ d }) => keyed(key, d), options);
155
+ query = query.orderBy(({ d }) => streamed(keyed(key, d)), options);
143
156
  }
144
157
  if (shape.orderBy.length > 0 || plan.limit !== null || plan.after !== null) {
145
- query = query.orderBy(({ d }) => d.key, { stringSort: 'lexical' });
158
+ query = query.orderBy(({ d }) => streamed(d.key), { stringSort: 'lexical' });
146
159
  }
147
160
  if (plan.limit !== null)
148
161
  query = query.limit(plan.limit + 1);
@@ -154,18 +167,68 @@ function documents(q, rows, plan, shape) {
154
167
  ...Object.fromEntries(shape.orderBy.map(({ key }, index) => [`order${index}`, keyed(key, d)])),
155
168
  ...(shape.fields === null ? {} : { fields: d.fields }),
156
169
  ...(bodied(shape) ? { text: d.text } : {}),
157
- ...(shape.backlinks ? { backlinks: toArray(backlinked(q, rows, d, plan.reach)) } : {}),
158
170
  ...(shape.change ? { change: d.change } : {}),
159
171
  ...(shape.fileId ? { fileId: d.fileId } : {}),
160
172
  }));
161
173
  }
162
- function backlinked(q, rows, document, reach) {
174
+ function references(q, rows, reach, locked) {
163
175
  const from = q.from({ r: rows });
164
176
  return from
165
- .where(({ r }) => every([eq(r.target, document.key), ...reached(reach, r.room, r.source)]))
166
- .orderBy(({ r }) => r.path, { stringSort: 'lexical' })
167
- .orderBy(({ r }) => r.field, { stringSort: 'lexical' })
168
- .select(({ r }) => ({ path: r.path, type: r.type, field: r.field }));
177
+ .where(({ r }) => every([eq(r.kind, 'reference'), ...reached(reach, locked, r.room, r.source)]))
178
+ .select(({ r }) => ({
179
+ key: r.key,
180
+ target: r.target,
181
+ path: r.path,
182
+ type: r.type,
183
+ field: r.field,
184
+ }));
185
+ }
186
+ function lexical(left, right) {
187
+ return left < right ? -1 : left > right ? 1 : 0;
188
+ }
189
+ function linkOrder([leftKey, left], [rightKey, right]) {
190
+ return (lexical(left.path, right.path) || lexical(left.field, right.field) || lexical(leftKey, rightKey));
191
+ }
192
+ function linkAt(held, entry) {
193
+ let low = 0;
194
+ for (let high = held.length; low < high;) {
195
+ const middle = (low + high) >>> 1;
196
+ if (linkOrder(held[middle], entry) < 0)
197
+ low = middle + 1;
198
+ else
199
+ high = middle;
200
+ }
201
+ return low;
202
+ }
203
+ function targetOf(change) {
204
+ return change.value.target;
205
+ }
206
+ function relink(linked, { type, key, value }) {
207
+ const row = value;
208
+ const entry = [String(key), { path: row.path, type: row.type, field: row.field }];
209
+ const held = linked.get(row.target) ?? [];
210
+ const at = linkAt(held, entry);
211
+ const present = held[at]?.[0] === entry[0];
212
+ const shifted = (type === 'delete') === present ? held.length - at : 0;
213
+ if (type === 'delete') {
214
+ if (present)
215
+ held.splice(at, 1);
216
+ }
217
+ else if (present)
218
+ held[at] = entry;
219
+ else
220
+ held.splice(at, 0, entry);
221
+ if (held.length === 0)
222
+ linked.delete(row.target);
223
+ else
224
+ linked.set(row.target, held);
225
+ return shifted;
226
+ }
227
+ function relinked(linked, changes) {
228
+ return strides(changes, (change) => ROW_WEIGHT + relink(linked, change) / HELD_PER_WEIGHT);
229
+ }
230
+ function linking(links, received) {
231
+ return links.subscribeChanges(received, { includeInitialState: true });
169
232
  }
170
233
  function groups(q, stage, shape) {
171
234
  const keys = shape.groupBy.map((_, index) => `key${index}`);
@@ -186,13 +249,19 @@ function built(rows, plan) {
186
249
  const count = plan.total
187
250
  ? live((q) => filtered(q, rows, plan).select(({ d }) => ({ key: d.key })))
188
251
  : null;
252
+ const links = shape.backlinks
253
+ ? live((q) => references(q, rows, plan.reach, plan.locked))
254
+ : null;
189
255
  return {
190
256
  live: collection,
191
257
  count,
258
+ links,
192
259
  dispose: () => {
193
260
  void collection.cleanup();
194
261
  if (count !== null)
195
262
  void count.cleanup();
263
+ if (links !== null)
264
+ void links.cleanup();
196
265
  },
197
266
  };
198
267
  }
@@ -204,6 +273,7 @@ function built(rows, plan) {
204
273
  return {
205
274
  live: collection,
206
275
  count: null,
276
+ links: null,
207
277
  dispose: () => {
208
278
  void collection.cleanup();
209
279
  void stage.cleanup();
@@ -276,7 +346,7 @@ function cut(value, mask) {
276
346
  function selected(fields, pointers) {
277
347
  return (cut(fields, masked(pointers)) ?? {});
278
348
  }
279
- function documentOf(key, row, shape, bodies) {
349
+ function documentOf(key, row, shape, bodies, linked) {
280
350
  if (row.type === null) {
281
351
  const binary = row.binary;
282
352
  return {
@@ -305,19 +375,52 @@ function documentOf(key, row, shape, bodies) {
305
375
  ...(shape.markdown && body !== null ? { markdown: body.content } : {}),
306
376
  ...(shape.backlinks
307
377
  ? {
308
- backlinks: row.backlinks
378
+ backlinks: (linked.get(documentKey(row.room, row.path)) ?? [])
309
379
  .slice(0, MAXIMUM_WINDOW)
310
- .map(({ path, type, field }) => ({ path, type, field })),
380
+ .map(([, link]) => link),
311
381
  }
312
382
  : {}),
313
383
  ...(shape.change ? { change: row.change } : {}),
314
384
  ...(shape.fileId ? { fileId: row.fileId } : {}),
315
385
  };
316
386
  }
387
+ function valueAt(row, path) {
388
+ let value = row;
389
+ for (const part of path)
390
+ value = value?.[part];
391
+ return value;
392
+ }
317
393
  function weight(row) {
318
394
  return ROW_WEIGHT + (typeof row?.text === 'string' ? row.text.length : 0);
319
395
  }
320
396
  const HELD_PER_WEIGHT = 16;
397
+ const STRIDE = ROW_WEIGHT * 16;
398
+ function* strides(items, each) {
399
+ let spent = 0;
400
+ for (const item of items) {
401
+ spent += each(item);
402
+ if (spent < STRIDE)
403
+ continue;
404
+ yield spent;
405
+ spent = 0;
406
+ }
407
+ if (spent > 0)
408
+ yield spent;
409
+ }
410
+ function joined(plan) {
411
+ return (plan.hop !== null ||
412
+ plan.backlinks !== null ||
413
+ (plan.shape.kind === 'documents' && plan.shape.backlinks));
414
+ }
415
+ function admission(plan) {
416
+ if (joined(plan))
417
+ return () => true;
418
+ const rooms = plan.rooms === null ? null : new Set(plan.rooms);
419
+ const paths = plan.paths === null ? null : new Set(plan.paths);
420
+ return (row) => row.kind === 'document' &&
421
+ (rooms === null || rooms.has(row.room)) &&
422
+ (paths === null || paths.has(row.path));
423
+ }
321
424
  function groupValues(shape, [key, row]) {
322
425
  return [...shape.orderBy.map(({ by }) => (by === 'count' ? row.count : row[`key${by}`])), key];
323
426
  }
@@ -330,23 +433,33 @@ function groupOrder(shape) {
330
433
  localeOptions: undefined,
331
434
  }));
332
435
  }
333
- function windowOf(plan, entries, total) {
436
+ function* windowOf(plan, keys, valueOf, total, each) {
334
437
  const shape = plan.shape;
335
- if (shape.kind === 'documents') {
336
- return {
337
- ...windowed(entries, plan.limit, plan.after?.text, ([, row]) => documentCursor(shape, row)),
338
- total,
339
- };
340
- }
341
- const cursor = plan.after?.values ?? null;
342
- const orderings = groupOrder(shape);
343
- const following = cursor === null
344
- ? entries
345
- : entries.filter((entry) => beyond(groupValues(shape, entry), cursor, orderings));
346
- return {
347
- ...windowed(following, plan.limit, plan.after?.text, (entry) => cursorOf(groupValues(shape, entry))),
348
- total: plan.total ? entries.length : undefined,
349
- };
438
+ const cursor = shape.kind === 'groups' ? (plan.after?.values ?? null) : null;
439
+ const orderings = shape.kind === 'groups' ? groupOrder(shape) : [];
440
+ let all = 0;
441
+ let following = 0;
442
+ let last;
443
+ yield* strides(keys, (key) => {
444
+ all += 1;
445
+ const admitted = shape.kind === 'documents' ||
446
+ cursor === null ||
447
+ beyond(groupValues(shape, [key, valueOf(key)]), cursor, orderings);
448
+ if (admitted)
449
+ following += 1;
450
+ if (!admitted || (plan.limit !== null && following > plan.limit))
451
+ return ROW_WEIGHT;
452
+ last = key;
453
+ return each(key);
454
+ });
455
+ const next = plan.limit === null || following <= plan.limit
456
+ ? undefined
457
+ : last === undefined
458
+ ? (plan.after?.text ?? FROM_START)
459
+ : shape.kind === 'documents'
460
+ ? documentCursor(shape, valueOf(last))
461
+ : cursorOf(groupValues(shape, [last, valueOf(last)]));
462
+ return { next, total: shape.kind === 'documents' ? total : plan.total ? all : undefined };
350
463
  }
351
464
  function framed(rows, { next, total }) {
352
465
  return {
@@ -355,27 +468,36 @@ function framed(rows, { next, total }) {
355
468
  ...(total === undefined ? {} : { total }),
356
469
  };
357
470
  }
358
- function rowsOfPlan(plan, entries, total, bodies, each = (row) => row) {
359
- const shape = plan.shape;
360
- const { kept, ...window } = windowOf(plan, entries, total);
361
- const keys = kept.map(([key]) => key);
362
- if (shape.kind === 'documents') {
363
- const documents = [];
364
- return after(inUnits(kept, ([key, row]) => {
365
- documents.push(each(documentOf(key, row, shape, bodies)));
366
- return weight(row);
367
- }), () => ({ rows: framed({ documents }, window), keys, ...window }));
368
- }
369
- const groups = [];
370
- return after(inUnits(kept, ([, row]) => {
371
- groups.push(each({
372
- key: shape.groupBy.map((_, index) => row[`key${index}`]),
373
- count: row.count,
374
- }));
375
- return ROW_WEIGHT;
376
- }), () => ({ rows: framed({ groups }, window), keys, ...window }));
471
+ function rowOf(shape, [key, row], bodies, linked) {
472
+ if (shape.kind === 'documents')
473
+ return documentOf(key, row, shape, bodies, linked);
474
+ return { key: shape.groupBy.map((_, index) => row[`key${index}`]), count: row.count };
377
475
  }
378
- export function reconciled(previous, next) {
476
+ function resultOf(grouped, rows, coverage, window) {
477
+ return grouped
478
+ ? framed({ groups: rows, coverage }, window)
479
+ : framed({ documents: rows, coverage }, window);
480
+ }
481
+ function* rowsOfPlan(plan, keys, valueOf, total, linked) {
482
+ const rows = [];
483
+ const window = yield* windowOf(plan, keys, valueOf, total, (key) => {
484
+ const value = valueOf(key);
485
+ rows.push(structuredClone(rowOf(plan.shape, [key, value], null, linked)));
486
+ return weight(value);
487
+ });
488
+ return plan.shape.kind === 'groups'
489
+ ? framed({ groups: rows }, window)
490
+ : framed({ documents: rows }, window);
491
+ }
492
+ function textOf(row) {
493
+ if (!('path' in row))
494
+ return 0;
495
+ let length = (row.markdown?.length ?? 0) + (row.preamble?.length ?? 0);
496
+ for (const section of Object.values(row.sections ?? {}))
497
+ length += section.length;
498
+ return length;
499
+ }
500
+ export function* reconciled(previous, next) {
379
501
  const before = previous.result;
380
502
  const after = next.result;
381
503
  if ('pending' in before || 'pending' in after) {
@@ -383,40 +505,67 @@ export function reconciled(previous, next) {
383
505
  return null;
384
506
  return { keyed: next, message: next };
385
507
  }
386
- const earlier = 'groups' in before ? before.groups : before.documents;
387
- const held = new Map(previous.keys.map((key, index) => [key, earlier[index]]));
508
+ const earlier = rowsOf(before);
509
+ const held = new Map();
510
+ yield* strides(previous.keys.entries(), ([index, key]) => {
511
+ held.set(key, earlier[index]);
512
+ return ROW_WEIGHT;
513
+ });
514
+ const later = rowsOf(after);
515
+ const present = new Set();
388
516
  const changed = [];
389
- const rows = ('groups' in after ? after.groups : after.documents).map((row, index) => {
390
- const key = next.keys[index];
517
+ const rows = [];
518
+ yield* strides(next.keys.entries(), ([index, key]) => {
519
+ const row = later[index];
391
520
  const kept = held.get(key);
521
+ present.add(key);
392
522
  if (kept !== undefined && deepEquals(kept, row))
393
- return kept;
394
- changed.push([key, row]);
395
- return row;
523
+ rows.push(kept);
524
+ else {
525
+ changed.push([key, row]);
526
+ rows.push(row);
527
+ }
528
+ return ROW_WEIGHT + textOf(row);
529
+ });
530
+ const keyed = {
531
+ result: resultOf('groups' in after, rows, after.coverage, {
532
+ next: after.next,
533
+ total: after.total,
534
+ }),
535
+ keys: next.keys,
536
+ };
537
+ const message = yield* changeOf(previous, keyed, changed, (key) => present.has(key));
538
+ return message === null ? null : { keyed, message };
539
+ }
540
+ function* changeOf(previous, next, changed, present, dropped = () => { }) {
541
+ const before = previous.result;
542
+ const after = next.result;
543
+ const removed = [];
544
+ let at = 0;
545
+ let moved = false;
546
+ yield* strides(previous.keys.entries(), ([index, key]) => {
547
+ if (present(key, index)) {
548
+ moved ||= next.keys[at] !== key;
549
+ at += 1;
550
+ }
551
+ else {
552
+ removed.push(key);
553
+ dropped(key);
554
+ }
555
+ return ROW_WEIGHT;
396
556
  });
397
- const present = new Set(next.keys);
398
- const removed = previous.keys.filter((key) => !present.has(key));
399
- const moved = reordered(previous.keys, next.keys, present);
557
+ moved ||= at !== next.keys.length;
400
558
  const covered = !deepEquals(before.coverage, after.coverage);
401
559
  const framing = reframed(before, after);
402
560
  if (changed.length === 0 && removed.length === 0 && !moved && !covered && framing === null) {
403
561
  return null;
404
562
  }
405
- const window = { next: after.next, total: after.total };
406
563
  return {
407
- keyed: {
408
- result: 'groups' in after
409
- ? framed({ groups: rows, coverage: after.coverage }, window)
410
- : framed({ documents: rows, coverage: after.coverage }, window),
411
- keys: next.keys,
412
- },
413
- message: {
414
- changed,
415
- removed,
416
- order: moved ? next.keys : null,
417
- ...(covered ? { coverage: after.coverage } : {}),
418
- ...framing,
419
- },
564
+ changed,
565
+ removed,
566
+ order: moved ? next.keys : null,
567
+ ...(covered ? { coverage: after.coverage } : {}),
568
+ ...framing,
420
569
  };
421
570
  }
422
571
  function reframed(before, after) {
@@ -426,38 +575,121 @@ function reframed(before, after) {
426
575
  ? null
427
576
  : { ...next, ...total };
428
577
  }
429
- function reordered(previous, next, present) {
430
- let at = 0;
431
- for (const key of previous) {
432
- if (!present.has(key))
433
- continue;
434
- if (next[at] !== key)
435
- return true;
436
- at += 1;
437
- }
438
- return at !== next.length;
439
- }
440
578
  export function createReadSide() {
441
579
  configureIndexDevMode({ enabled: false });
442
- let sync;
443
- const rows = createCollection({
444
- getKey: (row) => row.key,
445
- gcTime: 0,
446
- startSync: true,
447
- autoIndex: 'eager',
448
- defaultIndexType: BasicIndex,
449
- sync: {
450
- rowUpdateMode: 'full',
451
- sync: (params) => {
452
- sync = params;
453
- params.markReady();
454
- },
455
- },
456
- });
580
+ const rows = new Map();
581
+ const roomPaths = new Map();
582
+ const views = new Set();
457
583
  const shared = new Map();
458
584
  let writing = false;
459
585
  let publishing = null;
460
586
  let closed = false;
587
+ function viewOf(plan) {
588
+ let sync;
589
+ const view = {
590
+ plan,
591
+ rows: createCollection({
592
+ getKey: (row) => row.key,
593
+ gcTime: 0,
594
+ startSync: true,
595
+ autoIndex: 'off',
596
+ sync: {
597
+ rowUpdateMode: 'full',
598
+ sync: (params) => {
599
+ sync = params;
600
+ },
601
+ },
602
+ }),
603
+ sync,
604
+ admits: admission(plan),
605
+ stopped: false,
606
+ waiting: null,
607
+ };
608
+ views.add(view);
609
+ return view;
610
+ }
611
+ function freeze(view) {
612
+ view.waiting ??= new Map();
613
+ }
614
+ function thaw(view) {
615
+ const waiting = view.waiting;
616
+ if (waiting === null)
617
+ return;
618
+ let unit = new Map();
619
+ return after(inUnits(waiting, ([key, row]) => {
620
+ waiting.delete(key);
621
+ unit.set(key, row);
622
+ const was = view.rows.get(key);
623
+ return weight(row ?? undefined) + charged(view, was, row ?? undefined);
624
+ }, () => {
625
+ synced(view, unit);
626
+ unit = new Map();
627
+ }), () => {
628
+ if (waiting.size > 0)
629
+ return thaw(view);
630
+ view.waiting = null;
631
+ });
632
+ }
633
+ function dropped(view) {
634
+ view.stopped = true;
635
+ views.delete(view);
636
+ void view.rows.cleanup();
637
+ }
638
+ function* offered(view) {
639
+ const { plan } = view;
640
+ const { rooms, paths } = plan;
641
+ if (joined(plan) || rooms === null || paths === null) {
642
+ for (const key of rows.keys()) {
643
+ if (view.stopped)
644
+ return;
645
+ yield key;
646
+ }
647
+ return;
648
+ }
649
+ for (const room of rooms) {
650
+ for (const path of paths) {
651
+ if (view.stopped)
652
+ return;
653
+ yield documentKey(room, path);
654
+ }
655
+ }
656
+ }
657
+ function written(view, writes) {
658
+ if (view.waiting === null)
659
+ synced(view, writes);
660
+ else
661
+ for (const [key, row] of writes)
662
+ view.waiting.set(key, row);
663
+ }
664
+ function synced(view, writes) {
665
+ if (view.stopped || writes.size === 0)
666
+ return;
667
+ view.sync.begin();
668
+ for (const [key, row] of writes) {
669
+ if (row !== null) {
670
+ view.sync.write({ type: view.rows.has(key) ? 'update' : 'insert', value: row });
671
+ }
672
+ else if (view.rows.has(key))
673
+ view.sync.write({ type: 'delete', key });
674
+ }
675
+ view.sync.commit();
676
+ }
677
+ function filled(view) {
678
+ let unit = new Map();
679
+ return after(inUnits(offered(view), (key) => {
680
+ const row = rows.get(key);
681
+ if (row === undefined || !view.admits(row) || view.rows.has(key))
682
+ return ROW_WEIGHT;
683
+ unit.set(key, row);
684
+ return weight(row) + charged(view, undefined, row);
685
+ }, () => {
686
+ written(view, unit);
687
+ unit = new Map();
688
+ }), () => {
689
+ if (!view.stopped)
690
+ view.sync.markReady();
691
+ });
692
+ }
461
693
  function searched(row) {
462
694
  const read = row.text === null ? null : parse(row.text, row.path);
463
695
  if (read === null || !read.ok)
@@ -471,80 +703,116 @@ export function createReadSide() {
471
703
  sections: read.document.sections,
472
704
  };
473
705
  }
474
- function kept(entry, key, row) {
475
- const held = entry.rows.get(key);
476
- if (held !== undefined && deepEquals(held, row))
477
- return;
478
- entry.rows.set(key, row);
479
- }
480
706
  function counted(entry) {
481
707
  return entry.built.count === null ? undefined : entry.built.count.size;
482
708
  }
483
- function rebuilt(entry) {
484
- return after(rowsOfPlan(entry.plan, [...entry.built.live.entries()], counted(entry), entry.bodies), ({ rows: found, keys, next, total }) => {
485
- const present = new Set(keys);
486
- for (const key of [...entry.rows.keys()])
487
- if (!present.has(key))
488
- entry.rows.delete(key);
489
- for (const key of [...entry.bodies.keys()]) {
490
- if (!present.has(key))
491
- entry.bodies.delete(key);
709
+ function* delivery(entry, whole, dirty, touched, relinking, coverage) {
710
+ const { plan } = entry;
711
+ const live = entry.built.live;
712
+ const valueOf = (key) => live.get(key);
713
+ const previous = entry.result;
714
+ const answered = previous === undefined || 'pending' in previous.result ? null : previous;
715
+ const answeredKeys = answered?.keys ?? [];
716
+ const answeredRows = answered === null ? [] : rowsOf(answered.result);
717
+ const matched = new Uint8Array(answeredKeys.length);
718
+ const generation = (entry.generation += 1);
719
+ const keys = [];
720
+ const changed = [];
721
+ const rows = [];
722
+ let at = 0;
723
+ const touches = (key) => {
724
+ const { room, path } = valueOf(key);
725
+ return touched.has(documentKey(room, path));
726
+ };
727
+ const behind = (key) => !live.has(key) || entry.rows.get(key)?.at === generation;
728
+ yield* relinked(entry.linked, relinking);
729
+ const window = yield* windowOf(plan, live.keys(), valueOf, counted(entry), (key) => {
730
+ let skipped = 0;
731
+ while (skipped < STRIDE &&
732
+ at < answeredKeys.length &&
733
+ answeredKeys[at] !== key &&
734
+ behind(answeredKeys[at])) {
735
+ at += 1;
736
+ skipped += ROW_WEIGHT;
737
+ }
738
+ let holder;
739
+ let prior;
740
+ if (at < answeredKeys.length && answeredKeys[at] === key) {
741
+ prior = answeredRows[at];
742
+ matched[at] = 1;
743
+ at += 1;
492
744
  }
493
- keys.forEach((key, index) => kept(entry, key, rowAt(found, index)));
494
- return { keys, next, total };
745
+ else {
746
+ holder = entry.rows.get(key);
747
+ prior = holder?.row;
748
+ if (holder !== undefined)
749
+ holder.at = generation;
750
+ }
751
+ let row = prior;
752
+ let spent = ROW_WEIGHT;
753
+ if (row === undefined || whole || dirty.has(key) || (touched.size > 0 && touches(key))) {
754
+ const value = valueOf(key);
755
+ const renewed = rowOf(plan.shape, [key, value], entry.bodies, entry.linked);
756
+ if (row === undefined || !deepEquals(row, renewed))
757
+ row = renewed;
758
+ spent = weight(value);
759
+ }
760
+ if (row !== prior) {
761
+ holder ??= entry.rows.get(key);
762
+ if (holder === undefined)
763
+ entry.rows.set(key, { row, at: generation });
764
+ else
765
+ holder.row = row;
766
+ changed.push([key, row]);
767
+ }
768
+ keys.push(key);
769
+ rows.push(row);
770
+ return skipped + spent;
495
771
  });
496
- }
497
- function refreshed(entry, dirty) {
498
- const shape = entry.plan.shape;
499
- const { kept: inWindow, next, total, } = windowOf(entry.plan, [...entry.built.live.entries()], counted(entry));
500
- const keys = inWindow.map(([key]) => key);
501
- const present = new Set(keys);
502
- for (const key of [...entry.rows.keys()]) {
503
- if (present.has(key))
504
- continue;
772
+ const result = {
773
+ result: resultOf(plan.shape.kind === 'groups', rows, coverage, window),
774
+ keys,
775
+ };
776
+ if (previous === undefined)
777
+ return { result, message: null };
778
+ if (answered === null)
779
+ return { result, message: result };
780
+ const message = yield* changeOf(answered, result, changed, (key, index) => matched[index] === 1 || entry.rows.get(key)?.at === generation, (key) => {
505
781
  entry.rows.delete(key);
506
782
  entry.bodies.delete(key);
507
- }
508
- const values = inWindow.filter(([key]) => dirty.has(key) || !entry.rows.has(key));
509
- const done = inUnits(values, ([key, value]) => {
510
- kept(entry, key, documentOf(key, value, shape, entry.bodies));
511
- return weight(value);
512
783
  });
513
- return after(done, () => ({ keys, next, total }));
784
+ return { result, message };
514
785
  }
515
- function current(entry, found, coverage) {
516
- const rows = found.keys.map((key) => entry.rows.get(key));
517
- return {
518
- result: entry.plan.shape.kind === 'groups'
519
- ? framed({ groups: rows, coverage }, found)
520
- : framed({ documents: rows, coverage }, found),
521
- keys: found.keys,
522
- };
523
- }
524
- function delivered(entry, next) {
525
- const previous = entry.result;
526
- entry.result = next;
527
- const message = previous === undefined ? null : changedFrom(previous, next);
786
+ function delivered(entry, { result, message }) {
787
+ entry.result = result;
528
788
  if (message !== null)
529
789
  for (const listener of entry.ready)
530
790
  listener(message);
531
791
  for (const listener of entry.fresh) {
532
792
  entry.ready.add(listener);
533
- listener(next);
793
+ listener(result);
534
794
  }
535
795
  entry.fresh.clear();
536
796
  }
537
797
  function run(entry) {
538
798
  while (entry.listeners.size > 0 &&
539
- (entry.whole || entry.dirty.size > 0 || entry.stale || entry.fresh.size > 0)) {
799
+ (entry.whole ||
800
+ entry.dirty.size > 0 ||
801
+ entry.touched.size > 0 ||
802
+ entry.stale ||
803
+ entry.fresh.size > 0)) {
540
804
  entry.stale = false;
541
805
  const cover = entry.cover();
542
806
  if ('pending' in cover) {
543
807
  entry.whole = true;
544
808
  entry.dirty.clear();
809
+ entry.touched.clear();
545
810
  entry.rows.clear();
546
811
  entry.bodies.clear();
547
- delivered(entry, { result: cover, keys: [] });
812
+ const result = { result: cover, keys: [] };
813
+ const previous = entry.result;
814
+ const same = previous === undefined || deepEquals(previous.result, cover);
815
+ delivered(entry, { result, message: same ? null : result });
548
816
  break;
549
817
  }
550
818
  const whole = entry.whole ||
@@ -552,23 +820,38 @@ export function createReadSide() {
552
820
  entry.result === undefined ||
553
821
  'pending' in entry.result.result;
554
822
  const dirty = entry.dirty;
823
+ const touched = entry.touched;
824
+ const relinking = entry.relinking;
555
825
  entry.dirty = new Set();
826
+ entry.touched = new Set();
827
+ entry.relinking = [];
556
828
  entry.whole = false;
557
- const found = whole ? rebuilt(entry) : refreshed(entry, dirty);
558
- if (found instanceof Promise) {
559
- void found
560
- .then((window) => {
829
+ freeze(entry.view);
830
+ let made;
831
+ try {
832
+ made = stepped(delivery(entry, whole, dirty, touched, relinking, cover.coverage));
833
+ }
834
+ catch (error) {
835
+ void thaw(entry.view);
836
+ throw error;
837
+ }
838
+ if (made instanceof Promise) {
839
+ void made
840
+ .then((delivering) => {
561
841
  if (entry.listeners.size > 0)
562
- delivered(entry, current(entry, window, cover.coverage));
842
+ delivered(entry, delivering);
843
+ })
844
+ .finally(() => thaw(entry.view))
845
+ .then(() => {
563
846
  entry.running = false;
564
847
  wake(entry);
565
- })
566
- .catch(() => {
848
+ }, () => {
567
849
  entry.running = false;
568
850
  });
569
851
  return;
570
852
  }
571
- delivered(entry, current(entry, found, cover.coverage));
853
+ void thaw(entry.view);
854
+ delivered(entry, made);
572
855
  }
573
856
  entry.running = false;
574
857
  }
@@ -599,34 +882,84 @@ export function createReadSide() {
599
882
  wake(entry);
600
883
  }
601
884
  }
885
+ function listed(key, row) {
886
+ const held = rows.get(key) ?? row;
887
+ if (held?.kind !== 'document')
888
+ return;
889
+ const paths = roomPaths.get(held.room) ?? new Set();
890
+ if (row?.kind === 'document' && row.type !== null)
891
+ paths.add(key);
892
+ else
893
+ paths.delete(key);
894
+ if (paths.size === 0)
895
+ roomPaths.delete(held.room);
896
+ else
897
+ roomPaths.set(held.room, paths);
898
+ }
602
899
  function committed(next) {
603
900
  if (closed || next.size === 0)
604
901
  return;
605
- sync.begin();
606
902
  for (const [key, row] of next) {
607
- if (row === null) {
608
- if (rows.has(key))
609
- sync.write({ type: 'delete', key });
610
- continue;
903
+ listed(key, row);
904
+ if (row === null)
905
+ rows.delete(key);
906
+ else
907
+ rows.set(key, row);
908
+ }
909
+ for (const view of views) {
910
+ const writes = new Map();
911
+ for (const [key, row] of next) {
912
+ if (row !== null && view.admits(row))
913
+ writes.set(key, row);
914
+ else if (view.rows.has(key) || view.waiting?.has(key))
915
+ writes.set(key, null);
611
916
  }
612
- sync.write({ type: rows.has(key) ? 'update' : 'insert', value: row });
917
+ written(view, writes);
613
918
  }
614
- sync.commit();
919
+ }
920
+ function moved({ plan }, before, after) {
921
+ const shape = plan.shape;
922
+ if (shape.kind === 'documents' && plan.limit === null && shape.orderBy.length > 0) {
923
+ return true;
924
+ }
925
+ const keys = shape.kind === 'documents' ? shape.orderBy.map(({ key }) => key) : shape.groupBy;
926
+ for (const key of keys) {
927
+ const path = key.of === 'field' ? ['values', key.field] : [key.of];
928
+ if (!deepEquals(valueAt(before, path), valueAt(after, path)))
929
+ return true;
930
+ }
931
+ return false;
932
+ }
933
+ function charged(view, was, row) {
934
+ const kept = was !== undefined && view.admits(was);
935
+ const admitted = row !== undefined && view.admits(row);
936
+ if (!kept && !admitted)
937
+ return 0;
938
+ const shifted = view.rows.size / HELD_PER_WEIGHT;
939
+ return shifted + (kept && admitted && !moved(view, was, row) ? 0 : shifted);
940
+ }
941
+ function charges(was, row) {
942
+ let spent = 0;
943
+ for (const view of views)
944
+ spent += charged(view, was, row);
945
+ return spent;
615
946
  }
616
947
  function published(set) {
617
948
  let unit = new Map();
618
949
  return inUnits(set.documents, (change) => {
619
950
  if (closed)
620
951
  return 0;
621
- const shifted = rows.size / HELD_PER_WEIGHT;
952
+ const { before, after } = change;
953
+ const kept = new Set(after.map((row) => row.key));
954
+ const held = new Map(before.map((row) => [row.key, row]));
622
955
  let spent = 0;
623
- for (const row of change.before) {
956
+ for (const row of before) {
624
957
  unit.set(row.key, null);
625
- spent += ROW_WEIGHT + shifted;
958
+ spent += ROW_WEIGHT + (kept.has(row.key) ? 0 : charges(row, undefined));
626
959
  }
627
- for (const row of change.after) {
960
+ for (const row of after) {
628
961
  unit.set(row.key, row);
629
- spent += weight(row) + shifted;
962
+ spent += weight(row) + charges(held.get(row.key), row);
630
963
  }
631
964
  return spent;
632
965
  }, () => {
@@ -657,8 +990,8 @@ export function createReadSide() {
657
990
  return {
658
991
  apply(set, next = () => { }) {
659
992
  const pending = publishing;
660
- if (pending !== null) {
661
- return publishedLater(pending.then(() => after(published(set), next)));
993
+ if (pending !== null || set instanceof Promise) {
994
+ return publishedLater(Promise.all([set, pending]).then(([held]) => after(published(held), () => next(held))));
662
995
  }
663
996
  writing = true;
664
997
  let applied;
@@ -669,48 +1002,83 @@ export function createReadSide() {
669
1002
  writing = false;
670
1003
  }
671
1004
  if (applied instanceof Promise)
672
- return publishedLater(applied.then(next));
1005
+ return publishedLater(applied.then(() => next(set)));
673
1006
  released();
674
- next();
1007
+ next(set);
675
1008
  },
676
1009
  recover,
677
1010
  settled,
678
1011
  async query(plan, at) {
679
- const { live: collection, count, dispose } = built(rows, plan);
1012
+ const view = viewOf(plan);
1013
+ const linked = new Map();
1014
+ let made = null;
1015
+ let following = null;
680
1016
  try {
681
- await collection.toArrayWhenReady();
682
- if (count !== null)
683
- await count.toArrayWhenReady();
1017
+ const filling = filled(view);
1018
+ made = built(view.rows, plan);
1019
+ const { live: collection, count, links } = made;
1020
+ if (links !== null) {
1021
+ following = linking(links, (changes) => {
1022
+ for (const change of changes)
1023
+ relink(linked, change);
1024
+ });
1025
+ }
1026
+ await filling;
1027
+ for (const one of [collection, count, links])
1028
+ if (one !== null)
1029
+ await readied(one);
684
1030
  await settled();
685
- const entries = [...collection.entries()];
1031
+ freeze(view);
1032
+ following?.unsubscribe();
1033
+ following = null;
686
1034
  const held = at();
687
- const found = await rowsOfPlan(plan, entries, count?.size, null, structuredClone);
688
- return { rows: found.rows, at: held };
1035
+ const rows = await stepped(rowsOfPlan(plan, collection.keys(), (key) => collection.get(key), count?.size, linked));
1036
+ return { rows, at: held };
689
1037
  }
690
1038
  finally {
691
- dispose();
1039
+ following?.unsubscribe();
1040
+ made?.dispose();
1041
+ dropped(view);
692
1042
  }
693
1043
  },
694
1044
  watch(plan, identity, cover, deliverTo) {
695
1045
  let entry = shared.get(identity);
696
1046
  if (entry === undefined) {
1047
+ const view = viewOf(plan);
1048
+ void filled(view);
697
1049
  const created = {
698
1050
  plan,
699
- built: built(rows, plan),
1051
+ view,
1052
+ built: built(view.rows, plan),
700
1053
  cover,
701
1054
  result: undefined,
702
1055
  rows: new Map(),
1056
+ generation: 0,
703
1057
  bodies: new Map(),
704
1058
  listeners: new Set(),
705
1059
  fresh: new Set(),
706
1060
  ready: new Set(),
707
1061
  subscription: null,
708
1062
  counting: null,
1063
+ linking: null,
1064
+ linked: new Map(),
1065
+ relinking: [],
1066
+ touched: new Set(),
709
1067
  whole: true,
710
1068
  dirty: new Set(),
711
1069
  stale: false,
712
1070
  running: false,
713
1071
  };
1072
+ if (created.built.links !== null) {
1073
+ created.linking = linking(created.built.links, (changes) => {
1074
+ for (const change of changes) {
1075
+ created.relinking.push(change);
1076
+ created.touched.add(targetOf(change));
1077
+ }
1078
+ if (created.subscription !== null)
1079
+ queueMicrotask(() => wake(created));
1080
+ });
1081
+ }
714
1082
  created.built.live.on('status:change', ({ status }) => {
715
1083
  if (status !== 'cleaned-up' || created.listeners.size > 0)
716
1084
  return;
@@ -722,7 +1090,12 @@ export function createReadSide() {
722
1090
  }
723
1091
  const observed = entry;
724
1092
  observed.listeners.add(deliverTo);
725
- const ready = (then) => observed.built.live.onFirstReady(() => observed.built.count === null ? then() : observed.built.count.onFirstReady(then));
1093
+ const ready = (then) => observed.built.live.onFirstReady(() => {
1094
+ const linked = () => observed.built.links === null ? then() : observed.built.links.onFirstReady(then);
1095
+ return observed.built.count === null
1096
+ ? linked()
1097
+ : observed.built.count.onFirstReady(linked);
1098
+ });
726
1099
  const settled = () => {
727
1100
  if (observed.built.count === null)
728
1101
  wake(observed);
@@ -760,22 +1133,30 @@ export function createReadSide() {
760
1133
  observed.subscription = null;
761
1134
  observed.counting?.unsubscribe();
762
1135
  observed.counting = null;
1136
+ observed.linking?.unsubscribe();
1137
+ observed.linking = null;
763
1138
  observed.result = undefined;
764
1139
  observed.rows.clear();
765
1140
  observed.bodies.clear();
766
1141
  observed.dirty.clear();
1142
+ observed.touched.clear();
767
1143
  observed.whole = true;
768
1144
  if (shared.get(identity) === observed)
769
1145
  shared.delete(identity);
770
1146
  observed.built.dispose();
1147
+ dropped(observed.view);
771
1148
  };
772
1149
  },
773
- room(room) {
774
- return rows.toArray.flatMap((row) => row.kind !== 'document' || row.type === null || row.room !== room ? [] : [row.path]);
1150
+ *room(room) {
1151
+ for (const key of roomPaths.get(room) ?? []) {
1152
+ const row = rows.get(key);
1153
+ if (row?.kind === 'document')
1154
+ yield row.path;
1155
+ }
775
1156
  },
776
1157
  document(room, path) {
777
1158
  const row = rows.get(documentKey(room, path));
778
- return row === undefined || row.type === null ? null : searched(row);
1159
+ return row?.kind !== 'document' || row.type === null ? null : searched(row);
779
1160
  },
780
1161
  close() {
781
1162
  closed = true;
@@ -785,42 +1166,14 @@ export function createReadSide() {
785
1166
  entry.ready.clear();
786
1167
  entry.subscription?.unsubscribe();
787
1168
  entry.counting?.unsubscribe();
1169
+ entry.linking?.unsubscribe();
788
1170
  entry.built.dispose();
789
1171
  }
790
1172
  shared.clear();
791
- void rows.cleanup();
1173
+ for (const view of [...views])
1174
+ dropped(view);
1175
+ rows.clear();
1176
+ roomPaths.clear();
792
1177
  },
793
1178
  };
794
1179
  }
795
- function changedFrom(previous, next) {
796
- const before = previous.result;
797
- const after = next.result;
798
- if ('pending' in before || 'pending' in after)
799
- return deepEquals(before, after) ? null : next;
800
- const earlier = 'groups' in before ? before.groups : before.documents;
801
- const held = new Map(previous.keys.map((key, index) => [key, earlier[index]]));
802
- const later = 'groups' in after ? after.groups : after.documents;
803
- const changed = [];
804
- next.keys.forEach((key, index) => {
805
- if (held.get(key) !== later[index])
806
- changed.push([key, later[index]]);
807
- });
808
- const present = new Set(next.keys);
809
- const removed = previous.keys.filter((key) => !present.has(key));
810
- const moved = reordered(previous.keys, next.keys, present);
811
- const covered = !deepEquals(before.coverage, after.coverage);
812
- const framing = reframed(before, after);
813
- if (changed.length === 0 && removed.length === 0 && !moved && !covered && framing === null) {
814
- return null;
815
- }
816
- return {
817
- changed,
818
- removed,
819
- order: moved ? next.keys : null,
820
- ...(covered ? { coverage: after.coverage } : {}),
821
- ...framing,
822
- };
823
- }
824
- function rowAt(rows, index) {
825
- return 'groups' in rows ? rows.groups[index] : rows.documents[index];
826
- }