@agentshouse/mdruntime 0.1.0-cde2b99d2fcbc → 0.1.0-ce131cb435f8b

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 (146) hide show
  1. package/AGENTS.md +271 -290
  2. package/README.md +8 -8
  3. package/dist/adapter.d.ts +52 -10
  4. package/dist/adapter.d.ts.map +1 -1
  5. package/dist/attempt.d.ts.map +1 -1
  6. package/dist/attempt.js +14 -1
  7. package/dist/bootstrap.d.ts +3 -2
  8. package/dist/bootstrap.d.ts.map +1 -1
  9. package/dist/bootstrap.js +33 -7
  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 +8 -1
  23. package/dist/chat/list.d.ts.map +1 -1
  24. package/dist/chat/list.js +48 -8
  25. package/dist/chat/markdown.d.ts +2 -1
  26. package/dist/chat/markdown.d.ts.map +1 -1
  27. package/dist/chat/markdown.js +7 -6
  28. package/dist/chat/message.d.ts.map +1 -1
  29. package/dist/chat/message.js +2 -2
  30. package/dist/chat/messages.d.ts +1 -0
  31. package/dist/chat/messages.d.ts.map +1 -1
  32. package/dist/chat/messages.js +4 -0
  33. package/dist/chat/provider.d.ts +1 -1
  34. package/dist/chat/provider.d.ts.map +1 -1
  35. package/dist/chat/provider.js +62 -79
  36. package/dist/chat/question.d.ts +1 -1
  37. package/dist/chat/question.d.ts.map +1 -1
  38. package/dist/chat/question.js +7 -18
  39. package/dist/chat/ready.d.ts +1 -0
  40. package/dist/chat/ready.d.ts.map +1 -1
  41. package/dist/chat/ready.js +4 -4
  42. package/dist/chat/style.js +5 -5
  43. package/dist/client.d.ts +17 -9
  44. package/dist/client.d.ts.map +1 -1
  45. package/dist/client.js +19 -6
  46. package/dist/collection.d.ts +20 -5
  47. package/dist/collection.d.ts.map +1 -1
  48. package/dist/collection.js +309 -33
  49. package/dist/connection.d.ts +31 -9
  50. package/dist/connection.d.ts.map +1 -1
  51. package/dist/connection.js +1620 -397
  52. package/dist/conversation.d.ts +20 -7
  53. package/dist/conversation.d.ts.map +1 -1
  54. package/dist/conversation.js +474 -163
  55. package/dist/durability.d.ts +7 -1
  56. package/dist/durability.d.ts.map +1 -1
  57. package/dist/entry.d.ts +17 -0
  58. package/dist/entry.d.ts.map +1 -0
  59. package/dist/entry.js +157 -0
  60. package/dist/fence.d.ts +6 -0
  61. package/dist/fence.d.ts.map +1 -0
  62. package/dist/fence.js +10 -0
  63. package/dist/host.d.ts +15 -3
  64. package/dist/host.d.ts.map +1 -1
  65. package/dist/host.js +94 -12
  66. package/dist/identity.d.ts +2 -0
  67. package/dist/identity.d.ts.map +1 -1
  68. package/dist/identity.js +6 -0
  69. package/dist/index.d.ts +10 -7
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/index.js +2 -0
  72. package/dist/memory.d.ts +8 -2
  73. package/dist/memory.d.ts.map +1 -1
  74. package/dist/memory.js +33 -6
  75. package/dist/model.d.ts +4 -2
  76. package/dist/model.d.ts.map +1 -1
  77. package/dist/model.js +10 -1
  78. package/dist/observation.d.ts +7 -2
  79. package/dist/observation.d.ts.map +1 -1
  80. package/dist/observation.js +29 -12
  81. package/dist/outcome.d.ts +12 -2
  82. package/dist/outcome.d.ts.map +1 -1
  83. package/dist/outcome.js +4 -0
  84. package/dist/overlay.d.ts +3 -0
  85. package/dist/overlay.d.ts.map +1 -1
  86. package/dist/overlay.js +62 -55
  87. package/dist/query.d.ts +56 -4
  88. package/dist/query.d.ts.map +1 -1
  89. package/dist/query.js +36 -7
  90. package/dist/reach.d.ts +6 -3
  91. package/dist/reach.d.ts.map +1 -1
  92. package/dist/reach.js +30 -5
  93. package/dist/react.d.ts +8 -6
  94. package/dist/react.d.ts.map +1 -1
  95. package/dist/react.js +7 -17
  96. package/dist/read-side.d.ts +16 -7
  97. package/dist/read-side.d.ts.map +1 -1
  98. package/dist/read-side.js +484 -100
  99. package/dist/readiness.d.ts +53 -0
  100. package/dist/readiness.d.ts.map +1 -0
  101. package/dist/readiness.js +33 -0
  102. package/dist/reads.d.ts +11 -0
  103. package/dist/reads.d.ts.map +1 -0
  104. package/dist/reads.js +98 -0
  105. package/dist/recovery.d.ts +25 -0
  106. package/dist/recovery.d.ts.map +1 -0
  107. package/dist/recovery.js +197 -0
  108. package/dist/refresh.d.ts +4 -4
  109. package/dist/refresh.d.ts.map +1 -1
  110. package/dist/refresh.js +18 -11
  111. package/dist/replica.d.ts +10 -5
  112. package/dist/replica.d.ts.map +1 -1
  113. package/dist/replica.js +96 -44
  114. package/dist/room-draft.d.ts +13 -1
  115. package/dist/room-draft.d.ts.map +1 -1
  116. package/dist/room-draft.js +2 -2
  117. package/dist/rows.d.ts +6 -13
  118. package/dist/rows.d.ts.map +1 -1
  119. package/dist/rows.js +10 -13
  120. package/dist/save.d.ts +9 -3
  121. package/dist/save.d.ts.map +1 -1
  122. package/dist/save.js +68 -22
  123. package/dist/search.d.ts +49 -6
  124. package/dist/search.d.ts.map +1 -1
  125. package/dist/search.js +192 -47
  126. package/dist/seed.d.ts +16 -0
  127. package/dist/seed.d.ts.map +1 -0
  128. package/dist/seed.js +75 -0
  129. package/dist/shared.d.ts +14 -0
  130. package/dist/shared.d.ts.map +1 -0
  131. package/dist/shared.js +394 -0
  132. package/dist/status.d.ts +4 -0
  133. package/dist/status.d.ts.map +1 -1
  134. package/dist/store.d.ts +64 -16
  135. package/dist/store.d.ts.map +1 -1
  136. package/dist/store.js +447 -178
  137. package/dist/turn.d.ts +9 -0
  138. package/dist/turn.d.ts.map +1 -0
  139. package/dist/turn.js +64 -0
  140. package/dist/windows.d.ts +26 -0
  141. package/dist/windows.d.ts.map +1 -0
  142. package/dist/windows.js +93 -0
  143. package/dist/wire.d.ts +152 -32
  144. package/dist/wire.d.ts.map +1 -1
  145. package/dist/wire.js +52 -31
  146. package/package.json +12 -8
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));
276
345
  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),
346
+ ...windowed(following, plan.limit, plan.after?.text, (entry) => cursorOf(groupValues(shape, entry))),
347
+ total: plan.total ? entries.length : undefined,
284
348
  };
285
349
  }
350
+ function framed(rows, { next, total }) {
351
+ return {
352
+ ...rows,
353
+ ...(next === undefined ? {} : { next }),
354
+ ...(total === undefined ? {} : { total }),
355
+ };
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;
@@ -330,107 +454,335 @@ export function createReadSide() {
330
454
  },
331
455
  });
332
456
  const shared = new Map();
457
+ let writing = false;
458
+ let publishing = null;
459
+ let closed = false;
333
460
  function searched(row) {
334
- const body = rows.get(row.body);
461
+ const read = row.text === null ? null : parse(row.text, row.path);
462
+ if (read === null || !read.ok)
463
+ return null;
335
464
  return {
336
465
  room: row.room,
337
466
  path: row.path,
338
467
  type: row.type,
339
468
  fields: row.fields,
340
- preamble: body.preamble,
341
- sections: body.sections,
469
+ preamble: read.document.preamble,
470
+ sections: read.document.sections,
342
471
  };
343
472
  }
344
- function publish(entry) {
345
- const next = reconciled(entry.result, keyedOf(entry.plan, [...entry.built.live.entries()]));
346
- if (next === null)
473
+ function kept(entry, key, row) {
474
+ const held = entry.rows.get(key);
475
+ if (held !== undefined && deepEquals(held, row))
347
476
  return;
348
- entry.result = next.keyed;
349
- for (const listener of entry.listeners)
350
- listener(next.change);
477
+ entry.rows.set(key, row);
351
478
  }
352
- return {
353
- apply(set) {
354
- const next = new Map();
355
- for (const change of set.documents) {
356
- for (const row of change.before)
357
- next.set(row.key, null);
358
- for (const row of change.after)
359
- next.set(row.key, row);
479
+ function counted(entry) {
480
+ return entry.built.count === null ? undefined : entry.built.count.size;
481
+ }
482
+ function rebuilt(entry) {
483
+ return after(rowsOfPlan(entry.plan, [...entry.built.live.entries()], counted(entry), entry.bodies), ({ rows: found, keys, next, total }) => {
484
+ const present = new Set(keys);
485
+ for (const key of [...entry.rows.keys()])
486
+ if (!present.has(key))
487
+ entry.rows.delete(key);
488
+ for (const key of [...entry.bodies.keys()]) {
489
+ if (!present.has(key))
490
+ entry.bodies.delete(key);
491
+ }
492
+ keys.forEach((key, index) => kept(entry, key, rowAt(found, index)));
493
+ return { keys, next, total };
494
+ });
495
+ }
496
+ function refreshed(entry, dirty) {
497
+ const shape = entry.plan.shape;
498
+ const { kept: inWindow, next, total, } = windowOf(entry.plan, [...entry.built.live.entries()], counted(entry));
499
+ const keys = inWindow.map(([key]) => key);
500
+ const present = new Set(keys);
501
+ for (const key of [...entry.rows.keys()]) {
502
+ if (present.has(key))
503
+ continue;
504
+ entry.rows.delete(key);
505
+ entry.bodies.delete(key);
506
+ }
507
+ const values = inWindow.filter(([key]) => dirty.has(key) || !entry.rows.has(key));
508
+ const done = inUnits(values, ([key, value]) => {
509
+ kept(entry, key, documentOf(key, value, shape, entry.bodies));
510
+ return weight(value);
511
+ });
512
+ return after(done, () => ({ keys, next, total }));
513
+ }
514
+ function current(entry, found, coverage) {
515
+ const rows = found.keys.map((key) => entry.rows.get(key));
516
+ return {
517
+ result: entry.plan.shape.kind === 'groups'
518
+ ? framed({ groups: rows, coverage }, found)
519
+ : framed({ documents: rows, coverage }, found),
520
+ keys: found.keys,
521
+ };
522
+ }
523
+ function delivered(entry, next) {
524
+ const previous = entry.result;
525
+ entry.result = next;
526
+ const message = previous === undefined ? null : changedFrom(previous, next);
527
+ if (message !== null)
528
+ for (const listener of entry.ready)
529
+ listener(message);
530
+ for (const listener of entry.fresh) {
531
+ entry.ready.add(listener);
532
+ listener(next);
533
+ }
534
+ entry.fresh.clear();
535
+ }
536
+ function run(entry) {
537
+ while (entry.listeners.size > 0 &&
538
+ (entry.whole || entry.dirty.size > 0 || entry.stale || entry.fresh.size > 0)) {
539
+ entry.stale = false;
540
+ const cover = entry.cover();
541
+ if ('pending' in cover) {
542
+ entry.whole = true;
543
+ entry.dirty.clear();
544
+ entry.rows.clear();
545
+ entry.bodies.clear();
546
+ delivered(entry, { result: cover, keys: [] });
547
+ break;
360
548
  }
361
- if (next.size === 0)
549
+ const whole = entry.whole ||
550
+ entry.plan.shape.kind === 'groups' ||
551
+ entry.result === undefined ||
552
+ 'pending' in entry.result.result;
553
+ const dirty = entry.dirty;
554
+ entry.dirty = new Set();
555
+ entry.whole = false;
556
+ const found = whole ? rebuilt(entry) : refreshed(entry, dirty);
557
+ if (found instanceof Promise) {
558
+ void found
559
+ .then((window) => {
560
+ if (entry.listeners.size > 0)
561
+ delivered(entry, current(entry, window, cover.coverage));
562
+ entry.running = false;
563
+ wake(entry);
564
+ })
565
+ .catch(() => {
566
+ entry.running = false;
567
+ });
362
568
  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;
369
- }
370
- sync.write({ type: rows.has(key) ? 'update' : 'insert', value: row });
371
569
  }
372
- sync.commit();
570
+ delivered(entry, current(entry, found, cover.coverage));
571
+ }
572
+ entry.running = false;
573
+ }
574
+ function wake(entry) {
575
+ if (entry.subscription === null || entry.running || writing || publishing !== null)
576
+ return;
577
+ entry.running = true;
578
+ try {
579
+ run(entry);
580
+ }
581
+ catch (error) {
582
+ entry.running = false;
583
+ throw error;
584
+ }
585
+ }
586
+ function recover() {
587
+ for (const entry of shared.values()) {
588
+ if (entry.result === undefined || entry.subscription === null)
589
+ continue;
590
+ const cover = entry.cover();
591
+ const before = entry.result.result;
592
+ const held = 'pending' in cover
593
+ ? deepEquals(before, cover)
594
+ : 'coverage' in before && deepEquals(before.coverage, cover.coverage);
595
+ if (held)
596
+ continue;
597
+ entry.stale = true;
598
+ wake(entry);
599
+ }
600
+ }
601
+ function committed(next) {
602
+ if (closed || next.size === 0)
603
+ return;
604
+ sync.begin();
605
+ for (const [key, row] of next) {
606
+ if (row === null) {
607
+ if (rows.has(key))
608
+ sync.write({ type: 'delete', key });
609
+ continue;
610
+ }
611
+ sync.write({ type: rows.has(key) ? 'update' : 'insert', value: row });
612
+ }
613
+ sync.commit();
614
+ }
615
+ function published(set) {
616
+ let unit = new Map();
617
+ return inUnits(set.documents, (change) => {
618
+ if (closed)
619
+ return 0;
620
+ let spent = 0;
621
+ for (const row of change.before) {
622
+ unit.set(row.key, null);
623
+ spent += ROW_WEIGHT;
624
+ }
625
+ for (const row of change.after) {
626
+ unit.set(row.key, row);
627
+ spent += weight(row);
628
+ }
629
+ return spent;
630
+ }, () => {
631
+ committed(unit);
632
+ unit = new Map();
633
+ });
634
+ }
635
+ function released() {
636
+ recover();
637
+ for (const entry of shared.values())
638
+ wake(entry);
639
+ }
640
+ function publishedLater(applied) {
641
+ const fence = applied.then(() => { }, () => { });
642
+ publishing = fence;
643
+ void fence.then(() => {
644
+ if (publishing !== fence)
645
+ return;
646
+ publishing = null;
647
+ released();
648
+ });
649
+ return applied;
650
+ }
651
+ async function settled() {
652
+ while (publishing !== null)
653
+ await publishing;
654
+ }
655
+ return {
656
+ apply(set, next = () => { }) {
657
+ const pending = publishing;
658
+ if (pending !== null) {
659
+ return publishedLater(pending.then(() => after(published(set), next)));
660
+ }
661
+ writing = true;
662
+ let applied;
663
+ try {
664
+ applied = published(set);
665
+ }
666
+ finally {
667
+ writing = false;
668
+ }
669
+ if (applied instanceof Promise)
670
+ return publishedLater(applied.then(next));
671
+ released();
672
+ next();
373
673
  },
374
- async query(plan) {
375
- const { live: collection, dispose } = built(rows, plan);
674
+ recover,
675
+ settled,
676
+ async query(plan, at) {
677
+ const { live: collection, count, dispose } = built(rows, plan);
376
678
  try {
377
679
  await collection.toArrayWhenReady();
378
- return keyedOf(plan, [...collection.entries()]).result;
680
+ if (count !== null)
681
+ await count.toArrayWhenReady();
682
+ await settled();
683
+ const entries = [...collection.entries()];
684
+ const held = at();
685
+ const found = await rowsOfPlan(plan, entries, count?.size, null, structuredClone);
686
+ return { rows: found.rows, at: held };
379
687
  }
380
688
  finally {
381
689
  dispose();
382
690
  }
383
691
  },
384
- watch(plan, identity, deliver) {
692
+ watch(plan, identity, cover, deliverTo) {
385
693
  let entry = shared.get(identity);
386
694
  if (entry === undefined) {
387
695
  const created = {
388
696
  plan,
389
697
  built: built(rows, plan),
698
+ cover,
390
699
  result: undefined,
700
+ rows: new Map(),
701
+ bodies: new Map(),
391
702
  listeners: new Set(),
703
+ fresh: new Set(),
704
+ ready: new Set(),
392
705
  subscription: null,
706
+ counting: null,
707
+ whole: true,
708
+ dirty: new Set(),
709
+ stale: false,
710
+ running: false,
393
711
  };
394
712
  created.built.live.on('status:change', ({ status }) => {
395
- if (status === 'cleaned-up' && created.listeners.size === 0)
713
+ if (status !== 'cleaned-up' || created.listeners.size > 0)
714
+ return;
715
+ if (shared.get(identity) === created)
396
716
  shared.delete(identity);
397
717
  });
398
718
  shared.set(identity, created);
399
719
  entry = created;
400
720
  }
401
721
  const observed = entry;
402
- observed.listeners.add(deliver);
403
- observed.built.live.onFirstReady(() => {
404
- if (!observed.listeners.has(deliver))
722
+ observed.listeners.add(deliverTo);
723
+ const ready = (then) => observed.built.live.onFirstReady(() => observed.built.count === null ? then() : observed.built.count.onFirstReady(then));
724
+ const settled = () => {
725
+ if (observed.built.count === null)
726
+ wake(observed);
727
+ else
728
+ queueMicrotask(() => wake(observed));
729
+ };
730
+ ready(() => {
731
+ if (!observed.listeners.has(deliverTo))
405
732
  return;
733
+ observed.fresh.add(deliverTo);
406
734
  if (observed.subscription === null) {
407
- observed.result = keyedOf(observed.plan, [...observed.built.live.entries()]);
408
- observed.subscription = observed.built.live.subscribeChanges(() => {
409
- publish(observed);
735
+ observed.whole = true;
736
+ observed.subscription = observed.built.live.subscribeChanges((changes) => {
737
+ if (observed.plan.shape.kind === 'groups')
738
+ observed.whole = true;
739
+ else
740
+ for (const { key } of changes)
741
+ observed.dirty.add(key);
742
+ settled();
410
743
  }, { includeInitialState: false });
744
+ observed.counting =
745
+ observed.built.count?.subscribeChanges(() => {
746
+ observed.stale = true;
747
+ settled();
748
+ }, { includeInitialState: false }) ?? null;
411
749
  }
412
- deliver(observed.result);
750
+ wake(observed);
413
751
  });
414
752
  return () => {
415
- if (!observed.listeners.delete(deliver) || observed.listeners.size > 0)
753
+ observed.fresh.delete(deliverTo);
754
+ observed.ready.delete(deliverTo);
755
+ if (!observed.listeners.delete(deliverTo) || observed.listeners.size > 0)
416
756
  return;
417
757
  observed.subscription?.unsubscribe();
418
758
  observed.subscription = null;
759
+ observed.counting?.unsubscribe();
760
+ observed.counting = null;
761
+ observed.result = undefined;
762
+ observed.rows.clear();
763
+ observed.bodies.clear();
764
+ observed.dirty.clear();
765
+ observed.whole = true;
766
+ if (shared.get(identity) === observed)
767
+ shared.delete(identity);
768
+ observed.built.dispose();
419
769
  };
420
770
  },
421
- documents() {
422
- return rows.toArray
423
- .filter((row) => row.kind === 'document' && row.type !== null)
424
- .map(searched);
771
+ room(room) {
772
+ return rows.toArray.flatMap((row) => row.kind !== 'document' || row.type === null || row.room !== room ? [] : [row.path]);
425
773
  },
426
774
  document(room, path) {
427
775
  const row = rows.get(documentKey(room, path));
428
776
  return row === undefined || row.type === null ? null : searched(row);
429
777
  },
430
778
  close() {
779
+ closed = true;
431
780
  for (const entry of shared.values()) {
432
781
  entry.listeners.clear();
782
+ entry.fresh.clear();
783
+ entry.ready.clear();
433
784
  entry.subscription?.unsubscribe();
785
+ entry.counting?.unsubscribe();
434
786
  entry.built.dispose();
435
787
  }
436
788
  shared.clear();
@@ -438,3 +790,35 @@ export function createReadSide() {
438
790
  },
439
791
  };
440
792
  }
793
+ function changedFrom(previous, next) {
794
+ const before = previous.result;
795
+ const after = next.result;
796
+ if ('pending' in before || 'pending' in after)
797
+ return deepEquals(before, after) ? null : next;
798
+ const earlier = 'groups' in before ? before.groups : before.documents;
799
+ const held = new Map(previous.keys.map((key, index) => [key, earlier[index]]));
800
+ const later = 'groups' in after ? after.groups : after.documents;
801
+ const changed = [];
802
+ next.keys.forEach((key, index) => {
803
+ if (held.get(key) !== later[index])
804
+ changed.push([key, later[index]]);
805
+ });
806
+ const present = new Set(next.keys);
807
+ const removed = previous.keys.filter((key) => !present.has(key));
808
+ const moved = reordered(previous.keys, next.keys, present);
809
+ const covered = !deepEquals(before.coverage, after.coverage);
810
+ const framing = reframed(before, after);
811
+ if (changed.length === 0 && removed.length === 0 && !moved && !covered && framing === null) {
812
+ return null;
813
+ }
814
+ return {
815
+ changed,
816
+ removed,
817
+ order: moved ? next.keys : null,
818
+ ...(covered ? { coverage: after.coverage } : {}),
819
+ ...framing,
820
+ };
821
+ }
822
+ function rowAt(rows, index) {
823
+ return 'groups' in rows ? rows.groups[index] : rows.documents[index];
824
+ }