@agentshouse/mdruntime 0.1.0-c7f1d9323e816 → 0.1.0-c967d1ca66fee

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 (81) hide show
  1. package/AGENTS.md +27 -11
  2. package/README.md +2 -2
  3. package/dist/adapter.d.ts +7 -3
  4. package/dist/adapter.d.ts.map +1 -1
  5. package/dist/bootstrap.d.ts +17 -5
  6. package/dist/bootstrap.d.ts.map +1 -1
  7. package/dist/bootstrap.js +88 -35
  8. package/dist/change-set.d.ts.map +1 -1
  9. package/dist/change-set.js +13 -7
  10. package/dist/change.d.ts +4 -0
  11. package/dist/change.d.ts.map +1 -1
  12. package/dist/client.d.ts +4 -3
  13. package/dist/client.d.ts.map +1 -1
  14. package/dist/client.js +50 -14
  15. package/dist/collection.d.ts +6 -11
  16. package/dist/collection.d.ts.map +1 -1
  17. package/dist/collection.js +56 -19
  18. package/dist/connection.d.ts +10 -5
  19. package/dist/connection.d.ts.map +1 -1
  20. package/dist/connection.js +189 -91
  21. package/dist/conversation.d.ts +2 -1
  22. package/dist/conversation.d.ts.map +1 -1
  23. package/dist/conversation.js +2 -1
  24. package/dist/failure.d.ts +13 -0
  25. package/dist/failure.d.ts.map +1 -0
  26. package/dist/failure.js +19 -0
  27. package/dist/host.d.ts.map +1 -1
  28. package/dist/host.js +3 -7
  29. package/dist/identity.d.ts +1 -0
  30. package/dist/identity.d.ts.map +1 -1
  31. package/dist/identity.js +7 -1
  32. package/dist/index.d.ts +3 -2
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/memory.d.ts.map +1 -1
  35. package/dist/memory.js +24 -11
  36. package/dist/model.d.ts +3 -2
  37. package/dist/model.d.ts.map +1 -1
  38. package/dist/open.d.ts +2 -0
  39. package/dist/open.d.ts.map +1 -0
  40. package/dist/open.js +1 -0
  41. package/dist/outcome.d.ts +22 -3
  42. package/dist/outcome.d.ts.map +1 -1
  43. package/dist/outcome.js +35 -1
  44. package/dist/overlay.d.ts +1 -1
  45. package/dist/overlay.d.ts.map +1 -1
  46. package/dist/overlay.js +19 -15
  47. package/dist/query.d.ts +2 -0
  48. package/dist/query.d.ts.map +1 -1
  49. package/dist/query.js +3 -0
  50. package/dist/reach.d.ts.map +1 -1
  51. package/dist/reach.js +19 -17
  52. package/dist/react.d.ts +22 -15
  53. package/dist/react.d.ts.map +1 -1
  54. package/dist/react.js +8 -6
  55. package/dist/refresh.d.ts +10 -0
  56. package/dist/refresh.d.ts.map +1 -1
  57. package/dist/refresh.js +33 -24
  58. package/dist/replica.d.ts +4 -4
  59. package/dist/replica.d.ts.map +1 -1
  60. package/dist/replica.js +42 -27
  61. package/dist/room-draft.d.ts +8 -8
  62. package/dist/room-draft.d.ts.map +1 -1
  63. package/dist/room-draft.js +13 -22
  64. package/dist/rows.d.ts +2 -0
  65. package/dist/rows.d.ts.map +1 -1
  66. package/dist/rows.js +7 -1
  67. package/dist/save.d.ts +3 -1
  68. package/dist/save.d.ts.map +1 -1
  69. package/dist/save.js +38 -21
  70. package/dist/search.d.ts +3 -2
  71. package/dist/search.d.ts.map +1 -1
  72. package/dist/status.d.ts +2 -2
  73. package/dist/status.d.ts.map +1 -1
  74. package/dist/status.js +0 -8
  75. package/dist/store.d.ts +21 -1
  76. package/dist/store.d.ts.map +1 -1
  77. package/dist/store.js +194 -89
  78. package/dist/wire.d.ts +3 -3
  79. package/dist/wire.d.ts.map +1 -1
  80. package/dist/wire.js +11 -2
  81. package/package.json +2 -2
@@ -3,17 +3,20 @@ import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
3
3
  import { observing } from './observation.js';
4
4
  import { filterOf, identityOf, planOf, } from './query.js';
5
5
  import { createReadSide } from './read-side.js';
6
+ import { markdownPath, roomOfKey } from './rows.js';
6
7
  import { createSearchIndex, searchPlanOf, } from './search.js';
7
8
  import { bootstrapRoom } from './bootstrap.js';
8
9
  import { changeSet, mergeChangeSets } from './change-set.js';
9
10
  import { createConversationStore, } from './conversation.js';
10
- import { parseCollection, validRoot } from './collection.js';
11
+ import { validRoot } from './collection.js';
12
+ import { runtimeError } from './failure.js';
11
13
  import { createLog } from './log.js';
14
+ import { calledResult, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
12
15
  import { refreshRoom } from './refresh.js';
13
16
  import { buildBatch } from './save.js';
14
- import { applyReturned, compositionOf, landed, readConfirmed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
15
- import { changedParts, changesOf, held, readParts, returnedInto, underneath, } from './room-draft.js';
16
- import { settled, same, saving, STARTING, transferring, } from './status.js';
17
+ import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
18
+ import { changedParts, changesOf, held, readParts, returnedBy, returnedInto, staleWrite, underneath, } from './room-draft.js';
19
+ import { same, saving, STARTING, transferring, } from './status.js';
17
20
  import { detectStorageLoss, nextGeneration } from './durability.js';
18
21
  import { roomDraftRevision } from './identity.js';
19
22
  import { beneath, normalize } from './paths.js';
@@ -34,6 +37,7 @@ function declaredSelection(room) {
34
37
  selection.roots.every((other, at) => at === index || !within(other, root)));
35
38
  }
36
39
  export function composeRuntime(options) {
40
+ const localSaves = options.adapter.localSaves === true;
37
41
  let authorization = options.authorization;
38
42
  let opening;
39
43
  let store;
@@ -44,6 +48,7 @@ export function composeRuntime(options) {
44
48
  let limits = [];
45
49
  let capabilities = [];
46
50
  const rooms = new Map();
51
+ const counted = new Map();
47
52
  const unreadable = new Set();
48
53
  const models = new Map();
49
54
  const liveModels = new Map();
@@ -82,6 +87,7 @@ export function composeRuntime(options) {
82
87
  let withdrawStream;
83
88
  const announced = new Map();
84
89
  let underway = null;
90
+ let streamed = null;
85
91
  let handed = false;
86
92
  const submitting = new Map();
87
93
  const answering = new Map();
@@ -107,9 +113,12 @@ export function composeRuntime(options) {
107
113
  const serviceModeListeners = new Set();
108
114
  let member = null;
109
115
  const memberListeners = new Set();
116
+ const materialized = new Set();
117
+ let everyRoom = false;
118
+ const transfers = new Map();
110
119
  const conversations = createConversationStore({
111
120
  agents: declaredAgents,
112
- documents: (locator) => readSide.query(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
121
+ documents: (locator) => rowsQuery(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
113
122
  call: (name, input) => invokeAction(name, input),
114
123
  durably,
115
124
  stream: () => options.adapter.conversations,
@@ -185,7 +194,7 @@ export function composeRuntime(options) {
185
194
  }
186
195
  }
187
196
  function pending() {
188
- return [...replicas.values()].some((replica) => replica.queue.length > 0);
197
+ return !localSaves && [...replicas.values()].some((replica) => replica.queue.length > 0);
189
198
  }
190
199
  function attention() {
191
200
  const attentionRequired = [];
@@ -214,8 +223,24 @@ export function composeRuntime(options) {
214
223
  }
215
224
  return { attentionRequired, reset };
216
225
  }
226
+ function documentsIn(locator) {
227
+ const replica = replicas.get(locator);
228
+ if (replica === undefined)
229
+ return 0;
230
+ const roots = replica.confirmed.record.typedSelection?.roots ?? [];
231
+ let count = 0;
232
+ for (const path of Object.keys(replica.visible.documents)) {
233
+ if (markdownPath(path) && !roots.some((root) => beneath(root, path)))
234
+ count += 1;
235
+ }
236
+ return count;
237
+ }
217
238
  function listed() {
218
- return structuredClone([...rooms.values()]);
239
+ return structuredClone([...rooms.values()].map((room) => {
240
+ const documents = documentsIn(room.locator);
241
+ counted.set(room.locator, documents);
242
+ return { ...room, documents };
243
+ }));
219
244
  }
220
245
  function roomFacts(reach = null) {
221
246
  const held = [...rooms.values()]
@@ -270,6 +295,8 @@ export function composeRuntime(options) {
270
295
  listener(shown);
271
296
  }
272
297
  function entryOf(batch) {
298
+ if (localSaves)
299
+ return Object.freeze({ batch, state: 'local' });
273
300
  const state = pendings.get(batch.id);
274
301
  if (state?.accepted !== undefined) {
275
302
  return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
@@ -290,12 +317,47 @@ export function composeRuntime(options) {
290
317
  }
291
318
  }
292
319
  function applyChangeSet(set) {
320
+ const held = everyRoom
321
+ ? set
322
+ : { ...set, documents: set.documents.filter(({ locator }) => materialized.has(locator)) };
323
+ readSide.apply(held);
324
+ searchIndex.changed(held);
325
+ }
326
+ function materialize(locators) {
327
+ if (everyRoom)
328
+ return;
329
+ if (locators === null)
330
+ everyRoom = true;
331
+ const sets = [];
332
+ for (const locator of locators ?? [...replicas.keys()]) {
333
+ if (materialized.has(locator))
334
+ continue;
335
+ materialized.add(locator);
336
+ const replica = replicas.get(locator);
337
+ if (replica !== undefined)
338
+ sets.push(changeSet(locator, null, replica));
339
+ }
340
+ if (sets.length === 0)
341
+ return;
342
+ const set = mergeChangeSets(sets);
293
343
  readSide.apply(set);
294
344
  searchIndex.changed(set);
295
345
  }
346
+ function reachedRooms(plan) {
347
+ if (plan.rooms !== null)
348
+ return plan.rooms;
349
+ if (plan.reach === null)
350
+ return null;
351
+ return [...plan.reach.rooms, ...plan.reach.documents.map(roomOfKey)];
352
+ }
353
+ function rowsQuery(plan) {
354
+ materialize(reachedRooms(plan));
355
+ return readSide.query(plan);
356
+ }
296
357
  function publishChangeSet(set) {
297
358
  applyChangeSet(set);
298
- if (set.rooms.length > 0)
359
+ if (set.rooms.length > 0 ||
360
+ [...new Set(set.documents.map(({ locator }) => locator))].some((locator) => rooms.has(locator) && counted.get(locator) !== documentsIn(locator)))
299
361
  emitRooms();
300
362
  for (const locator of set.queues)
301
363
  emitQueue(locator);
@@ -369,10 +431,7 @@ export function composeRuntime(options) {
369
431
  model: revoked,
370
432
  selectionModel: revoked,
371
433
  };
372
- const read = readConfirmed(record, before.confirmed);
373
- replicas.set(locator, read.kind === 'staged'
374
- ? replicaOf(read.confirmed, before.queue, before)
375
- : { ...before, confirmed: { ...before.confirmed, record } });
434
+ replicas.set(locator, replicaOf(confirmedOf(record, before.confirmed), before.queue, before));
376
435
  }
377
436
  rooms.set(locator, {
378
437
  ...room,
@@ -389,7 +448,8 @@ export function composeRuntime(options) {
389
448
  }
390
449
  finally {
391
450
  refusal = {
392
- reason,
451
+ code: reason,
452
+ retryable: false,
393
453
  reauthorizationOffered: reason === 'revoked-authorization',
394
454
  refusal: provider,
395
455
  };
@@ -439,7 +499,7 @@ export function composeRuntime(options) {
439
499
  emitRooms();
440
500
  }
441
501
  function unlistedHeld(listed) {
442
- return [...new Set([...rooms.keys(), ...roomDrafts.keys()])].filter((locator) => !listed.has(locator));
502
+ return [...new Set([...rooms.keys(), ...roomDrafts.keys(), ...transfers.keys()])].filter((locator) => !listed.has(locator));
443
503
  }
444
504
  async function dropUnlisted(listed, local) {
445
505
  if (unlistedHeld(listed).length === 0)
@@ -458,6 +518,7 @@ export function composeRuntime(options) {
458
518
  replicas.delete(locator);
459
519
  rooms.delete(locator);
460
520
  roomDrafts.delete(locator);
521
+ transfers.delete(locator);
461
522
  unreadable.delete(locator);
462
523
  stale.delete(locator);
463
524
  scopedRefusals.delete(locator);
@@ -494,7 +555,7 @@ export function composeRuntime(options) {
494
555
  return model;
495
556
  }
496
557
  async function hydrate(opened) {
497
- const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known,] = await Promise.all([
558
+ const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged,] = await Promise.all([
498
559
  opened.all(),
499
560
  opened.queues(),
500
561
  opened.roomDrafts(),
@@ -505,7 +566,11 @@ export function composeRuntime(options) {
505
566
  opened.providerLimits(),
506
567
  opened.capabilities(),
507
568
  opened.member(),
569
+ opened.transfers(),
508
570
  ]);
571
+ transfers.clear();
572
+ for (const record of staged)
573
+ transfers.set(record.locator, record);
509
574
  limits = declared;
510
575
  capabilities = named;
511
576
  member = known;
@@ -528,14 +593,7 @@ export function composeRuntime(options) {
528
593
  const model = restored(modelKey(record.locator, 'collection'), record.model);
529
594
  const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
530
595
  const prior = previousReplicas.get(record.locator);
531
- const retyped = { ...record, model, selectionModel };
532
- const read = readConfirmed(retyped, prior?.confirmed);
533
- const confirmed = read.kind === 'staged'
534
- ? read.confirmed
535
- : {
536
- record: retyped,
537
- collection: parseCollection(retyped.documents, compositionOf(retyped)),
538
- };
596
+ const confirmed = confirmedOf({ ...record, model, selectionModel }, prior?.confirmed);
539
597
  replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], prior));
540
598
  const existing = rooms.get(record.locator);
541
599
  if (existing === undefined)
@@ -556,10 +614,6 @@ export function composeRuntime(options) {
556
614
  },
557
615
  });
558
616
  }
559
- if (read.kind === 'refused') {
560
- refusedSource(record, read);
561
- declarationRefusals.add(record.locator);
562
- }
563
617
  }
564
618
  return {
565
619
  lastCompleted,
@@ -621,11 +675,11 @@ export function composeRuntime(options) {
621
675
  return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
622
676
  }
623
677
  async function settle(local, locator, before, confirmed, queue, settling = {}) {
624
- const withdrawn = withdrawals(confirmed, queue, withdrawable);
678
+ const withdrawn = withdrawals(confirmed, queue, withdrawable, localSaves);
625
679
  const kept = queue.filter((batch) => !withdrawn.includes(batch));
626
680
  const unqueued = withdrawn.map((batch) => ({
627
681
  batch,
628
- returned: landed(batch, confirmed) ? null : { kind: 'withdrawn' },
682
+ returned: localSaves || landed(batch, confirmed) ? null : returnedBy('withdrawn'),
629
683
  }));
630
684
  const returning = [
631
685
  ...[...unqueued].reverse(),
@@ -649,6 +703,7 @@ export function composeRuntime(options) {
649
703
  await local.write({
650
704
  next: confirmed.record,
651
705
  previous: before?.confirmed.record ?? null,
706
+ ...(settling.stored === undefined ? {} : { stored: settling.stored }),
652
707
  queue: kept,
653
708
  previousQueue: before?.queue ?? [],
654
709
  roomDrafts: [...written.values()],
@@ -660,7 +715,8 @@ export function composeRuntime(options) {
660
715
  for (const record of written.values())
661
716
  rememberRoomDraft(record);
662
717
  for (const { batch, returned } of unqueued) {
663
- journal.record(`outcome ${locator} ${batch.id} ${returned === null ? 'landed' : 'withdrawn'}`);
718
+ const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
719
+ journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
664
720
  }
665
721
  replicas.set(locator, replicaOf(confirmed, kept, before));
666
722
  return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
@@ -700,7 +756,8 @@ export function composeRuntime(options) {
700
756
  selection: taken(answers?.selection, held.selection, 'typed-selection'),
701
757
  };
702
758
  }
703
- function publish(local, room, confirmed, base) {
759
+ function publish(local, room, confirmed, landing = {}) {
760
+ const { base, stored } = landing;
704
761
  return exclusive(async () => {
705
762
  if (!continuing())
706
763
  return true;
@@ -717,7 +774,9 @@ export function composeRuntime(options) {
717
774
  const accepted = pendings.get(batch.id)?.accepted;
718
775
  return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
719
776
  });
720
- const set = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true });
777
+ const set = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
778
+ if (stored !== undefined)
779
+ transfers.delete(room.locator);
721
780
  for (const batch of accepted)
722
781
  pendings.delete(batch.id);
723
782
  stale.delete(room.locator);
@@ -736,22 +795,23 @@ export function composeRuntime(options) {
736
795
  const replica = replicas.get(locator);
737
796
  const room = rooms.get(locator);
738
797
  if (replica === undefined || room === undefined) {
739
- throw new Error(`No Local Replica for ${locator}`);
798
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
740
799
  }
741
800
  const saved = [];
742
801
  for (const { path, revision } of named) {
743
802
  const record = roomDrafts.get(locator)?.get(path);
744
803
  if (record === undefined || record.revision !== revision) {
745
- return { outcome: 'refused', rule: 'stale', file: path };
804
+ return { outcome: 'refused', code: 'stale', retryable: false, file: path };
746
805
  }
747
806
  if (!saved.some((draft) => draft.path === path))
748
807
  saved.push({ path, parts: record.parts });
749
808
  }
750
809
  const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
751
810
  if (changed.length > 0)
752
- return { outcome: 'refused', rule: 'changed-underneath', parts: changed };
811
+ return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
753
812
  const parts = saved.map(({ path, parts }) => changesOf(path, parts));
754
813
  const built = buildBatch({
814
+ local: localSaves,
755
815
  baseRoomVersion: replica.confirmed.record.roomVersion,
756
816
  composition: compositionFor(replica.confirmed.record),
757
817
  operations: room.operations,
@@ -778,10 +838,11 @@ export function composeRuntime(options) {
778
838
  });
779
839
  }
780
840
  catch {
781
- return { outcome: 'refused', rule: 'durable-write' };
841
+ return { outcome: 'refused', code: 'durable-write', retryable: true };
782
842
  }
783
843
  pendings.set(built.batch.id, { drafts: saved, submission: { state: 'waiting' }, lost: false });
784
- recorded = false;
844
+ if (!localSaves)
845
+ recorded = false;
785
846
  for (const path of consumed)
786
847
  forgetRoomDraft(locator, path);
787
848
  replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
@@ -789,7 +850,7 @@ export function composeRuntime(options) {
789
850
  queue: true,
790
851
  drafts: consumed,
791
852
  }));
792
- moveTo({ ...saving(status), ...attention() });
853
+ moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
793
854
  submitLater(locator);
794
855
  return { outcome: 'saved', identifier: built.batch.id };
795
856
  }
@@ -817,16 +878,15 @@ export function composeRuntime(options) {
817
878
  }
818
879
  async function readHistory(locator, path, query = {}) {
819
880
  if (closed || refusal !== null)
820
- return { kind: 'transport-uncertainty' };
881
+ return UNCERTAIN;
821
882
  try {
822
883
  await ensureAuthorization();
823
884
  }
824
885
  catch {
825
- return { kind: 'transport-uncertainty' };
826
- }
827
- if (closed || authorization === undefined || refusal !== null) {
828
- return { kind: 'transport-uncertainty' };
886
+ return UNCERTAIN;
829
887
  }
888
+ if (closed || authorization === undefined || refusal !== null)
889
+ return UNCERTAIN;
830
890
  const transfer = ['transfer', 'history', locator, path];
831
891
  if (query.revision !== undefined)
832
892
  transfer.push(query.revision);
@@ -848,11 +908,11 @@ export function composeRuntime(options) {
848
908
  }
849
909
  if (result.kind === 'revoked-authorization')
850
910
  await refuse('revoked-authorization');
851
- return result;
911
+ return historyAnswer(result);
852
912
  }
853
913
  catch {
854
914
  journal.record('outcome history transport-uncertainty');
855
- return { kind: 'transport-uncertainty' };
915
+ return UNCERTAIN;
856
916
  }
857
917
  }
858
918
  function declaredAgents() {
@@ -874,18 +934,17 @@ export function composeRuntime(options) {
874
934
  async function invokeAction(name, input) {
875
935
  await ready;
876
936
  if (closed || refusal !== null)
877
- return { kind: 'transport-uncertainty' };
937
+ return UNCERTAIN;
878
938
  if (declaredAction(name) === undefined)
879
- return { kind: 'undeclared' };
939
+ return undeclared(name);
880
940
  try {
881
941
  await ensureAuthorization();
882
942
  }
883
943
  catch {
884
- return { kind: 'transport-uncertainty' };
885
- }
886
- if (closed || authorization === undefined || refusal !== null) {
887
- return { kind: 'transport-uncertainty' };
944
+ return UNCERTAIN;
888
945
  }
946
+ if (closed || authorization === undefined || refusal !== null)
947
+ return UNCERTAIN;
889
948
  journal.record(`transfer action ${name}`);
890
949
  try {
891
950
  const result = await options.adapter.action(authorization, { name, input });
@@ -899,11 +958,11 @@ export function composeRuntime(options) {
899
958
  journal.record(`outcome action ${result.kind}`);
900
959
  if (result.kind === 'rejected')
901
960
  journal.record(`refusal rejected ${result.code}`);
902
- return result;
961
+ return calledResult(result);
903
962
  }
904
963
  catch {
905
964
  journal.record('outcome action transport-uncertainty');
906
- return { kind: 'transport-uncertainty' };
965
+ return UNCERTAIN;
907
966
  }
908
967
  }
909
968
  function continuing() {
@@ -934,14 +993,33 @@ export function composeRuntime(options) {
934
993
  throw error;
935
994
  }
936
995
  }
937
- async function pullBootstrap(authorized, room) {
996
+ function staging(local) {
997
+ return {
998
+ held: (locator) => transfers.get(locator) ?? null,
999
+ staged: (record) => local.staged(record),
1000
+ stage: (record, page) => exclusive(async () => {
1001
+ if (!continuing())
1002
+ return false;
1003
+ await local.stage(record, page);
1004
+ transfers.set(record.locator, record);
1005
+ return true;
1006
+ }),
1007
+ abandon: (locator) => exclusive(async () => {
1008
+ if (!continuing())
1009
+ return;
1010
+ await local.abandon(locator);
1011
+ transfers.delete(locator);
1012
+ }),
1013
+ };
1014
+ }
1015
+ async function pullBootstrap(authorized, room, local) {
938
1016
  if (!continuing())
939
1017
  return unconfirmed;
940
1018
  try {
941
1019
  const result = await bootstrapRoom(options.adapter, authorized, room, {
942
1020
  collection: modelOf(room.locator),
943
1021
  selection: modelOf(room.locator, 'typed-selection'),
944
- }, (models, held) => remodel(room.locator, models, held), continuing);
1022
+ }, (models, held) => remodel(room.locator, models, held), continuing, staging(local));
945
1023
  const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
946
1024
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
947
1025
  return result;
@@ -969,7 +1047,7 @@ export function composeRuntime(options) {
969
1047
  return null;
970
1048
  }
971
1049
  if (refreshed.kind === 'advanced') {
972
- if (!(await publish(local, room, refreshed.confirmed, held.confirmed))) {
1050
+ if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }))) {
973
1051
  const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
974
1052
  const reached = now !== undefined &&
975
1053
  options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
@@ -981,12 +1059,12 @@ export function composeRuntime(options) {
981
1059
  if (refreshed.kind !== 'beyond-horizon')
982
1060
  return refreshed;
983
1061
  }
984
- const bootstrapped = await pullBootstrap(authorized, room);
1062
+ const bootstrapped = await pullBootstrap(authorized, room, local);
985
1063
  if (!continuing())
986
1064
  return unconfirmed;
987
1065
  if (bootstrapped.kind !== 'staged')
988
1066
  return bootstrapped;
989
- await publish(local, room, bootstrapped.confirmed);
1067
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
990
1068
  return null;
991
1069
  }
992
1070
  async function withhold(withheld, room, local, progress) {
@@ -1018,6 +1096,7 @@ export function composeRuntime(options) {
1018
1096
  function refusedSource(room, { refusal }) {
1019
1097
  sourceRefusals.set(room.locator, {
1020
1098
  code: refusal.rule,
1099
+ retryable: false,
1021
1100
  fields: {
1022
1101
  file: refusal.file,
1023
1102
  ...(refusal.location === undefined ? {} : { location: refusal.location }),
@@ -1032,8 +1111,16 @@ export function composeRuntime(options) {
1032
1111
  }
1033
1112
  function leaving(batch, result) {
1034
1113
  if (result.kind === 'expired')
1035
- return { batch, returned: { kind: 'expired' } };
1036
- return { batch, returned: { kind: result.kind, code: result.code, fields: result.fields } };
1114
+ return { batch, returned: returnedBy('expired') };
1115
+ return {
1116
+ batch,
1117
+ returned: {
1118
+ kind: result.kind,
1119
+ code: result.code,
1120
+ fields: result.fields,
1121
+ retryable: result.retryable,
1122
+ },
1123
+ };
1037
1124
  }
1038
1125
  function logAccepted(locator, submitted, accepted) {
1039
1126
  journal.record(`outcome ${locator} ${submitted.id} accepted ${accepted.roomVersion}` +
@@ -1051,7 +1138,7 @@ export function composeRuntime(options) {
1051
1138
  logAccepted(room.locator, submitted, accepted);
1052
1139
  const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
1053
1140
  ? { kind: 'staged', confirmed: current.confirmed }
1054
- : applyReturned(current.confirmed, accepted.documents, accepted.revisions, accepted.removals, accepted.attachments, options.adapter.follows(room.locator, held, accepted.roomVersion)
1141
+ : applyReturned(current.confirmed, accepted.documents, accepted.revisions, accepted.changes, accepted.removals, accepted.attachments, options.adapter.follows(room.locator, held, accepted.roomVersion)
1055
1142
  ? accepted.roomVersion
1056
1143
  : held, accepted.roomVersion);
1057
1144
  if (applied.kind !== 'staged') {
@@ -1259,7 +1346,7 @@ export function composeRuntime(options) {
1259
1346
  ...(attached === 'collection' ? { model: next } : { selectionModel: next }),
1260
1347
  };
1261
1348
  const reclassified = sameMembership(declared, held.confirmed.record)
1262
- ? readConfirmed(declared, held.confirmed)
1349
+ ? { kind: 'staged', confirmed: confirmedOf(declared, held.confirmed) }
1263
1350
  : readReclassified(declared, held.confirmed);
1264
1351
  const record = reclassified.kind === 'staged'
1265
1352
  ? declared
@@ -1267,7 +1354,9 @@ export function composeRuntime(options) {
1267
1354
  ...declared,
1268
1355
  typedSelection: held.confirmed.record.typedSelection,
1269
1356
  };
1270
- const read = reclassified.kind === 'deferred' ? readConfirmed(record, held.confirmed) : reclassified;
1357
+ const read = reclassified.kind === 'deferred'
1358
+ ? { kind: 'staged', confirmed: confirmedOf(record, held.confirmed) }
1359
+ : reclassified;
1271
1360
  await local.write({
1272
1361
  next: record,
1273
1362
  previous: held.confirmed.record,
@@ -1299,7 +1388,11 @@ export function composeRuntime(options) {
1299
1388
  });
1300
1389
  }
1301
1390
  async function scoped(result, room, local) {
1302
- scopedRefusals.set(room.locator, { code: result.code, fields: result.fields });
1391
+ scopedRefusals.set(room.locator, {
1392
+ code: result.code,
1393
+ fields: result.fields,
1394
+ retryable: result.retryable,
1395
+ });
1303
1396
  let scopedAway = room;
1304
1397
  if (result.scope === 'model')
1305
1398
  await retainModel(local, room, 'collection', {
@@ -1416,14 +1509,14 @@ export function composeRuntime(options) {
1416
1509
  async function reload(room, local, progress) {
1417
1510
  if (discoveryPaused || authorization === undefined)
1418
1511
  return;
1419
- const bootstrapped = await pullBootstrap(authorization, room);
1512
+ const bootstrapped = await pullBootstrap(authorization, room, local);
1420
1513
  if (!continuing())
1421
1514
  return;
1422
1515
  if (bootstrapped.kind !== 'staged') {
1423
1516
  await withhold(bootstrapped, room, local, progress);
1424
1517
  return;
1425
1518
  }
1426
- await publish(local, room, bootstrapped.confirmed);
1519
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
1427
1520
  await caughtUp(room, local, progress);
1428
1521
  }
1429
1522
  async function discoverRooms(local, progress, attempt) {
@@ -1458,7 +1551,11 @@ export function composeRuntime(options) {
1458
1551
  }
1459
1552
  if (result.kind === 'contract-unavailable') {
1460
1553
  journal.record(`connection contract unavailable ${result.code}`);
1461
- await refuse('missing-required-guarantee', { code: result.code, fields: result.fields });
1554
+ await refuse('missing-required-guarantee', {
1555
+ code: result.code,
1556
+ fields: result.fields,
1557
+ retryable: result.retryable,
1558
+ });
1462
1559
  notifyReady();
1463
1560
  return;
1464
1561
  }
@@ -1534,6 +1631,8 @@ export function composeRuntime(options) {
1534
1631
  }
1535
1632
  const progress = { complete: true, reachable: true };
1536
1633
  underway = attempt === 'opened' ? progress : null;
1634
+ if (attempt === 'opened')
1635
+ streamed = progress;
1537
1636
  complete = false;
1538
1637
  recorded = false;
1539
1638
  moveTo(transferring(status));
@@ -1574,9 +1673,14 @@ export function composeRuntime(options) {
1574
1673
  void refuse('revoked-authorization');
1575
1674
  return;
1576
1675
  }
1577
- if (opened || status.providerUnreachable)
1578
- return;
1579
- moveTo({ ...status, providerUnreachable: true });
1676
+ complete = false;
1677
+ if (streamed !== null)
1678
+ streamed.complete = false;
1679
+ moveTo({
1680
+ ...status,
1681
+ state: status.state === 'synced' ? 'syncing' : status.state,
1682
+ providerUnreachable: status.providerUnreachable || !opened,
1683
+ });
1580
1684
  }
1581
1685
  function applyOpening(expired) {
1582
1686
  return task(async () => {
@@ -1631,6 +1735,8 @@ export function composeRuntime(options) {
1631
1735
  }
1632
1736
  }
1633
1737
  function submitLater(locator) {
1738
+ if (localSaves)
1739
+ return;
1634
1740
  const running = submitting.get(locator);
1635
1741
  if (running !== undefined) {
1636
1742
  running.again = true;
@@ -1710,7 +1816,7 @@ export function composeRuntime(options) {
1710
1816
  }
1711
1817
  }
1712
1818
  moveTo({
1713
- state: pending() ? 'pending' : synced ? 'synced' : 'syncing',
1819
+ state: pending() ? 'pending' : synced && complete ? 'synced' : 'syncing',
1714
1820
  lastCompleted,
1715
1821
  providerUnreachable: !progress.reachable,
1716
1822
  ...attention(),
@@ -1819,20 +1925,15 @@ export function composeRuntime(options) {
1819
1925
  show();
1820
1926
  return;
1821
1927
  }
1822
- const resting = status;
1823
- const progress = { complete: false, reachable: !resting.providerUnreachable };
1824
- moveTo(transferring(resting));
1928
+ const progress = { complete: false, reachable: !status.providerUnreachable };
1929
+ moveTo(transferring(status));
1825
1930
  try {
1826
1931
  await conversations.catchUp(progress);
1827
1932
  }
1828
1933
  catch {
1829
1934
  progress.reachable = false;
1830
1935
  }
1831
- finally {
1832
- if (!closed) {
1833
- moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
1834
- }
1835
- }
1936
+ await rest(progress);
1836
1937
  }
1837
1938
  function close() {
1838
1939
  if (closed)
@@ -1886,13 +1987,16 @@ export function composeRuntime(options) {
1886
1987
  return planOf(input, roomFacts(reach), planReach(reach));
1887
1988
  }
1888
1989
  function watchPlan(plan, deliver) {
1990
+ materialize(reachedRooms(plan));
1889
1991
  return readSide.watch(plan, identityOf(plan), deliver);
1890
1992
  }
1891
1993
  async function searched(input, reach) {
1892
1994
  const plan = searchPlanOf(input);
1893
1995
  const selected = plan.documents === undefined && reach === null
1894
1996
  ? null
1895
- : (await readSide.query(filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)))).documents;
1997
+ : (await rowsQuery(filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)))).documents;
1998
+ if (selected === null)
1999
+ materialize(null);
1896
2000
  return searchIndex.search(reach === null || reach.conversations ? plan : { ...plan, conversations: [] }, selected);
1897
2001
  }
1898
2002
  const watch = (topic, input, deliver) => {
@@ -1996,7 +2100,7 @@ export function composeRuntime(options) {
1996
2100
  rooms() {
1997
2101
  return listed();
1998
2102
  },
1999
- query: ((input) => readSide.query(planned(input, null))),
2103
+ query: ((input) => rowsQuery(planned(input, null))),
2000
2104
  search: (input) => searched(input, null),
2001
2105
  types(locator) {
2002
2106
  return {
@@ -2025,14 +2129,11 @@ export function composeRuntime(options) {
2025
2129
  return exclusive(async () => {
2026
2130
  const parts = readParts(input);
2027
2131
  if (path === '' || normalize(path) !== path)
2028
- throw new Error('room-draft-malformed');
2132
+ throw runtimeError('room-draft-malformed');
2029
2133
  const local = await openLocal();
2030
2134
  const current = roomDrafts.get(room)?.get(path);
2031
2135
  if ((current?.revision ?? null) !== revision) {
2032
- return {
2033
- outcome: 'stale',
2034
- current: current === undefined ? null : roomDraftOf(current),
2035
- };
2136
+ return staleWrite(current === undefined ? null : roomDraftOf(current));
2036
2137
  }
2037
2138
  const record = {
2038
2139
  locator: room,
@@ -2052,10 +2153,7 @@ export function composeRuntime(options) {
2052
2153
  const local = await openLocal();
2053
2154
  const current = roomDrafts.get(room)?.get(path);
2054
2155
  if (current === undefined || current.revision !== revision) {
2055
- return {
2056
- outcome: 'stale',
2057
- current: current === undefined ? null : roomDraftOf(current),
2058
- };
2156
+ return staleWrite(current === undefined ? null : roomDraftOf(current));
2059
2157
  }
2060
2158
  await local.discardRoomDraft(room, path);
2061
2159
  forgetRoomDraft(room, path);
@@ -2123,7 +2221,7 @@ export function composeRuntime(options) {
2123
2221
  md,
2124
2222
  watch,
2125
2223
  plan: planned,
2126
- query: (plan) => readSide.query(plan),
2224
+ query: (plan) => rowsQuery(plan),
2127
2225
  watchPlan,
2128
2226
  search: searched,
2129
2227
  };