@agentshouse/mdruntime 0.1.0-cec6690d5712e → 0.1.0-cedb03eaa48b6

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 +277 -271
  2. package/README.md +8 -8
  3. package/dist/adapter.d.ts +85 -20
  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/binary.d.ts +23 -0
  8. package/dist/binary.d.ts.map +1 -0
  9. package/dist/binary.js +8 -0
  10. package/dist/bootstrap.d.ts +3 -2
  11. package/dist/bootstrap.d.ts.map +1 -1
  12. package/dist/bootstrap.js +37 -14
  13. package/dist/change-set.d.ts +2 -0
  14. package/dist/change-set.d.ts.map +1 -1
  15. package/dist/change-set.js +40 -13
  16. package/dist/change.d.ts +11 -0
  17. package/dist/change.d.ts.map +1 -1
  18. package/dist/change.js +26 -1
  19. package/dist/chat/acts.d.ts +3 -4
  20. package/dist/chat/acts.d.ts.map +1 -1
  21. package/dist/chat/acts.js +10 -10
  22. package/dist/chat/earlier.d.ts +10 -0
  23. package/dist/chat/earlier.d.ts.map +1 -0
  24. package/dist/chat/earlier.js +54 -0
  25. package/dist/chat/kit.d.ts +2 -1
  26. package/dist/chat/kit.d.ts.map +1 -1
  27. package/dist/chat/kit.js +3 -3
  28. package/dist/chat/list.d.ts +8 -1
  29. package/dist/chat/list.d.ts.map +1 -1
  30. package/dist/chat/list.js +48 -8
  31. package/dist/chat/markdown.d.ts +2 -1
  32. package/dist/chat/markdown.d.ts.map +1 -1
  33. package/dist/chat/markdown.js +7 -6
  34. package/dist/chat/message.d.ts.map +1 -1
  35. package/dist/chat/message.js +2 -2
  36. package/dist/chat/messages.d.ts +1 -0
  37. package/dist/chat/messages.d.ts.map +1 -1
  38. package/dist/chat/messages.js +4 -0
  39. package/dist/chat/provider.d.ts +1 -1
  40. package/dist/chat/provider.d.ts.map +1 -1
  41. package/dist/chat/provider.js +62 -79
  42. package/dist/chat/question.d.ts +1 -1
  43. package/dist/chat/question.d.ts.map +1 -1
  44. package/dist/chat/question.js +7 -18
  45. package/dist/chat/ready.d.ts +1 -0
  46. package/dist/chat/ready.d.ts.map +1 -1
  47. package/dist/chat/ready.js +4 -4
  48. package/dist/chat/style.js +5 -5
  49. package/dist/client.d.ts +22 -9
  50. package/dist/client.d.ts.map +1 -1
  51. package/dist/client.js +23 -6
  52. package/dist/collection.d.ts +17 -3
  53. package/dist/collection.d.ts.map +1 -1
  54. package/dist/collection.js +260 -19
  55. package/dist/connection.d.ts +39 -12
  56. package/dist/connection.d.ts.map +1 -1
  57. package/dist/connection.js +1769 -385
  58. package/dist/conversation.d.ts +20 -7
  59. package/dist/conversation.d.ts.map +1 -1
  60. package/dist/conversation.js +474 -163
  61. package/dist/durability.d.ts +7 -1
  62. package/dist/durability.d.ts.map +1 -1
  63. package/dist/entry.d.ts +17 -0
  64. package/dist/entry.d.ts.map +1 -0
  65. package/dist/entry.js +157 -0
  66. package/dist/fence.d.ts +6 -0
  67. package/dist/fence.d.ts.map +1 -0
  68. package/dist/fence.js +10 -0
  69. package/dist/host.d.ts +15 -3
  70. package/dist/host.d.ts.map +1 -1
  71. package/dist/host.js +98 -12
  72. package/dist/identity.d.ts +3 -0
  73. package/dist/identity.d.ts.map +1 -1
  74. package/dist/identity.js +9 -0
  75. package/dist/index.d.ts +12 -7
  76. package/dist/index.d.ts.map +1 -1
  77. package/dist/index.js +3 -0
  78. package/dist/memory.d.ts +10 -4
  79. package/dist/memory.d.ts.map +1 -1
  80. package/dist/memory.js +64 -17
  81. package/dist/model.d.ts +4 -2
  82. package/dist/model.d.ts.map +1 -1
  83. package/dist/model.js +10 -1
  84. package/dist/observation.d.ts +7 -2
  85. package/dist/observation.d.ts.map +1 -1
  86. package/dist/observation.js +29 -12
  87. package/dist/outcome.d.ts +17 -1
  88. package/dist/outcome.d.ts.map +1 -1
  89. package/dist/outcome.js +10 -0
  90. package/dist/overlay.d.ts +15 -2
  91. package/dist/overlay.d.ts.map +1 -1
  92. package/dist/overlay.js +99 -62
  93. package/dist/query.d.ts +58 -4
  94. package/dist/query.d.ts.map +1 -1
  95. package/dist/query.js +36 -7
  96. package/dist/reach.d.ts +8 -3
  97. package/dist/reach.d.ts.map +1 -1
  98. package/dist/reach.js +53 -6
  99. package/dist/react.d.ts +8 -6
  100. package/dist/react.d.ts.map +1 -1
  101. package/dist/react.js +7 -17
  102. package/dist/read-side.d.ts +16 -7
  103. package/dist/read-side.d.ts.map +1 -1
  104. package/dist/read-side.js +493 -102
  105. package/dist/readiness.d.ts +53 -0
  106. package/dist/readiness.d.ts.map +1 -0
  107. package/dist/readiness.js +33 -0
  108. package/dist/reads.d.ts +11 -0
  109. package/dist/reads.d.ts.map +1 -0
  110. package/dist/reads.js +98 -0
  111. package/dist/recovery.d.ts +25 -0
  112. package/dist/recovery.d.ts.map +1 -0
  113. package/dist/recovery.js +197 -0
  114. package/dist/refresh.d.ts +4 -4
  115. package/dist/refresh.d.ts.map +1 -1
  116. package/dist/refresh.js +27 -25
  117. package/dist/replica.d.ts +11 -4
  118. package/dist/replica.d.ts.map +1 -1
  119. package/dist/replica.js +106 -48
  120. package/dist/room-draft.d.ts +32 -3
  121. package/dist/room-draft.d.ts.map +1 -1
  122. package/dist/room-draft.js +121 -3
  123. package/dist/rows.d.ts +8 -13
  124. package/dist/rows.d.ts.map +1 -1
  125. package/dist/rows.js +12 -13
  126. package/dist/save.d.ts +29 -0
  127. package/dist/save.d.ts.map +1 -1
  128. package/dist/save.js +358 -30
  129. package/dist/search.d.ts +49 -6
  130. package/dist/search.d.ts.map +1 -1
  131. package/dist/search.js +192 -47
  132. package/dist/seed.d.ts +16 -0
  133. package/dist/seed.d.ts.map +1 -0
  134. package/dist/seed.js +75 -0
  135. package/dist/shared.d.ts +11 -0
  136. package/dist/shared.d.ts.map +1 -0
  137. package/dist/shared.js +368 -0
  138. package/dist/status.d.ts +4 -0
  139. package/dist/status.d.ts.map +1 -1
  140. package/dist/store.d.ts +68 -21
  141. package/dist/store.d.ts.map +1 -1
  142. package/dist/store.js +463 -206
  143. package/dist/turn.d.ts +7 -0
  144. package/dist/turn.d.ts.map +1 -0
  145. package/dist/turn.js +51 -0
  146. package/dist/windows.d.ts +26 -0
  147. package/dist/windows.d.ts.map +1 -0
  148. package/dist/windows.js +93 -0
  149. package/dist/wire.d.ts +152 -30
  150. package/dist/wire.d.ts.map +1 -1
  151. package/dist/wire.js +52 -29
  152. package/package.json +12 -8
  153. package/dist/attachment.d.ts +0 -3
  154. package/dist/attachment.d.ts.map +0 -1
  155. package/dist/attachment.js +0 -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,33 +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,
153
+ binary: d.binary,
154
+ ...Object.fromEntries(shape.orderBy.map(({ key }, index) => [`order${index}`, keyed(key, d)])),
124
155
  ...(shape.fields === null ? {} : { fields: d.fields }),
125
- ...(shape.sections === 'all' ? { sections: b.sections } : {}),
126
- ...(Array.isArray(shape.sections) ? named('section', shape.sections, b.sections) : {}),
127
- ...(shape.preamble ? { preamble: b.preamble } : {}),
128
- ...(shape.markdown ? { markdown: b.markdown } : {}),
156
+ ...(bodied(shape) ? { text: d.text } : {}),
129
157
  ...(shape.backlinks ? { backlinks: toArray(backlinked(q, rows, d, plan.reach)) } : {}),
130
158
  ...(shape.change ? { change: d.change } : {}),
159
+ ...(shape.fileId ? { fileId: d.fileId } : {}),
131
160
  }));
132
161
  }
133
162
  function backlinked(q, rows, document, reach) {
@@ -138,15 +167,13 @@ function backlinked(q, rows, document, reach) {
138
167
  .orderBy(({ r }) => r.field, { stringSort: 'lexical' })
139
168
  .select(({ r }) => ({ path: r.path, type: r.type, field: r.field }));
140
169
  }
141
- function groups(q, stage, shape, limit) {
170
+ function groups(q, stage, shape) {
142
171
  const keys = shape.groupBy.map((_, index) => `key${index}`);
143
172
  const from = q.from({ m: stage });
144
173
  let query = from.groupBy(({ m }) => keys.map((key) => m[key]));
145
174
  for (const { by, direction } of shape.orderBy) {
146
175
  query = query.orderBy(({ m }) => (by === 'count' ? count(m.key) : m[keys[by]]), direction);
147
176
  }
148
- if (limit !== null && shape.orderBy.length > 0)
149
- query = query.limit(limit);
150
177
  return query.select(({ m }) => ({
151
178
  ...Object.fromEntries(keys.map((key) => [key, m[key]])),
152
179
  count: count(m.key),
@@ -156,28 +183,49 @@ function built(rows, plan) {
156
183
  const shape = plan.shape;
157
184
  if (shape.kind === 'documents') {
158
185
  const collection = live((q) => documents(q, rows, plan, shape));
159
- 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
+ };
160
198
  }
161
199
  const stage = live((q) => filtered(q, rows, plan).select(({ d }) => ({
162
200
  key: d.key,
163
201
  ...Object.fromEntries(shape.groupBy.map((key, index) => [`key${index}`, keyed(key, d)])),
164
202
  })));
165
- const collection = live((q) => groups(q, stage, shape, plan.limit));
203
+ const collection = live((q) => groups(q, stage, shape));
166
204
  return {
167
205
  live: collection,
206
+ count: null,
168
207
  dispose: () => {
169
208
  void collection.cleanup();
170
209
  void stage.cleanup();
171
210
  },
172
211
  };
173
212
  }
174
- function present(entries) {
175
- return Object.fromEntries(entries.filter(([, value]) => value !== undefined));
176
- }
177
- function part(row, prefix, names, whole) {
213
+ function sectionsOf(sections, names) {
178
214
  if (names === 'all')
179
- return whole;
180
- 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;
181
229
  }
182
230
  function masked(pointers) {
183
231
  const root = { whole: false, members: new Map() };
@@ -228,9 +276,17 @@ function cut(value, mask) {
228
276
  function selected(fields, pointers) {
229
277
  return (cut(fields, masked(pointers)) ?? {});
230
278
  }
231
- function documentOf(row, shape) {
232
- if (row.type === null)
233
- return { room: row.room, path: row.path };
279
+ function documentOf(key, row, shape, bodies) {
280
+ if (row.type === null) {
281
+ const binary = row.binary;
282
+ return {
283
+ room: row.room,
284
+ path: row.path,
285
+ ...(binary === null ? {} : { binaryFile: binary }),
286
+ ...(shape.fileId ? { fileId: row.fileId } : {}),
287
+ };
288
+ }
289
+ const body = bodied(shape) ? bodyOf(row, key, bodies) : null;
234
290
  return {
235
291
  room: row.room,
236
292
  path: row.path,
@@ -242,45 +298,94 @@ function documentOf(row, shape) {
242
298
  ? row.fields
243
299
  : selected(row.fields, shape.fields),
244
300
  }),
245
- ...(shape.sections === null
301
+ ...(shape.sections === null || body === null
246
302
  ? {}
247
- : {
248
- sections: part(row, 'section', shape.sections, row.sections),
249
- }),
250
- ...(shape.preamble ? { preamble: row.preamble } : {}),
251
- ...(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 } : {}),
252
306
  ...(shape.backlinks
253
307
  ? {
254
- 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 })),
255
311
  }
256
312
  : {}),
257
313
  ...(shape.change ? { change: row.change } : {}),
314
+ ...(shape.fileId ? { fileId: row.fileId } : {}),
258
315
  };
259
316
  }
260
- 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) {
261
333
  const shape = plan.shape;
262
334
  if (shape.kind === 'documents') {
263
335
  return {
264
- result: structuredClone({ documents: entries.map(([, row]) => documentOf(row, shape)) }),
265
- keys: entries.map(([key]) => key),
336
+ ...windowed(entries, plan.limit, plan.after?.text, ([, row]) => documentCursor(shape, row)),
337
+ total,
266
338
  };
267
339
  }
268
- 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 }) {
269
351
  return {
270
- result: structuredClone({
271
- groups: bounded.map(([, row]) => ({
272
- key: shape.groupBy.map((_, index) => row[`key${index}`]),
273
- count: row.count,
274
- })),
275
- }),
276
- keys: bounded.map(([key]) => key),
352
+ ...rows,
353
+ ...(next === undefined ? {} : { next }),
354
+ ...(total === undefined ? {} : { total }),
277
355
  };
278
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
+ }
279
377
  export function reconciled(previous, next) {
280
- const before = rowsOf(previous.result);
281
- 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]]));
282
387
  const changed = [];
283
- const rows = rowsOf(next.result).map((row, index) => {
388
+ const rows = ('groups' in after ? after.groups : after.documents).map((row, index) => {
284
389
  const key = next.keys[index];
285
390
  const kept = held.get(key);
286
391
  if (kept !== undefined && deepEquals(kept, row))
@@ -289,22 +394,48 @@ export function reconciled(previous, next) {
289
394
  return row;
290
395
  });
291
396
  const present = new Set(next.keys);
292
- const survivors = previous.keys.filter((key) => present.has(key));
293
397
  const removed = previous.keys.filter((key) => !present.has(key));
294
- const moved = survivors.length !== next.keys.length ||
295
- survivors.some((key, index) => key !== next.keys[index]);
296
- 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) {
297
402
  return null;
403
+ }
404
+ const window = { next: after.next, total: after.total };
298
405
  return {
299
406
  keyed: {
300
- result: 'groups' in next.result
301
- ? { groups: rows }
302
- : { documents: rows },
407
+ result: 'groups' in after
408
+ ? framed({ groups: rows, coverage: after.coverage }, window)
409
+ : framed({ documents: rows, coverage: after.coverage }, window),
303
410
  keys: next.keys,
304
411
  },
305
- 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
+ },
306
419
  };
307
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
+ }
308
439
  export function createReadSide() {
309
440
  configureIndexDevMode({ enabled: false });
310
441
  let sync;
@@ -323,107 +454,335 @@ export function createReadSide() {
323
454
  },
324
455
  });
325
456
  const shared = new Map();
457
+ let writing = false;
458
+ let publishing = null;
459
+ let closed = false;
326
460
  function searched(row) {
327
- 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;
328
464
  return {
329
465
  room: row.room,
330
466
  path: row.path,
331
467
  type: row.type,
332
468
  fields: row.fields,
333
- preamble: body.preamble,
334
- sections: body.sections,
469
+ preamble: read.document.preamble,
470
+ sections: read.document.sections,
335
471
  };
336
472
  }
337
- function publish(entry) {
338
- const next = reconciled(entry.result, keyedOf(entry.plan, [...entry.built.live.entries()]));
339
- if (next === null)
473
+ function kept(entry, key, row) {
474
+ const held = entry.rows.get(key);
475
+ if (held !== undefined && deepEquals(held, row))
340
476
  return;
341
- entry.result = next.keyed;
342
- for (const listener of entry.listeners)
343
- listener(next.change);
477
+ entry.rows.set(key, row);
344
478
  }
345
- return {
346
- apply(set) {
347
- const next = new Map();
348
- for (const change of set.documents) {
349
- for (const row of change.before)
350
- next.set(row.key, null);
351
- for (const row of change.after)
352
- 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);
353
491
  }
354
- if (next.size === 0)
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;
548
+ }
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
+ });
355
568
  return;
356
- sync.begin();
357
- for (const [key, row] of next) {
358
- if (row === null) {
359
- if (rows.has(key))
360
- sync.write({ type: 'delete', key });
361
- continue;
362
- }
363
- sync.write({ type: rows.has(key) ? 'update' : 'insert', value: row });
364
569
  }
365
- 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();
366
673
  },
367
- async query(plan) {
368
- 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);
369
678
  try {
370
679
  await collection.toArrayWhenReady();
371
- 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 };
372
687
  }
373
688
  finally {
374
689
  dispose();
375
690
  }
376
691
  },
377
- watch(plan, identity, deliver) {
692
+ watch(plan, identity, cover, deliverTo) {
378
693
  let entry = shared.get(identity);
379
694
  if (entry === undefined) {
380
695
  const created = {
381
696
  plan,
382
697
  built: built(rows, plan),
698
+ cover,
383
699
  result: undefined,
700
+ rows: new Map(),
701
+ bodies: new Map(),
384
702
  listeners: new Set(),
703
+ fresh: new Set(),
704
+ ready: new Set(),
385
705
  subscription: null,
706
+ counting: null,
707
+ whole: true,
708
+ dirty: new Set(),
709
+ stale: false,
710
+ running: false,
386
711
  };
387
712
  created.built.live.on('status:change', ({ status }) => {
388
- 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)
389
716
  shared.delete(identity);
390
717
  });
391
718
  shared.set(identity, created);
392
719
  entry = created;
393
720
  }
394
721
  const observed = entry;
395
- observed.listeners.add(deliver);
396
- observed.built.live.onFirstReady(() => {
397
- 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))
398
732
  return;
733
+ observed.fresh.add(deliverTo);
399
734
  if (observed.subscription === null) {
400
- observed.result = keyedOf(observed.plan, [...observed.built.live.entries()]);
401
- observed.subscription = observed.built.live.subscribeChanges(() => {
402
- 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();
403
743
  }, { includeInitialState: false });
744
+ observed.counting =
745
+ observed.built.count?.subscribeChanges(() => {
746
+ observed.stale = true;
747
+ settled();
748
+ }, { includeInitialState: false }) ?? null;
404
749
  }
405
- deliver(observed.result);
750
+ wake(observed);
406
751
  });
407
752
  return () => {
408
- 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)
409
756
  return;
410
757
  observed.subscription?.unsubscribe();
411
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();
412
769
  };
413
770
  },
414
- documents() {
415
- return rows.toArray
416
- .filter((row) => row.kind === 'document' && row.type !== null)
417
- .map(searched);
771
+ room(room) {
772
+ return rows.toArray.flatMap((row) => row.kind !== 'document' || row.type === null || row.room !== room ? [] : [row.path]);
418
773
  },
419
774
  document(room, path) {
420
775
  const row = rows.get(documentKey(room, path));
421
776
  return row === undefined || row.type === null ? null : searched(row);
422
777
  },
423
778
  close() {
779
+ closed = true;
424
780
  for (const entry of shared.values()) {
425
781
  entry.listeners.clear();
782
+ entry.fresh.clear();
783
+ entry.ready.clear();
426
784
  entry.subscription?.unsubscribe();
785
+ entry.counting?.unsubscribe();
427
786
  entry.built.dispose();
428
787
  }
429
788
  shared.clear();
@@ -431,3 +790,35 @@ export function createReadSide() {
431
790
  },
432
791
  };
433
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
+ }