@agentshouse/mdruntime 0.1.0-c5d3fa6fb67bc → 0.1.0-c6636685d0c4b

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 (135) hide show
  1. package/AGENTS.md +205 -205
  2. package/README.md +8 -8
  3. package/dist/adapter.d.ts +36 -9
  4. package/dist/adapter.d.ts.map +1 -1
  5. package/dist/bootstrap.d.ts +2 -1
  6. package/dist/bootstrap.d.ts.map +1 -1
  7. package/dist/bootstrap.js +1 -1
  8. package/dist/change-set.d.ts +5 -2
  9. package/dist/change-set.d.ts.map +1 -1
  10. package/dist/change-set.js +6 -1
  11. package/dist/chat/earlier.d.ts +10 -0
  12. package/dist/chat/earlier.d.ts.map +1 -0
  13. package/dist/chat/earlier.js +54 -0
  14. package/dist/chat/kit.js +1 -1
  15. package/dist/chat/list.d.ts +2 -1
  16. package/dist/chat/list.d.ts.map +1 -1
  17. package/dist/chat/list.js +45 -5
  18. package/dist/chat/markdown.d.ts +1 -0
  19. package/dist/chat/markdown.d.ts.map +1 -1
  20. package/dist/chat/markdown.js +78 -20
  21. package/dist/chat/message.d.ts +1 -0
  22. package/dist/chat/message.d.ts.map +1 -1
  23. package/dist/chat/message.js +12 -2
  24. package/dist/chat/messages.d.ts +2 -0
  25. package/dist/chat/messages.d.ts.map +1 -1
  26. package/dist/chat/messages.js +52 -20
  27. package/dist/chat/overlay.d.ts +1 -0
  28. package/dist/chat/overlay.d.ts.map +1 -1
  29. package/dist/chat/overlay.js +10 -0
  30. package/dist/chat/pieces.d.ts +62 -0
  31. package/dist/chat/pieces.d.ts.map +1 -0
  32. package/dist/chat/pieces.js +1110 -0
  33. package/dist/chat/provider.d.ts +1 -0
  34. package/dist/chat/provider.d.ts.map +1 -1
  35. package/dist/chat/provider.js +23 -14
  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 -1
  40. package/dist/chat/ready.d.ts.map +1 -1
  41. package/dist/chat/ready.js +1 -1
  42. package/dist/chat/steady.d.ts +3 -0
  43. package/dist/chat/steady.d.ts.map +1 -0
  44. package/dist/chat/steady.js +26 -0
  45. package/dist/chat/style.js +1 -1
  46. package/dist/chat.d.ts +2 -2
  47. package/dist/chat.d.ts.map +1 -1
  48. package/dist/chat.js +2 -2
  49. package/dist/client.d.ts +6 -2
  50. package/dist/client.d.ts.map +1 -1
  51. package/dist/client.js +16 -5
  52. package/dist/collection.d.ts +4 -3
  53. package/dist/collection.d.ts.map +1 -1
  54. package/dist/collection.js +128 -63
  55. package/dist/connection.d.ts +12 -4
  56. package/dist/connection.d.ts.map +1 -1
  57. package/dist/connection.js +577 -217
  58. package/dist/conversation.d.ts +12 -0
  59. package/dist/conversation.d.ts.map +1 -1
  60. package/dist/conversation.js +196 -41
  61. package/dist/entry.d.ts.map +1 -1
  62. package/dist/entry.js +2 -1
  63. package/dist/host.d.ts +5 -4
  64. package/dist/host.d.ts.map +1 -1
  65. package/dist/host.js +45 -5
  66. package/dist/index.d.ts +5 -4
  67. package/dist/index.d.ts.map +1 -1
  68. package/dist/index.js +1 -0
  69. package/dist/memory.d.ts +6 -3
  70. package/dist/memory.d.ts.map +1 -1
  71. package/dist/memory.js +17 -11
  72. package/dist/observation.d.ts +2 -0
  73. package/dist/observation.d.ts.map +1 -1
  74. package/dist/observation.js +9 -3
  75. package/dist/outcome.d.ts +14 -2
  76. package/dist/outcome.d.ts.map +1 -1
  77. package/dist/outcome.js +7 -0
  78. package/dist/overlay.d.ts +4 -4
  79. package/dist/overlay.d.ts.map +1 -1
  80. package/dist/overlay.js +181 -140
  81. package/dist/query.d.ts +26 -2
  82. package/dist/query.d.ts.map +1 -1
  83. package/dist/query.js +34 -6
  84. package/dist/reach.d.ts +4 -1
  85. package/dist/reach.d.ts.map +1 -1
  86. package/dist/reach.js +90 -21
  87. package/dist/react.d.ts +3 -3
  88. package/dist/react.d.ts.map +1 -1
  89. package/dist/react.js +7 -17
  90. package/dist/read-side.d.ts +3 -1
  91. package/dist/read-side.d.ts.map +1 -1
  92. package/dist/read-side.js +271 -82
  93. package/dist/reads.d.ts +11 -0
  94. package/dist/reads.d.ts.map +1 -0
  95. package/dist/reads.js +98 -0
  96. package/dist/refresh.d.ts +4 -4
  97. package/dist/refresh.d.ts.map +1 -1
  98. package/dist/refresh.js +35 -16
  99. package/dist/replica.d.ts +11 -5
  100. package/dist/replica.d.ts.map +1 -1
  101. package/dist/replica.js +69 -40
  102. package/dist/room-draft.d.ts +12 -0
  103. package/dist/room-draft.d.ts.map +1 -1
  104. package/dist/room-draft.js +11 -12
  105. package/dist/rows.d.ts +1 -0
  106. package/dist/rows.d.ts.map +1 -1
  107. package/dist/rows.js +5 -0
  108. package/dist/save.d.ts +4 -2
  109. package/dist/save.d.ts.map +1 -1
  110. package/dist/save.js +195 -111
  111. package/dist/search.d.ts +17 -2
  112. package/dist/search.d.ts.map +1 -1
  113. package/dist/search.js +81 -7
  114. package/dist/seed.d.ts +16 -0
  115. package/dist/seed.d.ts.map +1 -0
  116. package/dist/seed.js +75 -0
  117. package/dist/shared.d.ts +4 -0
  118. package/dist/shared.d.ts.map +1 -1
  119. package/dist/shared.js +63 -33
  120. package/dist/store.d.ts +18 -2
  121. package/dist/store.d.ts.map +1 -1
  122. package/dist/store.js +398 -316
  123. package/dist/turn.d.ts +5 -3
  124. package/dist/turn.d.ts.map +1 -1
  125. package/dist/turn.js +26 -7
  126. package/dist/windows.d.ts +26 -0
  127. package/dist/windows.d.ts.map +1 -0
  128. package/dist/windows.js +93 -0
  129. package/dist/wire.d.ts +16 -0
  130. package/dist/wire.d.ts.map +1 -1
  131. package/dist/wire.js +4 -0
  132. package/package.json +2 -2
  133. package/dist/acquisition.d.ts +0 -14
  134. package/dist/acquisition.d.ts.map +0 -1
  135. package/dist/acquisition.js +0 -24
package/dist/read-side.js CHANGED
@@ -1,7 +1,8 @@
1
- import { and, BasicIndex, configureIndexDevMode, count, createCollection, createLiveQueryCollection, deepEquals, eq, inArray, or, toArray, } from '@tanstack/db';
1
+ import { and, BasicIndex, configureIndexDevMode, count, createCollection, createLiveQueryCollection, deepEquals, eq, inArray, not, or, toArray, } from '@tanstack/db';
2
2
  import { ARRAY_INDEX, parse, pointerSegments } from '@agentshouse/mdmodel';
3
3
  import { documentKey } from './rows.js';
4
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);
@@ -54,16 +55,21 @@ function conditioned(condition, row) {
54
55
  function narrowed(query, where, alias) {
55
56
  return where === null ? query : query.fn.where((row) => conditioned(where, row[alias]));
56
57
  }
57
- function reached(reach, room, document) {
58
- return reach === null
59
- ? []
60
- : [or(inArray(room, [...reach.rooms]), inArray(document, [...reach.documents]))];
58
+ function reached(reach, locked, room, document) {
59
+ return [
60
+ ...(reach === null
61
+ ? []
62
+ : [or(inArray(room, [...reach.rooms]), inArray(document, [...reach.documents]))]),
63
+ ...(locked.length === 0 ? [] : [not(inArray(room, [...locked]))]),
64
+ ];
61
65
  }
62
- function scoped(scope, row, rooms, reach) {
66
+ function scoped(scope, row, rooms, reach, locked, paths = null, folder = null) {
63
67
  return every([
64
68
  eq(row.kind, 'document'),
65
69
  ...(rooms === null ? [] : [inArray(row.room, [...rooms])]),
66
- ...reached(reach, row.room, row.key),
70
+ ...(paths === null ? [] : [inArray(row.path, [...paths])]),
71
+ ...(folder === null ? [] : [eq(row.folder, folder)]),
72
+ ...reached(reach, locked, row.room, row.key),
67
73
  ...(scope.types === null ? [] : [inArray(row.type, [...scope.types])]),
68
74
  ...(scope.pathPrefix === null ? [] : [inArray(scope.pathPrefix, row.directories)]),
69
75
  ]);
@@ -77,26 +83,26 @@ function live(query) {
77
83
  startSync: true,
78
84
  });
79
85
  }
80
- function related(q, rows, scope, field, kept, reach) {
86
+ function related(q, rows, scope, field, kept, reach, locked) {
81
87
  const matched = kept === 'source' ? 'target' : 'source';
82
88
  const from = q.from({ r: rows });
83
89
  const joined = from
84
90
  .where(({ r }) => every([eq(r.kind, 'reference'), ...(field === null ? [] : [inArray(field, r.forms)])]))
85
91
  .innerJoin({ m: rows }, ({ r, m }) => eq(r[matched], m.key))
86
- .where(({ m }) => scoped(scope, m, null, reach));
92
+ .where(({ m }) => scoped(scope, m, null, reach, locked));
87
93
  return narrowed(joined, scope.where, 'm')
88
94
  .groupBy(({ r }) => r[kept])
89
95
  .select(({ r }) => ({ key: r[kept] }));
90
96
  }
91
97
  function filtered(q, rows, plan) {
92
98
  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');
99
+ let query = narrowed(from.where(({ d }) => scoped(plan.root, d, plan.rooms, plan.reach, plan.locked, plan.paths, plan.folder)), plan.root.where, 'd');
94
100
  if (plan.hop !== null) {
95
- const hop = related(q, rows, plan.hop, plan.hop.field, 'source', plan.reach);
101
+ const hop = related(q, rows, plan.hop, plan.hop.field, 'source', plan.reach, plan.locked);
96
102
  query = query.innerJoin({ h: hop }, ({ d, h }) => eq(d.key, h.key));
97
103
  }
98
104
  if (plan.backlinks !== null) {
99
- const backlinks = related(q, rows, plan.backlinks, plan.backlinks.field, 'target', plan.reach);
105
+ const backlinks = related(q, rows, plan.backlinks, plan.backlinks.field, 'target', plan.reach, plan.locked);
100
106
  query = query.innerJoin({ k: backlinks }, ({ d, k }) => eq(d.key, k.key));
101
107
  }
102
108
  return query;
@@ -104,45 +110,75 @@ function filtered(q, rows, plan) {
104
110
  function bodied(shape) {
105
111
  return shape.sections !== null || shape.preamble || shape.markdown;
106
112
  }
113
+ function orderingOf(options) {
114
+ const collated = options.stringSort === 'locale';
115
+ return {
116
+ direction: options.direction,
117
+ nulls: options.nulls ?? 'first',
118
+ stringSort: options.stringSort ?? 'locale',
119
+ locale: collated ? options.locale : undefined,
120
+ localeOptions: collated
121
+ ? options.localeOptions
122
+ : undefined,
123
+ };
124
+ }
125
+ function storedValue(key, row) {
126
+ return key.of === 'field' ? row.values[key.field] : row[key.of];
127
+ }
128
+ function documentOrder(shape) {
129
+ return shape.orderBy.map(({ options }) => orderingOf(options));
130
+ }
131
+ function documentCursor(shape, row) {
132
+ return cursorOf([
133
+ ...shape.orderBy.map((_, index) => row[`order${index}`]),
134
+ documentKey(row.room, row.path),
135
+ ]);
136
+ }
107
137
  function documents(q, rows, plan, shape) {
108
138
  let query = filtered(q, rows, plan);
139
+ const cursor = plan.after?.values ?? null;
140
+ if (cursor !== null) {
141
+ const orderings = documentOrder(shape);
142
+ query = query.fn.where(({ d }) => beyond([...shape.orderBy.map(({ key }) => storedValue(key, d)), d.key], cursor, orderings));
143
+ }
109
144
  for (const { key, options } of shape.orderBy) {
110
145
  query = query.orderBy(({ d }) => keyed(key, d), options);
111
146
  }
112
- if (shape.orderBy.length === 0 && plan.limit !== null) {
147
+ if (shape.orderBy.length > 0 || plan.limit !== null || plan.after !== null) {
113
148
  query = query.orderBy(({ d }) => d.key, { stringSort: 'lexical' });
114
149
  }
115
150
  if (plan.limit !== null)
116
- query = query.limit(plan.limit);
151
+ query = query.limit(plan.limit + 1);
117
152
  return query.select(({ d }) => ({
118
153
  room: d.room,
119
154
  path: d.path,
120
155
  type: d.type,
121
156
  binary: d.binary,
157
+ ...Object.fromEntries(shape.orderBy.map(({ key }, index) => [`order${index}`, keyed(key, d)])),
122
158
  ...(shape.fields === null ? {} : { fields: d.fields }),
123
159
  ...(bodied(shape) ? { text: d.text } : {}),
124
- ...(shape.backlinks ? { backlinks: toArray(backlinked(q, rows, d, plan.reach)) } : {}),
160
+ ...(shape.backlinks
161
+ ? { backlinks: toArray(backlinked(q, rows, d, plan.reach, plan.locked)) }
162
+ : {}),
125
163
  ...(shape.change ? { change: d.change } : {}),
126
164
  ...(shape.fileId ? { fileId: d.fileId } : {}),
127
165
  }));
128
166
  }
129
- function backlinked(q, rows, document, reach) {
167
+ function backlinked(q, rows, document, reach, locked) {
130
168
  const from = q.from({ r: rows });
131
169
  return from
132
- .where(({ r }) => every([eq(r.target, document.key), ...reached(reach, r.room, r.source)]))
170
+ .where(({ r }) => every([eq(r.target, document.key), ...reached(reach, locked, r.room, r.source)]))
133
171
  .orderBy(({ r }) => r.path, { stringSort: 'lexical' })
134
172
  .orderBy(({ r }) => r.field, { stringSort: 'lexical' })
135
173
  .select(({ r }) => ({ path: r.path, type: r.type, field: r.field }));
136
174
  }
137
- function groups(q, stage, shape, limit) {
175
+ function groups(q, stage, shape) {
138
176
  const keys = shape.groupBy.map((_, index) => `key${index}`);
139
177
  const from = q.from({ m: stage });
140
178
  let query = from.groupBy(({ m }) => keys.map((key) => m[key]));
141
179
  for (const { by, direction } of shape.orderBy) {
142
180
  query = query.orderBy(({ m }) => (by === 'count' ? count(m.key) : m[keys[by]]), direction);
143
181
  }
144
- if (limit !== null && shape.orderBy.length > 0)
145
- query = query.limit(limit);
146
182
  return query.select(({ m }) => ({
147
183
  ...Object.fromEntries(keys.map((key) => [key, m[key]])),
148
184
  count: count(m.key),
@@ -152,15 +188,27 @@ function built(rows, plan) {
152
188
  const shape = plan.shape;
153
189
  if (shape.kind === 'documents') {
154
190
  const collection = live((q) => documents(q, rows, plan, shape));
155
- return { live: collection, dispose: () => void collection.cleanup() };
191
+ const count = plan.total
192
+ ? live((q) => filtered(q, rows, plan).select(({ d }) => ({ key: d.key })))
193
+ : null;
194
+ return {
195
+ live: collection,
196
+ count,
197
+ dispose: () => {
198
+ void collection.cleanup();
199
+ if (count !== null)
200
+ void count.cleanup();
201
+ },
202
+ };
156
203
  }
157
204
  const stage = live((q) => filtered(q, rows, plan).select(({ d }) => ({
158
205
  key: d.key,
159
206
  ...Object.fromEntries(shape.groupBy.map((key, index) => [`key${index}`, keyed(key, d)])),
160
207
  })));
161
- const collection = live((q) => groups(q, stage, shape, plan.limit));
208
+ const collection = live((q) => groups(q, stage, shape));
162
209
  return {
163
210
  live: collection,
211
+ count: null,
164
212
  dispose: () => {
165
213
  void collection.cleanup();
166
214
  void stage.cleanup();
@@ -262,7 +310,9 @@ function documentOf(key, row, shape, bodies) {
262
310
  ...(shape.markdown && body !== null ? { markdown: body.content } : {}),
263
311
  ...(shape.backlinks
264
312
  ? {
265
- backlinks: row.backlinks.map(({ path, type, field }) => ({ path, type, field })),
313
+ backlinks: row.backlinks
314
+ .slice(0, MAXIMUM_WINDOW)
315
+ .map(({ path, type, field }) => ({ path, type, field })),
266
316
  }
267
317
  : {}),
268
318
  ...(shape.change ? { change: row.change } : {}),
@@ -272,24 +322,63 @@ function documentOf(key, row, shape, bodies) {
272
322
  function weight(row) {
273
323
  return ROW_WEIGHT + (typeof row?.text === 'string' ? row.text.length : 0);
274
324
  }
275
- function rowsOfPlan(plan, entries, bodies, each = (row) => row) {
325
+ const HELD_PER_WEIGHT = 16;
326
+ function groupValues(shape, [key, row]) {
327
+ return [...shape.orderBy.map(({ by }) => (by === 'count' ? row.count : row[`key${by}`])), key];
328
+ }
329
+ function groupOrder(shape) {
330
+ return shape.orderBy.map(({ direction }) => ({
331
+ direction,
332
+ nulls: 'first',
333
+ stringSort: 'locale',
334
+ locale: undefined,
335
+ localeOptions: undefined,
336
+ }));
337
+ }
338
+ function windowOf(plan, entries, total) {
339
+ const shape = plan.shape;
340
+ if (shape.kind === 'documents') {
341
+ return {
342
+ ...windowed(entries, plan.limit, plan.after?.text, ([, row]) => documentCursor(shape, row)),
343
+ total,
344
+ };
345
+ }
346
+ const cursor = plan.after?.values ?? null;
347
+ const orderings = groupOrder(shape);
348
+ const following = cursor === null
349
+ ? entries
350
+ : entries.filter((entry) => beyond(groupValues(shape, entry), cursor, orderings));
351
+ return {
352
+ ...windowed(following, plan.limit, plan.after?.text, (entry) => cursorOf(groupValues(shape, entry))),
353
+ total: plan.total ? entries.length : undefined,
354
+ };
355
+ }
356
+ function framed(rows, { next, total }) {
357
+ return {
358
+ ...rows,
359
+ ...(next === undefined ? {} : { next }),
360
+ ...(total === undefined ? {} : { total }),
361
+ };
362
+ }
363
+ function rowsOfPlan(plan, entries, total, bodies, each = (row) => row) {
276
364
  const shape = plan.shape;
365
+ const { kept, ...window } = windowOf(plan, entries, total);
366
+ const keys = kept.map(([key]) => key);
277
367
  if (shape.kind === 'documents') {
278
368
  const documents = [];
279
- return after(inUnits(entries, ([key, row]) => {
369
+ return after(inUnits(kept, ([key, row]) => {
280
370
  documents.push(each(documentOf(key, row, shape, bodies)));
281
371
  return weight(row);
282
- }), () => ({ rows: { documents }, keys: entries.map(([key]) => key) }));
372
+ }), () => ({ rows: framed({ documents }, window), keys, ...window }));
283
373
  }
284
- const bounded = shape.orderBy.length === 0 && plan.limit !== null ? entries.slice(0, plan.limit) : entries;
285
374
  const groups = [];
286
- return after(inUnits(bounded, ([, row]) => {
375
+ return after(inUnits(kept, ([, row]) => {
287
376
  groups.push(each({
288
377
  key: shape.groupBy.map((_, index) => row[`key${index}`]),
289
378
  count: row.count,
290
379
  }));
291
380
  return ROW_WEIGHT;
292
- }), () => ({ rows: { groups }, keys: bounded.map(([key]) => key) }));
381
+ }), () => ({ rows: framed({ groups }, window), keys, ...window }));
293
382
  }
294
383
  export function reconciled(previous, next) {
295
384
  const before = previous.result;
@@ -314,13 +403,16 @@ export function reconciled(previous, next) {
314
403
  const removed = previous.keys.filter((key) => !present.has(key));
315
404
  const moved = reordered(previous.keys, next.keys, present);
316
405
  const covered = !deepEquals(before.coverage, after.coverage);
317
- if (changed.length === 0 && removed.length === 0 && !moved && !covered)
406
+ const framing = reframed(before, after);
407
+ if (changed.length === 0 && removed.length === 0 && !moved && !covered && framing === null) {
318
408
  return null;
409
+ }
410
+ const window = { next: after.next, total: after.total };
319
411
  return {
320
412
  keyed: {
321
413
  result: 'groups' in after
322
- ? { groups: rows, coverage: after.coverage }
323
- : { documents: rows, coverage: after.coverage },
414
+ ? framed({ groups: rows, coverage: after.coverage }, window)
415
+ : framed({ documents: rows, coverage: after.coverage }, window),
324
416
  keys: next.keys,
325
417
  },
326
418
  message: {
@@ -328,9 +420,17 @@ export function reconciled(previous, next) {
328
420
  removed,
329
421
  order: moved ? next.keys : null,
330
422
  ...(covered ? { coverage: after.coverage } : {}),
423
+ ...framing,
331
424
  },
332
425
  };
333
426
  }
427
+ function reframed(before, after) {
428
+ const next = before.next === after.next ? {} : { next: after.next ?? null };
429
+ const total = before.total === after.total || after.total === undefined ? {} : { total: after.total };
430
+ return Object.keys(next).length === 0 && Object.keys(total).length === 0
431
+ ? null
432
+ : { ...next, ...total };
433
+ }
334
434
  function reordered(previous, next, present) {
335
435
  let at = 0;
336
436
  for (const key of previous) {
@@ -360,6 +460,9 @@ export function createReadSide() {
360
460
  },
361
461
  });
362
462
  const shared = new Map();
463
+ let writing = false;
464
+ let publishing = null;
465
+ let closed = false;
363
466
  function searched(row) {
364
467
  const read = row.text === null ? null : parse(row.text, row.path);
365
468
  if (read === null || !read.ok)
@@ -379,8 +482,11 @@ export function createReadSide() {
379
482
  return;
380
483
  entry.rows.set(key, row);
381
484
  }
485
+ function counted(entry) {
486
+ return entry.built.count === null ? undefined : entry.built.count.size;
487
+ }
382
488
  function rebuilt(entry) {
383
- return after(rowsOfPlan(entry.plan, [...entry.built.live.entries()], entry.bodies), ({ rows: found, keys }) => {
489
+ return after(rowsOfPlan(entry.plan, [...entry.built.live.entries()], counted(entry), entry.bodies), ({ rows: found, keys, next, total }) => {
384
490
  const present = new Set(keys);
385
491
  for (const key of [...entry.rows.keys()])
386
492
  if (!present.has(key))
@@ -390,32 +496,34 @@ export function createReadSide() {
390
496
  entry.bodies.delete(key);
391
497
  }
392
498
  keys.forEach((key, index) => kept(entry, key, rowAt(found, index)));
393
- return keys;
499
+ return { keys, next, total };
394
500
  });
395
501
  }
396
502
  function refreshed(entry, dirty) {
397
503
  const shape = entry.plan.shape;
398
- const keys = [...entry.built.live.keys()];
399
- const named = new Set([...dirty, ...keys.filter((key) => !entry.rows.has(key))]);
400
- const values = [...named].map((key) => [key, entry.built.live.get(key)]);
504
+ const { kept: inWindow, next, total, } = windowOf(entry.plan, [...entry.built.live.entries()], counted(entry));
505
+ const keys = inWindow.map(([key]) => key);
506
+ const present = new Set(keys);
507
+ for (const key of [...entry.rows.keys()]) {
508
+ if (present.has(key))
509
+ continue;
510
+ entry.rows.delete(key);
511
+ entry.bodies.delete(key);
512
+ }
513
+ const values = inWindow.filter(([key]) => dirty.has(key) || !entry.rows.has(key));
401
514
  const done = inUnits(values, ([key, value]) => {
402
- if (value !== undefined)
403
- kept(entry, key, documentOf(key, value, shape, entry.bodies));
404
- else {
405
- entry.rows.delete(key);
406
- entry.bodies.delete(key);
407
- }
515
+ kept(entry, key, documentOf(key, value, shape, entry.bodies));
408
516
  return weight(value);
409
517
  });
410
- return after(done, () => keys);
518
+ return after(done, () => ({ keys, next, total }));
411
519
  }
412
- function current(entry, keys, coverage) {
413
- const found = keys.map((key) => entry.rows.get(key));
520
+ function current(entry, found, coverage) {
521
+ const rows = found.keys.map((key) => entry.rows.get(key));
414
522
  return {
415
523
  result: entry.plan.shape.kind === 'groups'
416
- ? { groups: found, coverage }
417
- : { documents: found, coverage },
418
- keys,
524
+ ? framed({ groups: rows, coverage }, found)
525
+ : framed({ documents: rows, coverage }, found),
526
+ keys: found.keys,
419
527
  };
420
528
  }
421
529
  function delivered(entry, next) {
@@ -451,25 +559,26 @@ export function createReadSide() {
451
559
  const dirty = entry.dirty;
452
560
  entry.dirty = new Set();
453
561
  entry.whole = false;
454
- const keys = whole ? rebuilt(entry) : refreshed(entry, dirty);
455
- if (keys instanceof Promise) {
456
- void keys
457
- .then((found) => {
562
+ const found = whole ? rebuilt(entry) : refreshed(entry, dirty);
563
+ if (found instanceof Promise) {
564
+ void found
565
+ .then((window) => {
458
566
  if (entry.listeners.size > 0)
459
- delivered(entry, current(entry, found, cover.coverage));
460
- run(entry);
567
+ delivered(entry, current(entry, window, cover.coverage));
568
+ entry.running = false;
569
+ wake(entry);
461
570
  })
462
571
  .catch(() => {
463
572
  entry.running = false;
464
573
  });
465
574
  return;
466
575
  }
467
- delivered(entry, current(entry, keys, cover.coverage));
576
+ delivered(entry, current(entry, found, cover.coverage));
468
577
  }
469
578
  entry.running = false;
470
579
  }
471
580
  function wake(entry) {
472
- if (entry.subscription === null || entry.running)
581
+ if (entry.subscription === null || entry.running || writing || publishing !== null)
473
582
  return;
474
583
  entry.running = true;
475
584
  try {
@@ -495,37 +604,92 @@ export function createReadSide() {
495
604
  wake(entry);
496
605
  }
497
606
  }
607
+ function committed(next) {
608
+ if (closed || next.size === 0)
609
+ return;
610
+ sync.begin();
611
+ for (const [key, row] of next) {
612
+ if (row === null) {
613
+ if (rows.has(key))
614
+ sync.write({ type: 'delete', key });
615
+ continue;
616
+ }
617
+ sync.write({ type: rows.has(key) ? 'update' : 'insert', value: row });
618
+ }
619
+ sync.commit();
620
+ }
621
+ function published(set) {
622
+ let unit = new Map();
623
+ return inUnits(set.documents, (change) => {
624
+ if (closed)
625
+ return 0;
626
+ const shifted = rows.size / HELD_PER_WEIGHT;
627
+ let spent = 0;
628
+ for (const row of change.before) {
629
+ unit.set(row.key, null);
630
+ spent += ROW_WEIGHT + shifted;
631
+ }
632
+ for (const row of change.after) {
633
+ unit.set(row.key, row);
634
+ spent += weight(row) + shifted;
635
+ }
636
+ return spent;
637
+ }, () => {
638
+ committed(unit);
639
+ unit = new Map();
640
+ });
641
+ }
642
+ function released() {
643
+ recover();
644
+ for (const entry of shared.values())
645
+ wake(entry);
646
+ }
647
+ function publishedLater(applied) {
648
+ const fence = applied.then(() => { }, () => { });
649
+ publishing = fence;
650
+ void fence.then(() => {
651
+ if (publishing !== fence)
652
+ return;
653
+ publishing = null;
654
+ released();
655
+ });
656
+ return applied;
657
+ }
658
+ async function settled() {
659
+ while (publishing !== null)
660
+ await publishing;
661
+ }
498
662
  return {
499
- apply(set) {
500
- const next = new Map();
501
- for (const change of set.documents) {
502
- for (const row of change.before)
503
- next.set(row.key, null);
504
- for (const row of change.after)
505
- next.set(row.key, row);
663
+ apply(set, next = () => { }) {
664
+ const pending = publishing;
665
+ if (pending !== null) {
666
+ return publishedLater(pending.then(() => after(published(set), next)));
506
667
  }
507
- if (next.size > 0) {
508
- sync.begin();
509
- for (const [key, row] of next) {
510
- if (row === null) {
511
- if (rows.has(key))
512
- sync.write({ type: 'delete', key });
513
- continue;
514
- }
515
- sync.write({ type: rows.has(key) ? 'update' : 'insert', value: row });
516
- }
517
- sync.commit();
668
+ writing = true;
669
+ let applied;
670
+ try {
671
+ applied = published(set);
672
+ }
673
+ finally {
674
+ writing = false;
518
675
  }
519
- recover();
676
+ if (applied instanceof Promise)
677
+ return publishedLater(applied.then(next));
678
+ released();
679
+ next();
520
680
  },
521
681
  recover,
682
+ settled,
522
683
  async query(plan, at) {
523
- const { live: collection, dispose } = built(rows, plan);
684
+ const { live: collection, count, dispose } = built(rows, plan);
524
685
  try {
525
686
  await collection.toArrayWhenReady();
687
+ if (count !== null)
688
+ await count.toArrayWhenReady();
689
+ await settled();
526
690
  const entries = [...collection.entries()];
527
691
  const held = at();
528
- const found = await rowsOfPlan(plan, entries, null, structuredClone);
692
+ const found = await rowsOfPlan(plan, entries, count?.size, null, structuredClone);
529
693
  return { rows: found.rows, at: held };
530
694
  }
531
695
  finally {
@@ -546,13 +710,16 @@ export function createReadSide() {
546
710
  fresh: new Set(),
547
711
  ready: new Set(),
548
712
  subscription: null,
713
+ counting: null,
549
714
  whole: true,
550
715
  dirty: new Set(),
551
716
  stale: false,
552
717
  running: false,
553
718
  };
554
719
  created.built.live.on('status:change', ({ status }) => {
555
- if (status === 'cleaned-up' && created.listeners.size === 0)
720
+ if (status !== 'cleaned-up' || created.listeners.size > 0)
721
+ return;
722
+ if (shared.get(identity) === created)
556
723
  shared.delete(identity);
557
724
  });
558
725
  shared.set(identity, created);
@@ -560,7 +727,14 @@ export function createReadSide() {
560
727
  }
561
728
  const observed = entry;
562
729
  observed.listeners.add(deliverTo);
563
- observed.built.live.onFirstReady(() => {
730
+ const ready = (then) => observed.built.live.onFirstReady(() => observed.built.count === null ? then() : observed.built.count.onFirstReady(then));
731
+ const settled = () => {
732
+ if (observed.built.count === null)
733
+ wake(observed);
734
+ else
735
+ queueMicrotask(() => wake(observed));
736
+ };
737
+ ready(() => {
564
738
  if (!observed.listeners.has(deliverTo))
565
739
  return;
566
740
  observed.fresh.add(deliverTo);
@@ -572,8 +746,13 @@ export function createReadSide() {
572
746
  else
573
747
  for (const { key } of changes)
574
748
  observed.dirty.add(key);
575
- wake(observed);
749
+ settled();
576
750
  }, { includeInitialState: false });
751
+ observed.counting =
752
+ observed.built.count?.subscribeChanges(() => {
753
+ observed.stale = true;
754
+ settled();
755
+ }, { includeInitialState: false }) ?? null;
577
756
  }
578
757
  wake(observed);
579
758
  });
@@ -584,11 +763,16 @@ export function createReadSide() {
584
763
  return;
585
764
  observed.subscription?.unsubscribe();
586
765
  observed.subscription = null;
766
+ observed.counting?.unsubscribe();
767
+ observed.counting = null;
587
768
  observed.result = undefined;
588
769
  observed.rows.clear();
589
770
  observed.bodies.clear();
590
771
  observed.dirty.clear();
591
772
  observed.whole = true;
773
+ if (shared.get(identity) === observed)
774
+ shared.delete(identity);
775
+ observed.built.dispose();
592
776
  };
593
777
  },
594
778
  room(room) {
@@ -599,11 +783,13 @@ export function createReadSide() {
599
783
  return row === undefined || row.type === null ? null : searched(row);
600
784
  },
601
785
  close() {
786
+ closed = true;
602
787
  for (const entry of shared.values()) {
603
788
  entry.listeners.clear();
604
789
  entry.fresh.clear();
605
790
  entry.ready.clear();
606
791
  entry.subscription?.unsubscribe();
792
+ entry.counting?.unsubscribe();
607
793
  entry.built.dispose();
608
794
  }
609
795
  shared.clear();
@@ -628,13 +814,16 @@ function changedFrom(previous, next) {
628
814
  const removed = previous.keys.filter((key) => !present.has(key));
629
815
  const moved = reordered(previous.keys, next.keys, present);
630
816
  const covered = !deepEquals(before.coverage, after.coverage);
631
- if (changed.length === 0 && removed.length === 0 && !moved && !covered)
817
+ const framing = reframed(before, after);
818
+ if (changed.length === 0 && removed.length === 0 && !moved && !covered && framing === null) {
632
819
  return null;
820
+ }
633
821
  return {
634
822
  changed,
635
823
  removed,
636
824
  order: moved ? next.keys : null,
637
825
  ...(covered ? { coverage: after.coverage } : {}),
826
+ ...framing,
638
827
  };
639
828
  }
640
829
  function rowAt(rows, index) {
@@ -0,0 +1,11 @@
1
+ import type { Adapter } from './adapter.js';
2
+ import type { Reads } from './bootstrap.js';
3
+ export type DoorReads = Reads & {
4
+ carried(locator: string): number;
5
+ finished(locator: string, turn: number): void;
6
+ opened(): void;
7
+ applied(): void;
8
+ lost(): void;
9
+ };
10
+ export declare function doorReads(adapter: Adapter, declaration: (locator: string) => unknown, generation: () => number): DoorReads;
11
+ //# sourceMappingURL=reads.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reads.d.ts","sourceRoot":"","sources":["../src/reads.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,OAAO,EAOR,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAE5C,MAAM,MAAM,SAAS,GAAG,KAAK,GAAG;IAC9B,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;IACjC,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9C,MAAM,IAAI,IAAI,CAAC;IACf,OAAO,IAAI,IAAI,CAAC;IAChB,IAAI,IAAI,IAAI,CAAC;CACd,CAAC;AAiCF,wBAAgB,SAAS,CACvB,OAAO,EAAE,OAAO,EAChB,WAAW,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,EACzC,UAAU,EAAE,MAAM,MAAM,GACvB,SAAS,CA0FX"}