@agentshouse/mdruntime 0.1.0-c93103f5357ff → 0.1.0-c94cfdbdb6f68

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 (90) hide show
  1. package/AGENTS.md +194 -194
  2. package/README.md +8 -8
  3. package/dist/adapter.d.ts +20 -6
  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/chat/earlier.d.ts +10 -0
  8. package/dist/chat/earlier.d.ts.map +1 -0
  9. package/dist/chat/earlier.js +54 -0
  10. package/dist/chat/list.d.ts.map +1 -1
  11. package/dist/chat/list.js +39 -3
  12. package/dist/chat/provider.d.ts.map +1 -1
  13. package/dist/chat/provider.js +11 -12
  14. package/dist/chat/question.d.ts +1 -1
  15. package/dist/chat/question.d.ts.map +1 -1
  16. package/dist/chat/question.js +7 -18
  17. package/dist/chat/style.js +1 -1
  18. package/dist/client.d.ts +6 -2
  19. package/dist/client.d.ts.map +1 -1
  20. package/dist/client.js +7 -3
  21. package/dist/collection.d.ts.map +1 -1
  22. package/dist/collection.js +64 -34
  23. package/dist/connection.d.ts +9 -4
  24. package/dist/connection.d.ts.map +1 -1
  25. package/dist/connection.js +312 -154
  26. package/dist/conversation.d.ts +6 -0
  27. package/dist/conversation.d.ts.map +1 -1
  28. package/dist/conversation.js +161 -41
  29. package/dist/entry.d.ts.map +1 -1
  30. package/dist/entry.js +2 -1
  31. package/dist/host.d.ts +0 -3
  32. package/dist/host.d.ts.map +1 -1
  33. package/dist/host.js +8 -3
  34. package/dist/index.d.ts +3 -3
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/memory.d.ts +6 -3
  37. package/dist/memory.d.ts.map +1 -1
  38. package/dist/memory.js +17 -11
  39. package/dist/observation.d.ts +2 -0
  40. package/dist/observation.d.ts.map +1 -1
  41. package/dist/observation.js +9 -3
  42. package/dist/outcome.d.ts +8 -2
  43. package/dist/outcome.d.ts.map +1 -1
  44. package/dist/outcome.js +3 -0
  45. package/dist/query.d.ts +21 -1
  46. package/dist/query.d.ts.map +1 -1
  47. package/dist/query.js +28 -5
  48. package/dist/reach.d.ts.map +1 -1
  49. package/dist/reach.js +13 -0
  50. package/dist/react.d.ts +1 -2
  51. package/dist/react.d.ts.map +1 -1
  52. package/dist/react.js +2 -14
  53. package/dist/read-side.d.ts +3 -1
  54. package/dist/read-side.d.ts.map +1 -1
  55. package/dist/read-side.js +251 -69
  56. package/dist/reads.d.ts +11 -0
  57. package/dist/reads.d.ts.map +1 -0
  58. package/dist/reads.js +98 -0
  59. package/dist/refresh.d.ts +4 -4
  60. package/dist/refresh.d.ts.map +1 -1
  61. package/dist/replica.d.ts +5 -1
  62. package/dist/replica.d.ts.map +1 -1
  63. package/dist/replica.js +19 -3
  64. package/dist/rows.d.ts +1 -0
  65. package/dist/rows.d.ts.map +1 -1
  66. package/dist/rows.js +5 -0
  67. package/dist/save.d.ts +1 -0
  68. package/dist/save.d.ts.map +1 -1
  69. package/dist/save.js +22 -8
  70. package/dist/search.d.ts +15 -2
  71. package/dist/search.d.ts.map +1 -1
  72. package/dist/search.js +76 -17
  73. package/dist/seed.d.ts.map +1 -1
  74. package/dist/seed.js +18 -4
  75. package/dist/store.d.ts +16 -2
  76. package/dist/store.d.ts.map +1 -1
  77. package/dist/store.js +48 -14
  78. package/dist/turn.d.ts +3 -3
  79. package/dist/turn.d.ts.map +1 -1
  80. package/dist/turn.js +11 -5
  81. package/dist/windows.d.ts +26 -0
  82. package/dist/windows.d.ts.map +1 -0
  83. package/dist/windows.js +93 -0
  84. package/dist/wire.d.ts +16 -0
  85. package/dist/wire.d.ts.map +1 -1
  86. package/dist/wire.js +4 -0
  87. package/package.json +2 -2
  88. package/dist/acquisition.d.ts +0 -14
  89. package/dist/acquisition.d.ts.map +0 -1
  90. package/dist/acquisition.js +0 -24
package/dist/read-side.js CHANGED
@@ -2,6 +2,7 @@ import { and, BasicIndex, configureIndexDevMode, count, createCollection, create
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);
@@ -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));
@@ -104,21 +107,51 @@ function filtered(q, rows, plan) {
104
107
  function bodied(shape) {
105
108
  return shape.sections !== null || shape.preamble || shape.markdown;
106
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
+ ]);
133
+ }
107
134
  function documents(q, rows, plan, shape) {
108
135
  let query = filtered(q, rows, plan);
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));
140
+ }
109
141
  for (const { key, options } of shape.orderBy) {
110
142
  query = query.orderBy(({ d }) => keyed(key, d), options);
111
143
  }
112
- if (shape.orderBy.length === 0 && plan.limit !== null) {
144
+ if (shape.orderBy.length > 0 || plan.limit !== null || plan.after !== null) {
113
145
  query = query.orderBy(({ d }) => d.key, { stringSort: 'lexical' });
114
146
  }
115
147
  if (plan.limit !== null)
116
- query = query.limit(plan.limit);
148
+ query = query.limit(plan.limit + 1);
117
149
  return query.select(({ d }) => ({
118
150
  room: d.room,
119
151
  path: d.path,
120
152
  type: d.type,
121
153
  binary: d.binary,
154
+ ...Object.fromEntries(shape.orderBy.map(({ key }, index) => [`order${index}`, keyed(key, d)])),
122
155
  ...(shape.fields === null ? {} : { fields: d.fields }),
123
156
  ...(bodied(shape) ? { text: d.text } : {}),
124
157
  ...(shape.backlinks ? { backlinks: toArray(backlinked(q, rows, d, plan.reach)) } : {}),
@@ -134,15 +167,13 @@ function backlinked(q, rows, document, reach) {
134
167
  .orderBy(({ r }) => r.field, { stringSort: 'lexical' })
135
168
  .select(({ r }) => ({ path: r.path, type: r.type, field: r.field }));
136
169
  }
137
- function groups(q, stage, shape, limit) {
170
+ function groups(q, stage, shape) {
138
171
  const keys = shape.groupBy.map((_, index) => `key${index}`);
139
172
  const from = q.from({ m: stage });
140
173
  let query = from.groupBy(({ m }) => keys.map((key) => m[key]));
141
174
  for (const { by, direction } of shape.orderBy) {
142
175
  query = query.orderBy(({ m }) => (by === 'count' ? count(m.key) : m[keys[by]]), direction);
143
176
  }
144
- if (limit !== null && shape.orderBy.length > 0)
145
- query = query.limit(limit);
146
177
  return query.select(({ m }) => ({
147
178
  ...Object.fromEntries(keys.map((key) => [key, m[key]])),
148
179
  count: count(m.key),
@@ -152,15 +183,27 @@ function built(rows, plan) {
152
183
  const shape = plan.shape;
153
184
  if (shape.kind === 'documents') {
154
185
  const collection = live((q) => documents(q, rows, plan, shape));
155
- 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
+ };
156
198
  }
157
199
  const stage = live((q) => filtered(q, rows, plan).select(({ d }) => ({
158
200
  key: d.key,
159
201
  ...Object.fromEntries(shape.groupBy.map((key, index) => [`key${index}`, keyed(key, d)])),
160
202
  })));
161
- const collection = live((q) => groups(q, stage, shape, plan.limit));
203
+ const collection = live((q) => groups(q, stage, shape));
162
204
  return {
163
205
  live: collection,
206
+ count: null,
164
207
  dispose: () => {
165
208
  void collection.cleanup();
166
209
  void stage.cleanup();
@@ -262,7 +305,9 @@ function documentOf(key, row, shape, bodies) {
262
305
  ...(shape.markdown && body !== null ? { markdown: body.content } : {}),
263
306
  ...(shape.backlinks
264
307
  ? {
265
- 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 })),
266
311
  }
267
312
  : {}),
268
313
  ...(shape.change ? { change: row.change } : {}),
@@ -272,24 +317,62 @@ function documentOf(key, row, shape, bodies) {
272
317
  function weight(row) {
273
318
  return ROW_WEIGHT + (typeof row?.text === 'string' ? row.text.length : 0);
274
319
  }
275
- function rowsOfPlan(plan, entries, bodies, each = (row) => row) {
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) {
276
333
  const shape = plan.shape;
334
+ if (shape.kind === 'documents') {
335
+ return {
336
+ ...windowed(entries, plan.limit, plan.after?.text, ([, row]) => documentCursor(shape, row)),
337
+ total,
338
+ };
339
+ }
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 }) {
351
+ return {
352
+ ...rows,
353
+ ...(next === undefined ? {} : { next }),
354
+ ...(total === undefined ? {} : { total }),
355
+ };
356
+ }
357
+ function rowsOfPlan(plan, entries, total, bodies, each = (row) => row) {
358
+ const shape = plan.shape;
359
+ const { kept, ...window } = windowOf(plan, entries, total);
360
+ const keys = kept.map(([key]) => key);
277
361
  if (shape.kind === 'documents') {
278
362
  const documents = [];
279
- return after(inUnits(entries, ([key, row]) => {
363
+ return after(inUnits(kept, ([key, row]) => {
280
364
  documents.push(each(documentOf(key, row, shape, bodies)));
281
365
  return weight(row);
282
- }), () => ({ rows: { documents }, keys: entries.map(([key]) => key) }));
366
+ }), () => ({ rows: framed({ documents }, window), keys, ...window }));
283
367
  }
284
- const bounded = shape.orderBy.length === 0 && plan.limit !== null ? entries.slice(0, plan.limit) : entries;
285
368
  const groups = [];
286
- return after(inUnits(bounded, ([, row]) => {
369
+ return after(inUnits(kept, ([, row]) => {
287
370
  groups.push(each({
288
371
  key: shape.groupBy.map((_, index) => row[`key${index}`]),
289
372
  count: row.count,
290
373
  }));
291
374
  return ROW_WEIGHT;
292
- }), () => ({ rows: { groups }, keys: bounded.map(([key]) => key) }));
375
+ }), () => ({ rows: framed({ groups }, window), keys, ...window }));
293
376
  }
294
377
  export function reconciled(previous, next) {
295
378
  const before = previous.result;
@@ -314,13 +397,16 @@ export function reconciled(previous, next) {
314
397
  const removed = previous.keys.filter((key) => !present.has(key));
315
398
  const moved = reordered(previous.keys, next.keys, present);
316
399
  const covered = !deepEquals(before.coverage, after.coverage);
317
- if (changed.length === 0 && removed.length === 0 && !moved && !covered)
400
+ const framing = reframed(before, after);
401
+ if (changed.length === 0 && removed.length === 0 && !moved && !covered && framing === null) {
318
402
  return null;
403
+ }
404
+ const window = { next: after.next, total: after.total };
319
405
  return {
320
406
  keyed: {
321
407
  result: 'groups' in after
322
- ? { groups: rows, coverage: after.coverage }
323
- : { documents: rows, coverage: after.coverage },
408
+ ? framed({ groups: rows, coverage: after.coverage }, window)
409
+ : framed({ documents: rows, coverage: after.coverage }, window),
324
410
  keys: next.keys,
325
411
  },
326
412
  message: {
@@ -328,9 +414,17 @@ export function reconciled(previous, next) {
328
414
  removed,
329
415
  order: moved ? next.keys : null,
330
416
  ...(covered ? { coverage: after.coverage } : {}),
417
+ ...framing,
331
418
  },
332
419
  };
333
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
+ }
334
428
  function reordered(previous, next, present) {
335
429
  let at = 0;
336
430
  for (const key of previous) {
@@ -360,6 +454,9 @@ export function createReadSide() {
360
454
  },
361
455
  });
362
456
  const shared = new Map();
457
+ let writing = false;
458
+ let publishing = null;
459
+ let closed = false;
363
460
  function searched(row) {
364
461
  const read = row.text === null ? null : parse(row.text, row.path);
365
462
  if (read === null || !read.ok)
@@ -379,8 +476,11 @@ export function createReadSide() {
379
476
  return;
380
477
  entry.rows.set(key, row);
381
478
  }
479
+ function counted(entry) {
480
+ return entry.built.count === null ? undefined : entry.built.count.size;
481
+ }
382
482
  function rebuilt(entry) {
383
- return after(rowsOfPlan(entry.plan, [...entry.built.live.entries()], entry.bodies), ({ rows: found, keys }) => {
483
+ return after(rowsOfPlan(entry.plan, [...entry.built.live.entries()], counted(entry), entry.bodies), ({ rows: found, keys, next, total }) => {
384
484
  const present = new Set(keys);
385
485
  for (const key of [...entry.rows.keys()])
386
486
  if (!present.has(key))
@@ -390,32 +490,34 @@ export function createReadSide() {
390
490
  entry.bodies.delete(key);
391
491
  }
392
492
  keys.forEach((key, index) => kept(entry, key, rowAt(found, index)));
393
- return keys;
493
+ return { keys, next, total };
394
494
  });
395
495
  }
396
496
  function refreshed(entry, dirty) {
397
497
  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)]);
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));
401
508
  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
- }
509
+ kept(entry, key, documentOf(key, value, shape, entry.bodies));
408
510
  return weight(value);
409
511
  });
410
- return after(done, () => keys);
512
+ return after(done, () => ({ keys, next, total }));
411
513
  }
412
- function current(entry, keys, coverage) {
413
- const found = keys.map((key) => entry.rows.get(key));
514
+ function current(entry, found, coverage) {
515
+ const rows = found.keys.map((key) => entry.rows.get(key));
414
516
  return {
415
517
  result: entry.plan.shape.kind === 'groups'
416
- ? { groups: found, coverage }
417
- : { documents: found, coverage },
418
- keys,
518
+ ? framed({ groups: rows, coverage }, found)
519
+ : framed({ documents: rows, coverage }, found),
520
+ keys: found.keys,
419
521
  };
420
522
  }
421
523
  function delivered(entry, next) {
@@ -451,25 +553,26 @@ export function createReadSide() {
451
553
  const dirty = entry.dirty;
452
554
  entry.dirty = new Set();
453
555
  entry.whole = false;
454
- const keys = whole ? rebuilt(entry) : refreshed(entry, dirty);
455
- if (keys instanceof Promise) {
456
- void keys
457
- .then((found) => {
556
+ const found = whole ? rebuilt(entry) : refreshed(entry, dirty);
557
+ if (found instanceof Promise) {
558
+ void found
559
+ .then((window) => {
458
560
  if (entry.listeners.size > 0)
459
- delivered(entry, current(entry, found, cover.coverage));
460
- run(entry);
561
+ delivered(entry, current(entry, window, cover.coverage));
562
+ entry.running = false;
563
+ wake(entry);
461
564
  })
462
565
  .catch(() => {
463
566
  entry.running = false;
464
567
  });
465
568
  return;
466
569
  }
467
- delivered(entry, current(entry, keys, cover.coverage));
570
+ delivered(entry, current(entry, found, cover.coverage));
468
571
  }
469
572
  entry.running = false;
470
573
  }
471
574
  function wake(entry) {
472
- if (entry.subscription === null || entry.running)
575
+ if (entry.subscription === null || entry.running || writing || publishing !== null)
473
576
  return;
474
577
  entry.running = true;
475
578
  try {
@@ -495,37 +598,91 @@ export function createReadSide() {
495
598
  wake(entry);
496
599
  }
497
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
+ }
498
655
  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);
656
+ apply(set, next = () => { }) {
657
+ const pending = publishing;
658
+ if (pending !== null) {
659
+ return publishedLater(pending.then(() => after(published(set), next)));
506
660
  }
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();
661
+ writing = true;
662
+ let applied;
663
+ try {
664
+ applied = published(set);
665
+ }
666
+ finally {
667
+ writing = false;
518
668
  }
519
- recover();
669
+ if (applied instanceof Promise)
670
+ return publishedLater(applied.then(next));
671
+ released();
672
+ next();
520
673
  },
521
674
  recover,
675
+ settled,
522
676
  async query(plan, at) {
523
- const { live: collection, dispose } = built(rows, plan);
677
+ const { live: collection, count, dispose } = built(rows, plan);
524
678
  try {
525
679
  await collection.toArrayWhenReady();
680
+ if (count !== null)
681
+ await count.toArrayWhenReady();
682
+ await settled();
526
683
  const entries = [...collection.entries()];
527
684
  const held = at();
528
- const found = await rowsOfPlan(plan, entries, null, structuredClone);
685
+ const found = await rowsOfPlan(plan, entries, count?.size, null, structuredClone);
529
686
  return { rows: found.rows, at: held };
530
687
  }
531
688
  finally {
@@ -546,13 +703,16 @@ export function createReadSide() {
546
703
  fresh: new Set(),
547
704
  ready: new Set(),
548
705
  subscription: null,
706
+ counting: null,
549
707
  whole: true,
550
708
  dirty: new Set(),
551
709
  stale: false,
552
710
  running: false,
553
711
  };
554
712
  created.built.live.on('status:change', ({ status }) => {
555
- 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)
556
716
  shared.delete(identity);
557
717
  });
558
718
  shared.set(identity, created);
@@ -560,7 +720,14 @@ export function createReadSide() {
560
720
  }
561
721
  const observed = entry;
562
722
  observed.listeners.add(deliverTo);
563
- observed.built.live.onFirstReady(() => {
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(() => {
564
731
  if (!observed.listeners.has(deliverTo))
565
732
  return;
566
733
  observed.fresh.add(deliverTo);
@@ -572,8 +739,13 @@ export function createReadSide() {
572
739
  else
573
740
  for (const { key } of changes)
574
741
  observed.dirty.add(key);
575
- wake(observed);
742
+ settled();
576
743
  }, { includeInitialState: false });
744
+ observed.counting =
745
+ observed.built.count?.subscribeChanges(() => {
746
+ observed.stale = true;
747
+ settled();
748
+ }, { includeInitialState: false }) ?? null;
577
749
  }
578
750
  wake(observed);
579
751
  });
@@ -584,11 +756,16 @@ export function createReadSide() {
584
756
  return;
585
757
  observed.subscription?.unsubscribe();
586
758
  observed.subscription = null;
759
+ observed.counting?.unsubscribe();
760
+ observed.counting = null;
587
761
  observed.result = undefined;
588
762
  observed.rows.clear();
589
763
  observed.bodies.clear();
590
764
  observed.dirty.clear();
591
765
  observed.whole = true;
766
+ if (shared.get(identity) === observed)
767
+ shared.delete(identity);
768
+ observed.built.dispose();
592
769
  };
593
770
  },
594
771
  room(room) {
@@ -599,11 +776,13 @@ export function createReadSide() {
599
776
  return row === undefined || row.type === null ? null : searched(row);
600
777
  },
601
778
  close() {
779
+ closed = true;
602
780
  for (const entry of shared.values()) {
603
781
  entry.listeners.clear();
604
782
  entry.fresh.clear();
605
783
  entry.ready.clear();
606
784
  entry.subscription?.unsubscribe();
785
+ entry.counting?.unsubscribe();
607
786
  entry.built.dispose();
608
787
  }
609
788
  shared.clear();
@@ -628,13 +807,16 @@ function changedFrom(previous, next) {
628
807
  const removed = previous.keys.filter((key) => !present.has(key));
629
808
  const moved = reordered(previous.keys, next.keys, present);
630
809
  const covered = !deepEquals(before.coverage, after.coverage);
631
- if (changed.length === 0 && removed.length === 0 && !moved && !covered)
810
+ const framing = reframed(before, after);
811
+ if (changed.length === 0 && removed.length === 0 && !moved && !covered && framing === null) {
632
812
  return null;
813
+ }
633
814
  return {
634
815
  changed,
635
816
  removed,
636
817
  order: moved ? next.keys : null,
637
818
  ...(covered ? { coverage: after.coverage } : {}),
819
+ ...framing,
638
820
  };
639
821
  }
640
822
  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"}
package/dist/reads.js ADDED
@@ -0,0 +1,98 @@
1
+ function declaredAs(declaration) {
2
+ if (declaration === null || typeof declaration !== 'object')
3
+ return declaration;
4
+ const { roomVersion: _version, ...declared } = declaration;
5
+ return declared;
6
+ }
7
+ function sameHeld(one, other) {
8
+ return one?.documents === other?.documents && one?.revisions === other?.revisions;
9
+ }
10
+ function failed(answer) {
11
+ return answer?.kind === 'transport-uncertainty';
12
+ }
13
+ function continues(answer) {
14
+ const page = answer;
15
+ return page?.kind === 'net-advancement' && page.pageToken !== undefined;
16
+ }
17
+ export function doorReads(adapter, declaration, generation) {
18
+ const reading = new Map();
19
+ const taken = new Map();
20
+ let carrying = false;
21
+ function joined(read, authorization, request, ask) {
22
+ const key = `${read} ${request.locator} ${request.pageToken ?? ''}`;
23
+ const { held, ...named } = request;
24
+ const asked = JSON.stringify([declaredAs(declaration(request.locator)), named]);
25
+ const asking = generation();
26
+ const current = reading.get(key);
27
+ if (current !== undefined &&
28
+ current.authorization === authorization &&
29
+ current.asked === asked &&
30
+ sameHeld(current.held, held) &&
31
+ (!current.settled || current.holder < asking)) {
32
+ if (current.settled && !current.continued)
33
+ reading.delete(key);
34
+ else
35
+ current.holder = Math.max(current.holder, asking);
36
+ if (current.from === asking)
37
+ return current.answer;
38
+ const again = () => joined(read, authorization, request, ask);
39
+ return current.answer.then((answer) => {
40
+ if (failed(answer))
41
+ return again();
42
+ taken.set(request.locator, (taken.get(request.locator) ?? 0) + 1);
43
+ return answer;
44
+ }, again);
45
+ }
46
+ const answer = ask();
47
+ const sent = {
48
+ locator: request.locator,
49
+ authorization,
50
+ asked,
51
+ held,
52
+ answer,
53
+ from: asking,
54
+ holder: asking,
55
+ settled: false,
56
+ continued: false,
57
+ };
58
+ reading.set(key, sent);
59
+ const settled = (result, kept) => {
60
+ sent.settled = true;
61
+ sent.continued = kept && read === 'refresh' && continues(result);
62
+ if (reading.get(key) !== sent)
63
+ return;
64
+ const live = sent.holder === generation();
65
+ if (!kept || (live ? !sent.continued : !carrying))
66
+ reading.delete(key);
67
+ };
68
+ answer.then((result) => settled(result, !failed(result)), () => settled(null, false));
69
+ return answer;
70
+ }
71
+ return {
72
+ bootstrap: (authorization, request) => joined('bootstrap', authorization, request, () => adapter.bootstrap(authorization, request)),
73
+ refresh: (authorization, request) => joined('refresh', authorization, request, () => adapter.refresh(authorization, request)),
74
+ reached: (locator, roomVersion, target) => adapter.reached(locator, roomVersion, target),
75
+ carried: (locator) => taken.get(locator) ?? 0,
76
+ finished(locator, turn) {
77
+ for (const [key, held] of reading) {
78
+ if (held.locator !== locator || !held.settled || held.holder !== turn)
79
+ continue;
80
+ if (turn === generation() || !carrying)
81
+ reading.delete(key);
82
+ }
83
+ },
84
+ opened() {
85
+ carrying = true;
86
+ },
87
+ applied() {
88
+ carrying = false;
89
+ for (const [key, held] of reading) {
90
+ if (held.settled && held.holder !== generation())
91
+ reading.delete(key);
92
+ }
93
+ },
94
+ lost() {
95
+ reading.clear();
96
+ },
97
+ };
98
+ }