@agentshouse/mdruntime 0.1.0-ca9afacd5aebd → 0.1.0-caee7332d2235

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