@agentshouse/mdruntime 0.1.0-cb0eb0d87ff49 → 0.1.0-cb2e390ee7b89

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 (117) hide show
  1. package/AGENTS.md +275 -285
  2. package/dist/acquisition.d.ts +14 -0
  3. package/dist/acquisition.d.ts.map +1 -0
  4. package/dist/acquisition.js +24 -0
  5. package/dist/adapter.d.ts +25 -7
  6. package/dist/adapter.d.ts.map +1 -1
  7. package/dist/bootstrap.d.ts +2 -1
  8. package/dist/bootstrap.d.ts.map +1 -1
  9. package/dist/bootstrap.js +4 -0
  10. package/dist/change-set.d.ts.map +1 -1
  11. package/dist/change-set.js +2 -1
  12. package/dist/chat/earlier.d.ts +10 -0
  13. package/dist/chat/earlier.d.ts.map +1 -0
  14. package/dist/chat/earlier.js +54 -0
  15. package/dist/chat/kit.d.ts +2 -1
  16. package/dist/chat/kit.d.ts.map +1 -1
  17. package/dist/chat/kit.js +3 -3
  18. package/dist/chat/list.d.ts +2 -1
  19. package/dist/chat/list.d.ts.map +1 -1
  20. package/dist/chat/list.js +42 -5
  21. package/dist/chat/messages.d.ts +1 -0
  22. package/dist/chat/messages.d.ts.map +1 -1
  23. package/dist/chat/messages.js +4 -0
  24. package/dist/chat/provider.d.ts +1 -1
  25. package/dist/chat/provider.d.ts.map +1 -1
  26. package/dist/chat/provider.js +34 -19
  27. package/dist/chat/question.d.ts +1 -1
  28. package/dist/chat/question.d.ts.map +1 -1
  29. package/dist/chat/question.js +7 -18
  30. package/dist/chat/ready.d.ts +1 -0
  31. package/dist/chat/ready.d.ts.map +1 -1
  32. package/dist/chat/ready.js +2 -2
  33. package/dist/chat/style.js +1 -1
  34. package/dist/client.d.ts +8 -2
  35. package/dist/client.d.ts.map +1 -1
  36. package/dist/client.js +16 -5
  37. package/dist/connection.d.ts +19 -3
  38. package/dist/connection.d.ts.map +1 -1
  39. package/dist/connection.js +741 -100
  40. package/dist/conversation.d.ts +7 -0
  41. package/dist/conversation.d.ts.map +1 -1
  42. package/dist/conversation.js +202 -39
  43. package/dist/durability.d.ts +7 -1
  44. package/dist/durability.d.ts.map +1 -1
  45. package/dist/entry.d.ts.map +1 -1
  46. package/dist/entry.js +1 -1
  47. package/dist/host.d.ts +15 -2
  48. package/dist/host.d.ts.map +1 -1
  49. package/dist/host.js +85 -8
  50. package/dist/index.d.ts +7 -5
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +1 -0
  53. package/dist/memory.d.ts +8 -2
  54. package/dist/memory.d.ts.map +1 -1
  55. package/dist/memory.js +26 -6
  56. package/dist/model.d.ts +3 -2
  57. package/dist/model.d.ts.map +1 -1
  58. package/dist/model.js +1 -1
  59. package/dist/observation.d.ts +2 -0
  60. package/dist/observation.d.ts.map +1 -1
  61. package/dist/observation.js +9 -3
  62. package/dist/outcome.d.ts +8 -2
  63. package/dist/outcome.d.ts.map +1 -1
  64. package/dist/outcome.js +3 -0
  65. package/dist/overlay.d.ts +1 -0
  66. package/dist/overlay.d.ts.map +1 -1
  67. package/dist/overlay.js +27 -3
  68. package/dist/query.d.ts +24 -1
  69. package/dist/query.d.ts.map +1 -1
  70. package/dist/query.js +32 -5
  71. package/dist/reach.d.ts +6 -3
  72. package/dist/reach.d.ts.map +1 -1
  73. package/dist/reach.js +18 -3
  74. package/dist/react.d.ts +4 -1
  75. package/dist/react.d.ts.map +1 -1
  76. package/dist/react.js +6 -4
  77. package/dist/read-side.d.ts.map +1 -1
  78. package/dist/read-side.js +169 -47
  79. package/dist/readiness.d.ts +53 -0
  80. package/dist/readiness.d.ts.map +1 -0
  81. package/dist/readiness.js +33 -0
  82. package/dist/reads.d.ts +11 -0
  83. package/dist/reads.d.ts.map +1 -0
  84. package/dist/reads.js +98 -0
  85. package/dist/refresh.d.ts +4 -4
  86. package/dist/refresh.d.ts.map +1 -1
  87. package/dist/refresh.js +6 -0
  88. package/dist/replica.d.ts +5 -1
  89. package/dist/replica.d.ts.map +1 -1
  90. package/dist/replica.js +30 -5
  91. package/dist/room-draft.d.ts +13 -1
  92. package/dist/room-draft.d.ts.map +1 -1
  93. package/dist/room-draft.js +2 -2
  94. package/dist/rows.d.ts +2 -1
  95. package/dist/rows.d.ts.map +1 -1
  96. package/dist/rows.js +3 -1
  97. package/dist/save.d.ts +1 -0
  98. package/dist/save.d.ts.map +1 -1
  99. package/dist/save.js +17 -1
  100. package/dist/search.d.ts +28 -7
  101. package/dist/search.d.ts.map +1 -1
  102. package/dist/search.js +93 -13
  103. package/dist/seed.d.ts +16 -0
  104. package/dist/seed.d.ts.map +1 -0
  105. package/dist/seed.js +75 -0
  106. package/dist/status.d.ts +4 -0
  107. package/dist/status.d.ts.map +1 -1
  108. package/dist/store.d.ts +26 -4
  109. package/dist/store.d.ts.map +1 -1
  110. package/dist/store.js +92 -25
  111. package/dist/windows.d.ts +18 -0
  112. package/dist/windows.d.ts.map +1 -0
  113. package/dist/windows.js +80 -0
  114. package/dist/wire.d.ts +144 -32
  115. package/dist/wire.d.ts.map +1 -1
  116. package/dist/wire.js +50 -31
  117. package/package.json +2 -2
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,11 @@ 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) {
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])]),
66
68
  ...reached(reach, row.room, row.key),
67
69
  ...(scope.types === null ? [] : [inArray(row.type, [...scope.types])]),
68
70
  ...(scope.pathPrefix === null ? [] : [inArray(scope.pathPrefix, row.directories)]),
@@ -90,7 +92,7 @@ function related(q, rows, scope, field, kept, reach) {
90
92
  }
91
93
  function filtered(q, rows, plan) {
92
94
  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');
95
+ let query = narrowed(from.where(({ d }) => scoped(plan.root, d, plan.rooms, plan.reach, plan.paths)), plan.root.where, 'd');
94
96
  if (plan.hop !== null) {
95
97
  const hop = related(q, rows, plan.hop, plan.hop.field, 'source', plan.reach);
96
98
  query = query.innerJoin({ h: hop }, ({ d, h }) => eq(d.key, h.key));
@@ -104,25 +106,56 @@ function filtered(q, rows, plan) {
104
106
  function bodied(shape) {
105
107
  return shape.sections !== null || shape.preamble || shape.markdown;
106
108
  }
109
+ function orderingOf(options) {
110
+ const collated = options.stringSort === 'locale';
111
+ return {
112
+ direction: options.direction,
113
+ nulls: options.nulls ?? 'first',
114
+ stringSort: options.stringSort ?? 'locale',
115
+ locale: collated ? options.locale : undefined,
116
+ localeOptions: collated
117
+ ? options.localeOptions
118
+ : undefined,
119
+ };
120
+ }
121
+ function storedValue(key, row) {
122
+ return key.of === 'field' ? row.values[key.field] : row[key.of];
123
+ }
124
+ function documentOrder(shape) {
125
+ return shape.orderBy.map(({ options }) => orderingOf(options));
126
+ }
127
+ function documentCursor(shape, row) {
128
+ return cursorOf([
129
+ ...shape.orderBy.map((_, index) => row[`order${index}`]),
130
+ documentKey(row.room, row.path),
131
+ ]);
132
+ }
107
133
  function documents(q, rows, plan, shape) {
108
134
  let query = filtered(q, rows, plan);
135
+ const cursor = plan.after?.values ?? null;
136
+ if (cursor !== null) {
137
+ const orderings = documentOrder(shape);
138
+ query = query.fn.where(({ d }) => beyond([...shape.orderBy.map(({ key }) => storedValue(key, d)), d.key], cursor, orderings));
139
+ }
109
140
  for (const { key, options } of shape.orderBy) {
110
141
  query = query.orderBy(({ d }) => keyed(key, d), options);
111
142
  }
112
- if (shape.orderBy.length === 0 && plan.limit !== null) {
143
+ if (shape.orderBy.length > 0 || plan.limit !== null || plan.after !== null) {
113
144
  query = query.orderBy(({ d }) => d.key, { stringSort: 'lexical' });
114
145
  }
115
146
  if (plan.limit !== null)
116
- query = query.limit(plan.limit);
147
+ query = query.limit(plan.limit + 1);
117
148
  return query.select(({ d }) => ({
118
149
  room: d.room,
119
150
  path: d.path,
120
151
  type: d.type,
121
152
  binary: d.binary,
153
+ ...Object.fromEntries(shape.orderBy.map(({ key }, index) => [`order${index}`, keyed(key, d)])),
122
154
  ...(shape.fields === null ? {} : { fields: d.fields }),
123
155
  ...(bodied(shape) ? { text: d.text } : {}),
124
156
  ...(shape.backlinks ? { backlinks: toArray(backlinked(q, rows, d, plan.reach)) } : {}),
125
157
  ...(shape.change ? { change: d.change } : {}),
158
+ ...(shape.fileId ? { fileId: d.fileId } : {}),
126
159
  }));
127
160
  }
128
161
  function backlinked(q, rows, document, reach) {
@@ -133,15 +166,13 @@ function backlinked(q, rows, document, reach) {
133
166
  .orderBy(({ r }) => r.field, { stringSort: 'lexical' })
134
167
  .select(({ r }) => ({ path: r.path, type: r.type, field: r.field }));
135
168
  }
136
- function groups(q, stage, shape, limit) {
169
+ function groups(q, stage, shape) {
137
170
  const keys = shape.groupBy.map((_, index) => `key${index}`);
138
171
  const from = q.from({ m: stage });
139
172
  let query = from.groupBy(({ m }) => keys.map((key) => m[key]));
140
173
  for (const { by, direction } of shape.orderBy) {
141
174
  query = query.orderBy(({ m }) => (by === 'count' ? count(m.key) : m[keys[by]]), direction);
142
175
  }
143
- if (limit !== null && shape.orderBy.length > 0)
144
- query = query.limit(limit);
145
176
  return query.select(({ m }) => ({
146
177
  ...Object.fromEntries(keys.map((key) => [key, m[key]])),
147
178
  count: count(m.key),
@@ -151,15 +182,27 @@ function built(rows, plan) {
151
182
  const shape = plan.shape;
152
183
  if (shape.kind === 'documents') {
153
184
  const collection = live((q) => documents(q, rows, plan, shape));
154
- return { live: collection, dispose: () => void collection.cleanup() };
185
+ const count = plan.total
186
+ ? live((q) => filtered(q, rows, plan).select(({ d }) => ({ key: d.key })))
187
+ : null;
188
+ return {
189
+ live: collection,
190
+ count,
191
+ dispose: () => {
192
+ void collection.cleanup();
193
+ if (count !== null)
194
+ void count.cleanup();
195
+ },
196
+ };
155
197
  }
156
198
  const stage = live((q) => filtered(q, rows, plan).select(({ d }) => ({
157
199
  key: d.key,
158
200
  ...Object.fromEntries(shape.groupBy.map((key, index) => [`key${index}`, keyed(key, d)])),
159
201
  })));
160
- const collection = live((q) => groups(q, stage, shape, plan.limit));
202
+ const collection = live((q) => groups(q, stage, shape));
161
203
  return {
162
204
  live: collection,
205
+ count: null,
163
206
  dispose: () => {
164
207
  void collection.cleanup();
165
208
  void stage.cleanup();
@@ -239,6 +282,7 @@ function documentOf(key, row, shape, bodies) {
239
282
  room: row.room,
240
283
  path: row.path,
241
284
  ...(binary === null ? {} : { binaryFile: binary }),
285
+ ...(shape.fileId ? { fileId: row.fileId } : {}),
242
286
  };
243
287
  }
244
288
  const body = bodied(shape) ? bodyOf(row, key, bodies) : null;
@@ -260,33 +304,74 @@ function documentOf(key, row, shape, bodies) {
260
304
  ...(shape.markdown && body !== null ? { markdown: body.content } : {}),
261
305
  ...(shape.backlinks
262
306
  ? {
263
- backlinks: row.backlinks.map(({ path, type, field }) => ({ path, type, field })),
307
+ backlinks: row.backlinks
308
+ .slice(0, MAXIMUM_WINDOW)
309
+ .map(({ path, type, field }) => ({ path, type, field })),
264
310
  }
265
311
  : {}),
266
312
  ...(shape.change ? { change: row.change } : {}),
313
+ ...(shape.fileId ? { fileId: row.fileId } : {}),
267
314
  };
268
315
  }
269
316
  function weight(row) {
270
317
  return ROW_WEIGHT + (typeof row?.text === 'string' ? row.text.length : 0);
271
318
  }
272
- function rowsOfPlan(plan, entries, bodies, each = (row) => row) {
319
+ function groupValues(shape, [key, row]) {
320
+ return [...shape.orderBy.map(({ by }) => (by === 'count' ? row.count : row[`key${by}`])), key];
321
+ }
322
+ function groupOrder(shape) {
323
+ return shape.orderBy.map(({ direction }) => ({
324
+ direction,
325
+ nulls: 'first',
326
+ stringSort: 'locale',
327
+ locale: undefined,
328
+ localeOptions: undefined,
329
+ }));
330
+ }
331
+ function windowOf(plan, entries, total) {
332
+ const shape = plan.shape;
333
+ if (shape.kind === 'documents') {
334
+ return {
335
+ ...windowed(entries, plan.limit, plan.after?.text, ([, row]) => documentCursor(shape, row)),
336
+ total,
337
+ };
338
+ }
339
+ const cursor = plan.after?.values ?? null;
340
+ const orderings = groupOrder(shape);
341
+ const following = cursor === null
342
+ ? entries
343
+ : entries.filter((entry) => beyond(groupValues(shape, entry), cursor, orderings));
344
+ return {
345
+ ...windowed(following, plan.limit, plan.after?.text, (entry) => cursorOf(groupValues(shape, entry))),
346
+ total: plan.total ? entries.length : undefined,
347
+ };
348
+ }
349
+ function framed(rows, { next, total }) {
350
+ return {
351
+ ...rows,
352
+ ...(next === undefined ? {} : { next }),
353
+ ...(total === undefined ? {} : { total }),
354
+ };
355
+ }
356
+ function rowsOfPlan(plan, entries, total, bodies, each = (row) => row) {
273
357
  const shape = plan.shape;
358
+ const { kept, ...window } = windowOf(plan, entries, total);
359
+ const keys = kept.map(([key]) => key);
274
360
  if (shape.kind === 'documents') {
275
361
  const documents = [];
276
- return after(inUnits(entries, ([key, row]) => {
362
+ return after(inUnits(kept, ([key, row]) => {
277
363
  documents.push(each(documentOf(key, row, shape, bodies)));
278
364
  return weight(row);
279
- }), () => ({ rows: { documents }, keys: entries.map(([key]) => key) }));
365
+ }), () => ({ rows: framed({ documents }, window), keys, ...window }));
280
366
  }
281
- const bounded = shape.orderBy.length === 0 && plan.limit !== null ? entries.slice(0, plan.limit) : entries;
282
367
  const groups = [];
283
- return after(inUnits(bounded, ([, row]) => {
368
+ return after(inUnits(kept, ([, row]) => {
284
369
  groups.push(each({
285
370
  key: shape.groupBy.map((_, index) => row[`key${index}`]),
286
371
  count: row.count,
287
372
  }));
288
373
  return ROW_WEIGHT;
289
- }), () => ({ rows: { groups }, keys: bounded.map(([key]) => key) }));
374
+ }), () => ({ rows: framed({ groups }, window), keys, ...window }));
290
375
  }
291
376
  export function reconciled(previous, next) {
292
377
  const before = previous.result;
@@ -311,13 +396,16 @@ export function reconciled(previous, next) {
311
396
  const removed = previous.keys.filter((key) => !present.has(key));
312
397
  const moved = reordered(previous.keys, next.keys, present);
313
398
  const covered = !deepEquals(before.coverage, after.coverage);
314
- if (changed.length === 0 && removed.length === 0 && !moved && !covered)
399
+ const framing = reframed(before, after);
400
+ if (changed.length === 0 && removed.length === 0 && !moved && !covered && framing === null) {
315
401
  return null;
402
+ }
403
+ const window = { next: after.next, total: after.total };
316
404
  return {
317
405
  keyed: {
318
406
  result: 'groups' in after
319
- ? { groups: rows, coverage: after.coverage }
320
- : { documents: rows, coverage: after.coverage },
407
+ ? framed({ groups: rows, coverage: after.coverage }, window)
408
+ : framed({ documents: rows, coverage: after.coverage }, window),
321
409
  keys: next.keys,
322
410
  },
323
411
  message: {
@@ -325,9 +413,17 @@ export function reconciled(previous, next) {
325
413
  removed,
326
414
  order: moved ? next.keys : null,
327
415
  ...(covered ? { coverage: after.coverage } : {}),
416
+ ...framing,
328
417
  },
329
418
  };
330
419
  }
420
+ function reframed(before, after) {
421
+ const next = before.next === after.next ? {} : { next: after.next ?? null };
422
+ const total = before.total === after.total || after.total === undefined ? {} : { total: after.total };
423
+ return Object.keys(next).length === 0 && Object.keys(total).length === 0
424
+ ? null
425
+ : { ...next, ...total };
426
+ }
331
427
  function reordered(previous, next, present) {
332
428
  let at = 0;
333
429
  for (const key of previous) {
@@ -376,8 +472,11 @@ export function createReadSide() {
376
472
  return;
377
473
  entry.rows.set(key, row);
378
474
  }
475
+ function counted(entry) {
476
+ return entry.built.count === null ? undefined : entry.built.count.size;
477
+ }
379
478
  function rebuilt(entry) {
380
- return after(rowsOfPlan(entry.plan, [...entry.built.live.entries()], entry.bodies), ({ rows: found, keys }) => {
479
+ return after(rowsOfPlan(entry.plan, [...entry.built.live.entries()], counted(entry), entry.bodies), ({ rows: found, keys, next, total }) => {
381
480
  const present = new Set(keys);
382
481
  for (const key of [...entry.rows.keys()])
383
482
  if (!present.has(key))
@@ -387,32 +486,34 @@ export function createReadSide() {
387
486
  entry.bodies.delete(key);
388
487
  }
389
488
  keys.forEach((key, index) => kept(entry, key, rowAt(found, index)));
390
- return keys;
489
+ return { keys, next, total };
391
490
  });
392
491
  }
393
492
  function refreshed(entry, dirty) {
394
493
  const shape = entry.plan.shape;
395
- const keys = [...entry.built.live.keys()];
396
- const named = new Set([...dirty, ...keys.filter((key) => !entry.rows.has(key))]);
397
- const values = [...named].map((key) => [key, entry.built.live.get(key)]);
494
+ const { kept: inWindow, next, total, } = windowOf(entry.plan, [...entry.built.live.entries()], counted(entry));
495
+ const keys = inWindow.map(([key]) => key);
496
+ const present = new Set(keys);
497
+ for (const key of [...entry.rows.keys()]) {
498
+ if (present.has(key))
499
+ continue;
500
+ entry.rows.delete(key);
501
+ entry.bodies.delete(key);
502
+ }
503
+ const values = inWindow.filter(([key]) => dirty.has(key) || !entry.rows.has(key));
398
504
  const done = inUnits(values, ([key, value]) => {
399
- if (value !== undefined)
400
- kept(entry, key, documentOf(key, value, shape, entry.bodies));
401
- else {
402
- entry.rows.delete(key);
403
- entry.bodies.delete(key);
404
- }
505
+ kept(entry, key, documentOf(key, value, shape, entry.bodies));
405
506
  return weight(value);
406
507
  });
407
- return after(done, () => keys);
508
+ return after(done, () => ({ keys, next, total }));
408
509
  }
409
- function current(entry, keys, coverage) {
410
- const found = keys.map((key) => entry.rows.get(key));
510
+ function current(entry, found, coverage) {
511
+ const rows = found.keys.map((key) => entry.rows.get(key));
411
512
  return {
412
513
  result: entry.plan.shape.kind === 'groups'
413
- ? { groups: found, coverage }
414
- : { documents: found, coverage },
415
- keys,
514
+ ? framed({ groups: rows, coverage }, found)
515
+ : framed({ documents: rows, coverage }, found),
516
+ keys: found.keys,
416
517
  };
417
518
  }
418
519
  function delivered(entry, next) {
@@ -448,12 +549,12 @@ export function createReadSide() {
448
549
  const dirty = entry.dirty;
449
550
  entry.dirty = new Set();
450
551
  entry.whole = false;
451
- const keys = whole ? rebuilt(entry) : refreshed(entry, dirty);
452
- if (keys instanceof Promise) {
453
- void keys
454
- .then((found) => {
552
+ const found = whole ? rebuilt(entry) : refreshed(entry, dirty);
553
+ if (found instanceof Promise) {
554
+ void found
555
+ .then((window) => {
455
556
  if (entry.listeners.size > 0)
456
- delivered(entry, current(entry, found, cover.coverage));
557
+ delivered(entry, current(entry, window, cover.coverage));
457
558
  run(entry);
458
559
  })
459
560
  .catch(() => {
@@ -461,7 +562,7 @@ export function createReadSide() {
461
562
  });
462
563
  return;
463
564
  }
464
- delivered(entry, current(entry, keys, cover.coverage));
565
+ delivered(entry, current(entry, found, cover.coverage));
465
566
  }
466
567
  entry.running = false;
467
568
  }
@@ -517,12 +618,14 @@ export function createReadSide() {
517
618
  },
518
619
  recover,
519
620
  async query(plan, at) {
520
- const { live: collection, dispose } = built(rows, plan);
621
+ const { live: collection, count, dispose } = built(rows, plan);
521
622
  try {
522
623
  await collection.toArrayWhenReady();
624
+ if (count !== null)
625
+ await count.toArrayWhenReady();
523
626
  const entries = [...collection.entries()];
524
627
  const held = at();
525
- const found = await rowsOfPlan(plan, entries, null, structuredClone);
628
+ const found = await rowsOfPlan(plan, entries, count?.size, null, structuredClone);
526
629
  return { rows: found.rows, at: held };
527
630
  }
528
631
  finally {
@@ -543,6 +646,7 @@ export function createReadSide() {
543
646
  fresh: new Set(),
544
647
  ready: new Set(),
545
648
  subscription: null,
649
+ counting: null,
546
650
  whole: true,
547
651
  dirty: new Set(),
548
652
  stale: false,
@@ -557,7 +661,14 @@ export function createReadSide() {
557
661
  }
558
662
  const observed = entry;
559
663
  observed.listeners.add(deliverTo);
560
- observed.built.live.onFirstReady(() => {
664
+ const ready = (then) => observed.built.live.onFirstReady(() => observed.built.count === null ? then() : observed.built.count.onFirstReady(then));
665
+ const settled = () => {
666
+ if (observed.built.count === null)
667
+ wake(observed);
668
+ else
669
+ queueMicrotask(() => wake(observed));
670
+ };
671
+ ready(() => {
561
672
  if (!observed.listeners.has(deliverTo))
562
673
  return;
563
674
  observed.fresh.add(deliverTo);
@@ -569,8 +680,13 @@ export function createReadSide() {
569
680
  else
570
681
  for (const { key } of changes)
571
682
  observed.dirty.add(key);
572
- wake(observed);
683
+ settled();
573
684
  }, { includeInitialState: false });
685
+ observed.counting =
686
+ observed.built.count?.subscribeChanges(() => {
687
+ observed.stale = true;
688
+ settled();
689
+ }, { includeInitialState: false }) ?? null;
574
690
  }
575
691
  wake(observed);
576
692
  });
@@ -581,6 +697,8 @@ export function createReadSide() {
581
697
  return;
582
698
  observed.subscription?.unsubscribe();
583
699
  observed.subscription = null;
700
+ observed.counting?.unsubscribe();
701
+ observed.counting = null;
584
702
  observed.result = undefined;
585
703
  observed.rows.clear();
586
704
  observed.bodies.clear();
@@ -601,6 +719,7 @@ export function createReadSide() {
601
719
  entry.fresh.clear();
602
720
  entry.ready.clear();
603
721
  entry.subscription?.unsubscribe();
722
+ entry.counting?.unsubscribe();
604
723
  entry.built.dispose();
605
724
  }
606
725
  shared.clear();
@@ -625,13 +744,16 @@ function changedFrom(previous, next) {
625
744
  const removed = previous.keys.filter((key) => !present.has(key));
626
745
  const moved = reordered(previous.keys, next.keys, present);
627
746
  const covered = !deepEquals(before.coverage, after.coverage);
628
- if (changed.length === 0 && removed.length === 0 && !moved && !covered)
747
+ const framing = reframed(before, after);
748
+ if (changed.length === 0 && removed.length === 0 && !moved && !covered && framing === null) {
629
749
  return null;
750
+ }
630
751
  return {
631
752
  changed,
632
753
  removed,
633
754
  order: moved ? next.keys : null,
634
755
  ...(covered ? { coverage: after.coverage } : {}),
756
+ ...framing,
635
757
  };
636
758
  }
637
759
  function rowAt(rows, index) {
@@ -0,0 +1,53 @@
1
+ import type { Open } from './open.js';
2
+ export type CodeAvailability = Open<'stored' | 'absent'>;
3
+ export type RoomPreparation = Open<'complete' | 'acquiring' | 'preparing' | 'indexing' | 'refused' | 'unadmitted'>;
4
+ export type RoomCoverage = {
5
+ readonly room: string;
6
+ readonly state: RoomPreparation;
7
+ };
8
+ export type ConversationCoverage = Open<'complete' | 'acquiring' | 'indexing' | 'undeclared'>;
9
+ export type SearchObligation = Open<'required' | 'unused'>;
10
+ export type TargetCoverage = {
11
+ readonly complete: boolean;
12
+ readonly listed: boolean;
13
+ readonly rooms: readonly RoomCoverage[];
14
+ readonly conversations: ConversationCoverage;
15
+ readonly search: SearchObligation;
16
+ };
17
+ export type Persistence = Open<'granted' | 'denied' | 'unavailable'>;
18
+ export type Durability = Open<'persistent' | 'best-effort'>;
19
+ export type ConnectionAvailability = Open<'connected' | 'connecting' | 'unreachable'>;
20
+ export type HouseOutcome = Open<'open' | 'write-halted' | 'refused'>;
21
+ export type CapacityCause = {
22
+ readonly cause: Open<'insufficient-space' | 'quota-exceeded'>;
23
+ readonly needed: number | null;
24
+ readonly available: number | null;
25
+ };
26
+ export type RefusedRoom = {
27
+ readonly room: string;
28
+ readonly file: string;
29
+ readonly rule: string;
30
+ readonly location: string | null;
31
+ };
32
+ export type ReadinessStatus = Open<'preparing' | 'paused' | 'refused' | 'ready' | 'lost'>;
33
+ export type Readiness = {
34
+ readonly status: ReadinessStatus;
35
+ readonly ready: boolean;
36
+ readonly usable: boolean;
37
+ readonly code: CodeAvailability;
38
+ readonly coverage: TargetCoverage;
39
+ readonly durability: Durability;
40
+ readonly persistence: Persistence;
41
+ readonly connection: ConnectionAvailability;
42
+ readonly house: HouseOutcome;
43
+ readonly capacity: CapacityCause | null;
44
+ readonly refused: readonly RefusedRoom[];
45
+ readonly lost: boolean;
46
+ };
47
+ export type ReadinessFacts = Omit<Readiness, 'status' | 'ready' | 'usable' | 'durability' | 'coverage'> & {
48
+ readonly coverage: Omit<TargetCoverage, 'complete'>;
49
+ readonly established: boolean;
50
+ };
51
+ export declare const SETTLED_ROOMS: ReadonlySet<string>;
52
+ export declare function readinessOf(facts: ReadinessFacts): Readiness;
53
+ //# sourceMappingURL=readiness.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"readiness.d.ts","sourceRoot":"","sources":["../src/readiness.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ const COMPLETE_ROOMS = new Set(['complete', 'unadmitted']);
2
+ export const SETTLED_ROOMS = new Set([...COMPLETE_ROOMS, 'refused']);
3
+ export function readinessOf(facts) {
4
+ const { established, coverage: held, ...reported } = facts;
5
+ const settledConversations = held.conversations === 'complete' || held.conversations === 'undeclared';
6
+ const complete = held.listed &&
7
+ settledConversations &&
8
+ held.rooms.every((room) => COMPLETE_ROOMS.has(room.state));
9
+ const settled = held.listed &&
10
+ settledConversations &&
11
+ held.rooms.every((room) => SETTLED_ROOMS.has(room.state));
12
+ const coverage = { ...held, complete };
13
+ const stored = facts.code === 'stored';
14
+ const ready = stored && complete;
15
+ const usable = stored && (ready || established);
16
+ const status = ready
17
+ ? 'ready'
18
+ : facts.capacity !== null
19
+ ? 'paused'
20
+ : facts.lost && !established
21
+ ? 'lost'
22
+ : settled && !complete
23
+ ? 'refused'
24
+ : 'preparing';
25
+ return {
26
+ ...reported,
27
+ coverage,
28
+ status,
29
+ ready,
30
+ usable,
31
+ durability: facts.persistence === 'granted' ? 'persistent' : 'best-effort',
32
+ };
33
+ }
@@ -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
+ }
package/dist/refresh.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import type { Adapter, AdapterFailure, Authorization, RetryableRefusal } from './adapter.js';
2
- import type { Remodel } from './bootstrap.js';
1
+ import type { AdapterFailure, Authorization, RetryableRefusal } from './adapter.js';
2
+ import type { Reads, Remodel } from './bootstrap.js';
3
3
  import { type ConfirmedLayer } from './replica.js';
4
4
  import type { ReplicaRecord } from './store.js';
5
5
  import { type Refused, type Unconfirmed } from './transfer.js';
@@ -19,6 +19,6 @@ export type RefreshOutcome = {
19
19
  } | {
20
20
  kind: 'beyond-horizon';
21
21
  } | RetryableRefusal | Unconfirmed | Refused | AdapterFailure;
22
- export declare function advancement(adapter: Adapter, authorization: Authorization, held: ReplicaRecord, remodel: Remodel, continuing: () => boolean): Promise<Advancement>;
23
- export declare function refreshRoom(adapter: Adapter, authorization: Authorization, confirmed: ConfirmedLayer, remodel: Remodel, continuing: () => boolean): Promise<RefreshOutcome>;
22
+ export declare function advancement(adapter: Reads, authorization: Authorization, held: ReplicaRecord, remodel: Remodel, continuing: () => boolean): Promise<Advancement>;
23
+ export declare function refreshRoom(adapter: Reads, authorization: Authorization, confirmed: ConfirmedLayer, remodel: Remodel, continuing: () => boolean): Promise<RefreshOutcome>;
24
24
  //# sourceMappingURL=refresh.d.ts.map