@agentshouse/mdruntime 0.1.0-c69ca0e865e0b → 0.1.0-c6fd2cbbff187

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 (143) hide show
  1. package/AGENTS.md +275 -275
  2. package/dist/acquisition.d.ts +14 -0
  3. package/dist/acquisition.d.ts.map +1 -0
  4. package/dist/acquisition.js +24 -0
  5. package/dist/adapter.d.ts +50 -9
  6. package/dist/adapter.d.ts.map +1 -1
  7. package/dist/bootstrap.d.ts +3 -2
  8. package/dist/bootstrap.d.ts.map +1 -1
  9. package/dist/bootstrap.js +26 -4
  10. package/dist/change-set.d.ts +2 -0
  11. package/dist/change-set.d.ts.map +1 -1
  12. package/dist/change-set.js +36 -10
  13. package/dist/chat/acts.d.ts +3 -4
  14. package/dist/chat/acts.d.ts.map +1 -1
  15. package/dist/chat/acts.js +10 -10
  16. package/dist/chat/earlier.d.ts +10 -0
  17. package/dist/chat/earlier.d.ts.map +1 -0
  18. package/dist/chat/earlier.js +54 -0
  19. package/dist/chat/kit.d.ts +2 -1
  20. package/dist/chat/kit.d.ts.map +1 -1
  21. package/dist/chat/kit.js +3 -3
  22. package/dist/chat/list.d.ts +2 -1
  23. package/dist/chat/list.d.ts.map +1 -1
  24. package/dist/chat/list.js +42 -5
  25. package/dist/chat/message.d.ts.map +1 -1
  26. package/dist/chat/message.js +2 -2
  27. package/dist/chat/messages.d.ts +1 -0
  28. package/dist/chat/messages.d.ts.map +1 -1
  29. package/dist/chat/messages.js +4 -0
  30. package/dist/chat/provider.d.ts +1 -1
  31. package/dist/chat/provider.d.ts.map +1 -1
  32. package/dist/chat/provider.js +59 -69
  33. package/dist/chat/question.d.ts +1 -1
  34. package/dist/chat/question.d.ts.map +1 -1
  35. package/dist/chat/question.js +7 -18
  36. package/dist/chat/ready.d.ts +1 -0
  37. package/dist/chat/ready.d.ts.map +1 -1
  38. package/dist/chat/ready.js +2 -2
  39. package/dist/chat/style.js +1 -1
  40. package/dist/client.d.ts +14 -8
  41. package/dist/client.d.ts.map +1 -1
  42. package/dist/client.js +17 -6
  43. package/dist/collection.d.ts +17 -3
  44. package/dist/collection.d.ts.map +1 -1
  45. package/dist/collection.js +230 -19
  46. package/dist/connection.d.ts +27 -8
  47. package/dist/connection.d.ts.map +1 -1
  48. package/dist/connection.js +1567 -362
  49. package/dist/conversation.d.ts +19 -7
  50. package/dist/conversation.d.ts.map +1 -1
  51. package/dist/conversation.js +438 -152
  52. package/dist/durability.d.ts +7 -1
  53. package/dist/durability.d.ts.map +1 -1
  54. package/dist/entry.d.ts +2 -2
  55. package/dist/entry.d.ts.map +1 -1
  56. package/dist/entry.js +3 -3
  57. package/dist/fence.d.ts +6 -0
  58. package/dist/fence.d.ts.map +1 -0
  59. package/dist/fence.js +10 -0
  60. package/dist/host.d.ts +17 -2
  61. package/dist/host.d.ts.map +1 -1
  62. package/dist/host.js +90 -9
  63. package/dist/identity.d.ts +2 -0
  64. package/dist/identity.d.ts.map +1 -1
  65. package/dist/identity.js +6 -0
  66. package/dist/index.d.ts +9 -6
  67. package/dist/index.d.ts.map +1 -1
  68. package/dist/index.js +2 -0
  69. package/dist/memory.d.ts +9 -2
  70. package/dist/memory.d.ts.map +1 -1
  71. package/dist/memory.js +36 -7
  72. package/dist/model.d.ts +4 -2
  73. package/dist/model.d.ts.map +1 -1
  74. package/dist/model.js +10 -1
  75. package/dist/observation.d.ts +7 -2
  76. package/dist/observation.d.ts.map +1 -1
  77. package/dist/observation.js +29 -12
  78. package/dist/outcome.d.ts +12 -2
  79. package/dist/outcome.d.ts.map +1 -1
  80. package/dist/outcome.js +4 -0
  81. package/dist/overlay.d.ts +2 -0
  82. package/dist/overlay.d.ts.map +1 -1
  83. package/dist/overlay.js +58 -55
  84. package/dist/query.d.ts +56 -4
  85. package/dist/query.d.ts.map +1 -1
  86. package/dist/query.js +36 -7
  87. package/dist/reach.d.ts +6 -3
  88. package/dist/reach.d.ts.map +1 -1
  89. package/dist/reach.js +20 -5
  90. package/dist/react.d.ts +7 -4
  91. package/dist/react.d.ts.map +1 -1
  92. package/dist/react.js +6 -4
  93. package/dist/read-side.d.ts +13 -6
  94. package/dist/read-side.d.ts.map +1 -1
  95. package/dist/read-side.js +419 -92
  96. package/dist/readiness.d.ts +53 -0
  97. package/dist/readiness.d.ts.map +1 -0
  98. package/dist/readiness.js +33 -0
  99. package/dist/reads.d.ts +11 -0
  100. package/dist/reads.d.ts.map +1 -0
  101. package/dist/reads.js +98 -0
  102. package/dist/recovery.d.ts +25 -0
  103. package/dist/recovery.d.ts.map +1 -0
  104. package/dist/recovery.js +197 -0
  105. package/dist/refresh.d.ts +4 -4
  106. package/dist/refresh.d.ts.map +1 -1
  107. package/dist/refresh.js +17 -10
  108. package/dist/replica.d.ts +6 -2
  109. package/dist/replica.d.ts.map +1 -1
  110. package/dist/replica.js +76 -23
  111. package/dist/room-draft.d.ts +13 -1
  112. package/dist/room-draft.d.ts.map +1 -1
  113. package/dist/room-draft.js +2 -2
  114. package/dist/rows.d.ts +6 -13
  115. package/dist/rows.d.ts.map +1 -1
  116. package/dist/rows.js +10 -13
  117. package/dist/save.d.ts +6 -0
  118. package/dist/save.d.ts.map +1 -1
  119. package/dist/save.js +53 -14
  120. package/dist/search.d.ts +49 -6
  121. package/dist/search.d.ts.map +1 -1
  122. package/dist/search.js +192 -47
  123. package/dist/seed.d.ts +16 -0
  124. package/dist/seed.d.ts.map +1 -0
  125. package/dist/seed.js +75 -0
  126. package/dist/shared.d.ts +11 -0
  127. package/dist/shared.d.ts.map +1 -0
  128. package/dist/shared.js +368 -0
  129. package/dist/status.d.ts +4 -0
  130. package/dist/status.d.ts.map +1 -1
  131. package/dist/store.d.ts +56 -11
  132. package/dist/store.d.ts.map +1 -1
  133. package/dist/store.js +311 -84
  134. package/dist/turn.d.ts +7 -0
  135. package/dist/turn.d.ts.map +1 -0
  136. package/dist/turn.js +45 -0
  137. package/dist/windows.d.ts +19 -0
  138. package/dist/windows.d.ts.map +1 -0
  139. package/dist/windows.js +83 -0
  140. package/dist/wire.d.ts +144 -32
  141. package/dist/wire.d.ts.map +1 -1
  142. package/dist/wire.js +50 -31
  143. package/package.json +2 -2
package/dist/read-side.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { and, BasicIndex, configureIndexDevMode, count, createCollection, createLiveQueryCollection, deepEquals, eq, inArray, or, toArray, } from '@tanstack/db';
2
- import { ARRAY_INDEX, pointerSegments } from '@agentshouse/mdmodel';
3
- import { rowsOf } from './observation.js';
2
+ import { ARRAY_INDEX, parse, pointerSegments } from '@agentshouse/mdmodel';
4
3
  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
6
  function every(expressions) {
6
7
  const [first, second, ...rest] = expressions;
7
8
  return second === undefined ? first : and(first, second, ...rest);
@@ -59,10 +60,12 @@ function reached(reach, room, document) {
59
60
  ? []
60
61
  : [or(inArray(room, [...reach.rooms]), inArray(document, [...reach.documents]))];
61
62
  }
62
- function scoped(scope, row, rooms, reach) {
63
+ function scoped(scope, row, rooms, reach, paths = null, folder = null) {
63
64
  return every([
64
65
  eq(row.kind, 'document'),
65
66
  ...(rooms === null ? [] : [inArray(row.room, [...rooms])]),
67
+ ...(paths === null ? [] : [inArray(row.path, [...paths])]),
68
+ ...(folder === null ? [] : [eq(row.folder, folder)]),
66
69
  ...reached(reach, row.room, row.key),
67
70
  ...(scope.types === null ? [] : [inArray(row.type, [...scope.types])]),
68
71
  ...(scope.pathPrefix === null ? [] : [inArray(scope.pathPrefix, row.directories)]),
@@ -90,7 +93,7 @@ function related(q, rows, scope, field, kept, reach) {
90
93
  }
91
94
  function filtered(q, rows, plan) {
92
95
  const from = q.from({ d: rows });
93
- let query = narrowed(from.where(({ d }) => scoped(plan.root, d, plan.rooms, plan.reach)), plan.root.where, 'd');
96
+ let query = narrowed(from.where(({ d }) => scoped(plan.root, d, plan.rooms, plan.reach, plan.paths, plan.folder)), plan.root.where, 'd');
94
97
  if (plan.hop !== null) {
95
98
  const hop = related(q, rows, plan.hop, plan.hop.field, 'source', plan.reach);
96
99
  query = query.innerJoin({ h: hop }, ({ d, h }) => eq(d.key, h.key));
@@ -101,34 +104,59 @@ function filtered(q, rows, plan) {
101
104
  }
102
105
  return query;
103
106
  }
104
- function named(prefix, names, source) {
105
- return Object.fromEntries(names.map((name, index) => [`${prefix}${index}`, source[name]]));
107
+ function bodied(shape) {
108
+ return shape.sections !== null || shape.preamble || shape.markdown;
109
+ }
110
+ function orderingOf(options) {
111
+ const collated = options.stringSort === 'locale';
112
+ return {
113
+ direction: options.direction,
114
+ nulls: options.nulls ?? 'first',
115
+ stringSort: options.stringSort ?? 'locale',
116
+ locale: collated ? options.locale : undefined,
117
+ localeOptions: collated
118
+ ? options.localeOptions
119
+ : undefined,
120
+ };
121
+ }
122
+ function storedValue(key, row) {
123
+ return key.of === 'field' ? row.values[key.field] : row[key.of];
124
+ }
125
+ function documentOrder(shape) {
126
+ return shape.orderBy.map(({ options }) => orderingOf(options));
127
+ }
128
+ function documentCursor(shape, row) {
129
+ return cursorOf([
130
+ ...shape.orderBy.map((_, index) => row[`order${index}`]),
131
+ documentKey(row.room, row.path),
132
+ ]);
106
133
  }
107
134
  function documents(q, rows, plan, shape) {
108
135
  let query = filtered(q, rows, plan);
109
- if (shape.sections !== null || shape.preamble || shape.markdown) {
110
- query = query.leftJoin({ b: rows }, ({ d, b }) => eq(d.body, b.key));
136
+ const cursor = plan.after?.values ?? null;
137
+ if (cursor !== null) {
138
+ const orderings = documentOrder(shape);
139
+ query = query.fn.where(({ d }) => beyond([...shape.orderBy.map(({ key }) => storedValue(key, d)), d.key], cursor, orderings));
111
140
  }
112
141
  for (const { key, options } of shape.orderBy) {
113
142
  query = query.orderBy(({ d }) => keyed(key, d), options);
114
143
  }
115
- if (shape.orderBy.length === 0 && plan.limit !== null) {
144
+ if (shape.orderBy.length > 0 || plan.limit !== null || plan.after !== null) {
116
145
  query = query.orderBy(({ d }) => d.key, { stringSort: 'lexical' });
117
146
  }
118
147
  if (plan.limit !== null)
119
- query = query.limit(plan.limit);
120
- return query.select(({ d, b }) => ({
148
+ query = query.limit(plan.limit + 1);
149
+ return query.select(({ d }) => ({
121
150
  room: d.room,
122
151
  path: d.path,
123
152
  type: d.type,
124
153
  binary: d.binary,
154
+ ...Object.fromEntries(shape.orderBy.map(({ key }, index) => [`order${index}`, keyed(key, d)])),
125
155
  ...(shape.fields === null ? {} : { fields: d.fields }),
126
- ...(shape.sections === 'all' ? { sections: b.sections } : {}),
127
- ...(Array.isArray(shape.sections) ? named('section', shape.sections, b.sections) : {}),
128
- ...(shape.preamble ? { preamble: b.preamble } : {}),
129
- ...(shape.markdown ? { markdown: b.markdown } : {}),
156
+ ...(bodied(shape) ? { text: d.text } : {}),
130
157
  ...(shape.backlinks ? { backlinks: toArray(backlinked(q, rows, d, plan.reach)) } : {}),
131
158
  ...(shape.change ? { change: d.change } : {}),
159
+ ...(shape.fileId ? { fileId: d.fileId } : {}),
132
160
  }));
133
161
  }
134
162
  function backlinked(q, rows, document, reach) {
@@ -139,15 +167,13 @@ function backlinked(q, rows, document, reach) {
139
167
  .orderBy(({ r }) => r.field, { stringSort: 'lexical' })
140
168
  .select(({ r }) => ({ path: r.path, type: r.type, field: r.field }));
141
169
  }
142
- function groups(q, stage, shape, limit) {
170
+ function groups(q, stage, shape) {
143
171
  const keys = shape.groupBy.map((_, index) => `key${index}`);
144
172
  const from = q.from({ m: stage });
145
173
  let query = from.groupBy(({ m }) => keys.map((key) => m[key]));
146
174
  for (const { by, direction } of shape.orderBy) {
147
175
  query = query.orderBy(({ m }) => (by === 'count' ? count(m.key) : m[keys[by]]), direction);
148
176
  }
149
- if (limit !== null && shape.orderBy.length > 0)
150
- query = query.limit(limit);
151
177
  return query.select(({ m }) => ({
152
178
  ...Object.fromEntries(keys.map((key) => [key, m[key]])),
153
179
  count: count(m.key),
@@ -157,28 +183,49 @@ function built(rows, plan) {
157
183
  const shape = plan.shape;
158
184
  if (shape.kind === 'documents') {
159
185
  const collection = live((q) => documents(q, rows, plan, shape));
160
- return { live: collection, dispose: () => void collection.cleanup() };
186
+ const count = plan.total
187
+ ? live((q) => filtered(q, rows, plan).select(({ d }) => ({ key: d.key })))
188
+ : null;
189
+ return {
190
+ live: collection,
191
+ count,
192
+ dispose: () => {
193
+ void collection.cleanup();
194
+ if (count !== null)
195
+ void count.cleanup();
196
+ },
197
+ };
161
198
  }
162
199
  const stage = live((q) => filtered(q, rows, plan).select(({ d }) => ({
163
200
  key: d.key,
164
201
  ...Object.fromEntries(shape.groupBy.map((key, index) => [`key${index}`, keyed(key, d)])),
165
202
  })));
166
- const collection = live((q) => groups(q, stage, shape, plan.limit));
203
+ const collection = live((q) => groups(q, stage, shape));
167
204
  return {
168
205
  live: collection,
206
+ count: null,
169
207
  dispose: () => {
170
208
  void collection.cleanup();
171
209
  void stage.cleanup();
172
210
  },
173
211
  };
174
212
  }
175
- function present(entries) {
176
- return Object.fromEntries(entries.filter(([, value]) => value !== undefined));
177
- }
178
- function part(row, prefix, names, whole) {
213
+ function sectionsOf(sections, names) {
179
214
  if (names === 'all')
180
- return whole;
181
- return present(names.map((name, index) => [name, row[`${prefix}${index}`]]));
215
+ return sections;
216
+ return Object.fromEntries(names.flatMap((name) => (Object.hasOwn(sections, name) ? [[name, sections[name]]] : [])));
217
+ }
218
+ function bodyOf(row, key, bodies) {
219
+ const text = row.text;
220
+ if (text === null)
221
+ return null;
222
+ const held = bodies?.get(key);
223
+ if (held !== undefined && held.text === text)
224
+ return held.document;
225
+ const read = parse(text, row.path);
226
+ const document = read.ok ? read.document : null;
227
+ bodies?.set(key, { text, document });
228
+ return document;
182
229
  }
183
230
  function masked(pointers) {
184
231
  const root = { whole: false, members: new Map() };
@@ -229,15 +276,17 @@ function cut(value, mask) {
229
276
  function selected(fields, pointers) {
230
277
  return (cut(fields, masked(pointers)) ?? {});
231
278
  }
232
- function documentOf(row, shape) {
279
+ function documentOf(key, row, shape, bodies) {
233
280
  if (row.type === null) {
234
281
  const binary = row.binary;
235
282
  return {
236
283
  room: row.room,
237
284
  path: row.path,
238
285
  ...(binary === null ? {} : { binaryFile: binary }),
286
+ ...(shape.fileId ? { fileId: row.fileId } : {}),
239
287
  };
240
288
  }
289
+ const body = bodied(shape) ? bodyOf(row, key, bodies) : null;
241
290
  return {
242
291
  room: row.room,
243
292
  path: row.path,
@@ -249,45 +298,94 @@ function documentOf(row, shape) {
249
298
  ? row.fields
250
299
  : selected(row.fields, shape.fields),
251
300
  }),
252
- ...(shape.sections === null
301
+ ...(shape.sections === null || body === null
253
302
  ? {}
254
- : {
255
- sections: part(row, 'section', shape.sections, row.sections),
256
- }),
257
- ...(shape.preamble ? { preamble: row.preamble } : {}),
258
- ...(shape.markdown ? { markdown: row.markdown } : {}),
303
+ : { sections: sectionsOf(body.sections, shape.sections) }),
304
+ ...(shape.preamble && body !== null ? { preamble: body.preamble } : {}),
305
+ ...(shape.markdown && body !== null ? { markdown: body.content } : {}),
259
306
  ...(shape.backlinks
260
307
  ? {
261
- backlinks: row.backlinks.map(({ path, type, field }) => ({ path, type, field })),
308
+ backlinks: row.backlinks
309
+ .slice(0, MAXIMUM_WINDOW)
310
+ .map(({ path, type, field }) => ({ path, type, field })),
262
311
  }
263
312
  : {}),
264
313
  ...(shape.change ? { change: row.change } : {}),
314
+ ...(shape.fileId ? { fileId: row.fileId } : {}),
265
315
  };
266
316
  }
267
- function keyedOf(plan, entries) {
317
+ function weight(row) {
318
+ return ROW_WEIGHT + (typeof row?.text === 'string' ? row.text.length : 0);
319
+ }
320
+ function groupValues(shape, [key, row]) {
321
+ return [...shape.orderBy.map(({ by }) => (by === 'count' ? row.count : row[`key${by}`])), key];
322
+ }
323
+ function groupOrder(shape) {
324
+ return shape.orderBy.map(({ direction }) => ({
325
+ direction,
326
+ nulls: 'first',
327
+ stringSort: 'locale',
328
+ locale: undefined,
329
+ localeOptions: undefined,
330
+ }));
331
+ }
332
+ function windowOf(plan, entries, total) {
268
333
  const shape = plan.shape;
269
334
  if (shape.kind === 'documents') {
270
335
  return {
271
- result: structuredClone({ documents: entries.map(([, row]) => documentOf(row, shape)) }),
272
- keys: entries.map(([key]) => key),
336
+ ...windowed(entries, plan.limit, plan.after?.text, ([, row]) => documentCursor(shape, row)),
337
+ total,
273
338
  };
274
339
  }
275
- const bounded = shape.orderBy.length === 0 && plan.limit !== null ? entries.slice(0, plan.limit) : entries;
340
+ const cursor = plan.after?.values ?? null;
341
+ const orderings = groupOrder(shape);
342
+ const following = cursor === null
343
+ ? entries
344
+ : entries.filter((entry) => beyond(groupValues(shape, entry), cursor, orderings));
345
+ return {
346
+ ...windowed(following, plan.limit, plan.after?.text, (entry) => cursorOf(groupValues(shape, entry))),
347
+ total: plan.total ? entries.length : undefined,
348
+ };
349
+ }
350
+ function framed(rows, { next, total }) {
276
351
  return {
277
- result: structuredClone({
278
- groups: bounded.map(([, row]) => ({
279
- key: shape.groupBy.map((_, index) => row[`key${index}`]),
280
- count: row.count,
281
- })),
282
- }),
283
- keys: bounded.map(([key]) => key),
352
+ ...rows,
353
+ ...(next === undefined ? {} : { next }),
354
+ ...(total === undefined ? {} : { total }),
284
355
  };
285
356
  }
357
+ function rowsOfPlan(plan, entries, total, bodies, each = (row) => row) {
358
+ const shape = plan.shape;
359
+ const { kept, ...window } = windowOf(plan, entries, total);
360
+ const keys = kept.map(([key]) => key);
361
+ if (shape.kind === 'documents') {
362
+ const documents = [];
363
+ return after(inUnits(kept, ([key, row]) => {
364
+ documents.push(each(documentOf(key, row, shape, bodies)));
365
+ return weight(row);
366
+ }), () => ({ rows: framed({ documents }, window), keys, ...window }));
367
+ }
368
+ const groups = [];
369
+ return after(inUnits(kept, ([, row]) => {
370
+ groups.push(each({
371
+ key: shape.groupBy.map((_, index) => row[`key${index}`]),
372
+ count: row.count,
373
+ }));
374
+ return ROW_WEIGHT;
375
+ }), () => ({ rows: framed({ groups }, window), keys, ...window }));
376
+ }
286
377
  export function reconciled(previous, next) {
287
- const before = rowsOf(previous.result);
288
- const held = new Map(previous.keys.map((key, index) => [key, before[index]]));
378
+ const before = previous.result;
379
+ const after = next.result;
380
+ if ('pending' in before || 'pending' in after) {
381
+ if (deepEquals(before, after))
382
+ return null;
383
+ return { keyed: next, message: next };
384
+ }
385
+ const earlier = 'groups' in before ? before.groups : before.documents;
386
+ const held = new Map(previous.keys.map((key, index) => [key, earlier[index]]));
289
387
  const changed = [];
290
- const rows = rowsOf(next.result).map((row, index) => {
388
+ const rows = ('groups' in after ? after.groups : after.documents).map((row, index) => {
291
389
  const key = next.keys[index];
292
390
  const kept = held.get(key);
293
391
  if (kept !== undefined && deepEquals(kept, row))
@@ -296,22 +394,48 @@ export function reconciled(previous, next) {
296
394
  return row;
297
395
  });
298
396
  const present = new Set(next.keys);
299
- const survivors = previous.keys.filter((key) => present.has(key));
300
397
  const removed = previous.keys.filter((key) => !present.has(key));
301
- const moved = survivors.length !== next.keys.length ||
302
- survivors.some((key, index) => key !== next.keys[index]);
303
- if (changed.length === 0 && removed.length === 0 && !moved)
398
+ const moved = reordered(previous.keys, next.keys, present);
399
+ const covered = !deepEquals(before.coverage, after.coverage);
400
+ const framing = reframed(before, after);
401
+ if (changed.length === 0 && removed.length === 0 && !moved && !covered && framing === null) {
304
402
  return null;
403
+ }
404
+ const window = { next: after.next, total: after.total };
305
405
  return {
306
406
  keyed: {
307
- result: 'groups' in next.result
308
- ? { groups: rows }
309
- : { documents: rows },
407
+ result: 'groups' in after
408
+ ? framed({ groups: rows, coverage: after.coverage }, window)
409
+ : framed({ documents: rows, coverage: after.coverage }, window),
310
410
  keys: next.keys,
311
411
  },
312
- change: { changed, removed, order: moved ? next.keys : null },
412
+ message: {
413
+ changed,
414
+ removed,
415
+ order: moved ? next.keys : null,
416
+ ...(covered ? { coverage: after.coverage } : {}),
417
+ ...framing,
418
+ },
313
419
  };
314
420
  }
421
+ function reframed(before, after) {
422
+ const next = before.next === after.next ? {} : { next: after.next ?? null };
423
+ const total = before.total === after.total || after.total === undefined ? {} : { total: after.total };
424
+ return Object.keys(next).length === 0 && Object.keys(total).length === 0
425
+ ? null
426
+ : { ...next, ...total };
427
+ }
428
+ function reordered(previous, next, present) {
429
+ let at = 0;
430
+ for (const key of previous) {
431
+ if (!present.has(key))
432
+ continue;
433
+ if (next[at] !== key)
434
+ return true;
435
+ at += 1;
436
+ }
437
+ return at !== next.length;
438
+ }
315
439
  export function createReadSide() {
316
440
  configureIndexDevMode({ enabled: false });
317
441
  let sync;
@@ -331,23 +455,144 @@ export function createReadSide() {
331
455
  });
332
456
  const shared = new Map();
333
457
  function searched(row) {
334
- const body = rows.get(row.body);
458
+ const read = row.text === null ? null : parse(row.text, row.path);
459
+ if (read === null || !read.ok)
460
+ return null;
335
461
  return {
336
462
  room: row.room,
337
463
  path: row.path,
338
464
  type: row.type,
339
465
  fields: row.fields,
340
- preamble: body.preamble,
341
- sections: body.sections,
466
+ preamble: read.document.preamble,
467
+ sections: read.document.sections,
342
468
  };
343
469
  }
344
- function publish(entry) {
345
- const next = reconciled(entry.result, keyedOf(entry.plan, [...entry.built.live.entries()]));
346
- if (next === null)
470
+ function kept(entry, key, row) {
471
+ const held = entry.rows.get(key);
472
+ if (held !== undefined && deepEquals(held, row))
347
473
  return;
348
- entry.result = next.keyed;
349
- for (const listener of entry.listeners)
350
- listener(next.change);
474
+ entry.rows.set(key, row);
475
+ }
476
+ function counted(entry) {
477
+ return entry.built.count === null ? undefined : entry.built.count.size;
478
+ }
479
+ function rebuilt(entry) {
480
+ return after(rowsOfPlan(entry.plan, [...entry.built.live.entries()], counted(entry), entry.bodies), ({ rows: found, keys, next, total }) => {
481
+ const present = new Set(keys);
482
+ for (const key of [...entry.rows.keys()])
483
+ if (!present.has(key))
484
+ entry.rows.delete(key);
485
+ for (const key of [...entry.bodies.keys()]) {
486
+ if (!present.has(key))
487
+ entry.bodies.delete(key);
488
+ }
489
+ keys.forEach((key, index) => kept(entry, key, rowAt(found, index)));
490
+ return { keys, next, total };
491
+ });
492
+ }
493
+ function refreshed(entry, dirty) {
494
+ const shape = entry.plan.shape;
495
+ const { kept: inWindow, next, total, } = windowOf(entry.plan, [...entry.built.live.entries()], counted(entry));
496
+ const keys = inWindow.map(([key]) => key);
497
+ const present = new Set(keys);
498
+ for (const key of [...entry.rows.keys()]) {
499
+ if (present.has(key))
500
+ continue;
501
+ entry.rows.delete(key);
502
+ entry.bodies.delete(key);
503
+ }
504
+ const values = inWindow.filter(([key]) => dirty.has(key) || !entry.rows.has(key));
505
+ const done = inUnits(values, ([key, value]) => {
506
+ kept(entry, key, documentOf(key, value, shape, entry.bodies));
507
+ return weight(value);
508
+ });
509
+ return after(done, () => ({ keys, next, total }));
510
+ }
511
+ function current(entry, found, coverage) {
512
+ const rows = found.keys.map((key) => entry.rows.get(key));
513
+ return {
514
+ result: entry.plan.shape.kind === 'groups'
515
+ ? framed({ groups: rows, coverage }, found)
516
+ : framed({ documents: rows, coverage }, found),
517
+ keys: found.keys,
518
+ };
519
+ }
520
+ function delivered(entry, next) {
521
+ const previous = entry.result;
522
+ entry.result = next;
523
+ const message = previous === undefined ? null : changedFrom(previous, next);
524
+ if (message !== null)
525
+ for (const listener of entry.ready)
526
+ listener(message);
527
+ for (const listener of entry.fresh) {
528
+ entry.ready.add(listener);
529
+ listener(next);
530
+ }
531
+ entry.fresh.clear();
532
+ }
533
+ function run(entry) {
534
+ while (entry.listeners.size > 0 &&
535
+ (entry.whole || entry.dirty.size > 0 || entry.stale || entry.fresh.size > 0)) {
536
+ entry.stale = false;
537
+ const cover = entry.cover();
538
+ if ('pending' in cover) {
539
+ entry.whole = true;
540
+ entry.dirty.clear();
541
+ entry.rows.clear();
542
+ entry.bodies.clear();
543
+ delivered(entry, { result: cover, keys: [] });
544
+ break;
545
+ }
546
+ const whole = entry.whole ||
547
+ entry.plan.shape.kind === 'groups' ||
548
+ entry.result === undefined ||
549
+ 'pending' in entry.result.result;
550
+ const dirty = entry.dirty;
551
+ entry.dirty = new Set();
552
+ entry.whole = false;
553
+ const found = whole ? rebuilt(entry) : refreshed(entry, dirty);
554
+ if (found instanceof Promise) {
555
+ void found
556
+ .then((window) => {
557
+ if (entry.listeners.size > 0)
558
+ delivered(entry, current(entry, window, cover.coverage));
559
+ run(entry);
560
+ })
561
+ .catch(() => {
562
+ entry.running = false;
563
+ });
564
+ return;
565
+ }
566
+ delivered(entry, current(entry, found, cover.coverage));
567
+ }
568
+ entry.running = false;
569
+ }
570
+ function wake(entry) {
571
+ if (entry.subscription === null || entry.running)
572
+ return;
573
+ entry.running = true;
574
+ try {
575
+ run(entry);
576
+ }
577
+ catch (error) {
578
+ entry.running = false;
579
+ throw error;
580
+ }
581
+ }
582
+ function recover() {
583
+ for (const entry of shared.values()) {
584
+ if (entry.result === undefined || entry.subscription === null)
585
+ continue;
586
+ const cover = entry.cover();
587
+ const before = entry.result.result;
588
+ const held = 'pending' in cover
589
+ ? deepEquals(before, cover)
590
+ : 'coverage' in before && deepEquals(before.coverage, cover.coverage);
591
+ if (held)
592
+ continue;
593
+ entry.stale = true;
594
+ wake(entry);
595
+ }
351
596
  }
352
597
  return {
353
598
  apply(set) {
@@ -358,70 +603,117 @@ export function createReadSide() {
358
603
  for (const row of change.after)
359
604
  next.set(row.key, row);
360
605
  }
361
- if (next.size === 0)
362
- return;
363
- sync.begin();
364
- for (const [key, row] of next) {
365
- if (row === null) {
366
- if (rows.has(key))
367
- sync.write({ type: 'delete', key });
368
- continue;
606
+ if (next.size > 0) {
607
+ sync.begin();
608
+ for (const [key, row] of next) {
609
+ if (row === null) {
610
+ if (rows.has(key))
611
+ sync.write({ type: 'delete', key });
612
+ continue;
613
+ }
614
+ sync.write({ type: rows.has(key) ? 'update' : 'insert', value: row });
369
615
  }
370
- sync.write({ type: rows.has(key) ? 'update' : 'insert', value: row });
616
+ sync.commit();
371
617
  }
372
- sync.commit();
618
+ recover();
373
619
  },
374
- async query(plan) {
375
- const { live: collection, dispose } = built(rows, plan);
620
+ recover,
621
+ async query(plan, at) {
622
+ const { live: collection, count, dispose } = built(rows, plan);
376
623
  try {
377
624
  await collection.toArrayWhenReady();
378
- return keyedOf(plan, [...collection.entries()]).result;
625
+ if (count !== null)
626
+ await count.toArrayWhenReady();
627
+ const entries = [...collection.entries()];
628
+ const held = at();
629
+ const found = await rowsOfPlan(plan, entries, count?.size, null, structuredClone);
630
+ return { rows: found.rows, at: held };
379
631
  }
380
632
  finally {
381
633
  dispose();
382
634
  }
383
635
  },
384
- watch(plan, identity, deliver) {
636
+ watch(plan, identity, cover, deliverTo) {
385
637
  let entry = shared.get(identity);
386
638
  if (entry === undefined) {
387
639
  const created = {
388
640
  plan,
389
641
  built: built(rows, plan),
642
+ cover,
390
643
  result: undefined,
644
+ rows: new Map(),
645
+ bodies: new Map(),
391
646
  listeners: new Set(),
647
+ fresh: new Set(),
648
+ ready: new Set(),
392
649
  subscription: null,
650
+ counting: null,
651
+ whole: true,
652
+ dirty: new Set(),
653
+ stale: false,
654
+ running: false,
393
655
  };
394
656
  created.built.live.on('status:change', ({ status }) => {
395
- if (status === 'cleaned-up' && created.listeners.size === 0)
657
+ if (status !== 'cleaned-up' || created.listeners.size > 0)
658
+ return;
659
+ if (shared.get(identity) === created)
396
660
  shared.delete(identity);
397
661
  });
398
662
  shared.set(identity, created);
399
663
  entry = created;
400
664
  }
401
665
  const observed = entry;
402
- observed.listeners.add(deliver);
403
- observed.built.live.onFirstReady(() => {
404
- if (!observed.listeners.has(deliver))
666
+ observed.listeners.add(deliverTo);
667
+ const ready = (then) => observed.built.live.onFirstReady(() => observed.built.count === null ? then() : observed.built.count.onFirstReady(then));
668
+ const settled = () => {
669
+ if (observed.built.count === null)
670
+ wake(observed);
671
+ else
672
+ queueMicrotask(() => wake(observed));
673
+ };
674
+ ready(() => {
675
+ if (!observed.listeners.has(deliverTo))
405
676
  return;
677
+ observed.fresh.add(deliverTo);
406
678
  if (observed.subscription === null) {
407
- observed.result = keyedOf(observed.plan, [...observed.built.live.entries()]);
408
- observed.subscription = observed.built.live.subscribeChanges(() => {
409
- publish(observed);
679
+ observed.whole = true;
680
+ observed.subscription = observed.built.live.subscribeChanges((changes) => {
681
+ if (observed.plan.shape.kind === 'groups')
682
+ observed.whole = true;
683
+ else
684
+ for (const { key } of changes)
685
+ observed.dirty.add(key);
686
+ settled();
410
687
  }, { includeInitialState: false });
688
+ observed.counting =
689
+ observed.built.count?.subscribeChanges(() => {
690
+ observed.stale = true;
691
+ settled();
692
+ }, { includeInitialState: false }) ?? null;
411
693
  }
412
- deliver(observed.result);
694
+ wake(observed);
413
695
  });
414
696
  return () => {
415
- if (!observed.listeners.delete(deliver) || observed.listeners.size > 0)
697
+ observed.fresh.delete(deliverTo);
698
+ observed.ready.delete(deliverTo);
699
+ if (!observed.listeners.delete(deliverTo) || observed.listeners.size > 0)
416
700
  return;
417
701
  observed.subscription?.unsubscribe();
418
702
  observed.subscription = null;
703
+ observed.counting?.unsubscribe();
704
+ observed.counting = null;
705
+ observed.result = undefined;
706
+ observed.rows.clear();
707
+ observed.bodies.clear();
708
+ observed.dirty.clear();
709
+ observed.whole = true;
710
+ if (shared.get(identity) === observed)
711
+ shared.delete(identity);
712
+ observed.built.dispose();
419
713
  };
420
714
  },
421
- documents() {
422
- return rows.toArray
423
- .filter((row) => row.kind === 'document' && row.type !== null)
424
- .map(searched);
715
+ room(room) {
716
+ return rows.toArray.flatMap((row) => row.kind !== 'document' || row.type === null || row.room !== room ? [] : [row.path]);
425
717
  },
426
718
  document(room, path) {
427
719
  const row = rows.get(documentKey(room, path));
@@ -430,7 +722,10 @@ export function createReadSide() {
430
722
  close() {
431
723
  for (const entry of shared.values()) {
432
724
  entry.listeners.clear();
725
+ entry.fresh.clear();
726
+ entry.ready.clear();
433
727
  entry.subscription?.unsubscribe();
728
+ entry.counting?.unsubscribe();
434
729
  entry.built.dispose();
435
730
  }
436
731
  shared.clear();
@@ -438,3 +733,35 @@ export function createReadSide() {
438
733
  },
439
734
  };
440
735
  }
736
+ function changedFrom(previous, next) {
737
+ const before = previous.result;
738
+ const after = next.result;
739
+ if ('pending' in before || 'pending' in after)
740
+ return deepEquals(before, after) ? null : next;
741
+ const earlier = 'groups' in before ? before.groups : before.documents;
742
+ const held = new Map(previous.keys.map((key, index) => [key, earlier[index]]));
743
+ const later = 'groups' in after ? after.groups : after.documents;
744
+ const changed = [];
745
+ next.keys.forEach((key, index) => {
746
+ if (held.get(key) !== later[index])
747
+ changed.push([key, later[index]]);
748
+ });
749
+ const present = new Set(next.keys);
750
+ const removed = previous.keys.filter((key) => !present.has(key));
751
+ const moved = reordered(previous.keys, next.keys, present);
752
+ const covered = !deepEquals(before.coverage, after.coverage);
753
+ const framing = reframed(before, after);
754
+ if (changed.length === 0 && removed.length === 0 && !moved && !covered && framing === null) {
755
+ return null;
756
+ }
757
+ return {
758
+ changed,
759
+ removed,
760
+ order: moved ? next.keys : null,
761
+ ...(covered ? { coverage: after.coverage } : {}),
762
+ ...framing,
763
+ };
764
+ }
765
+ function rowAt(rows, index) {
766
+ return 'groups' in rows ? rows.groups[index] : rows.documents[index];
767
+ }