@agentshouse/mdruntime 0.1.0-c0bed82db8d73 → 0.1.0-c0c9861edaba9

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 (48) hide show
  1. package/README.md +2 -1
  2. package/dist/adapter.d.ts +0 -1
  3. package/dist/adapter.d.ts.map +1 -1
  4. package/dist/attached-model.d.ts +1 -0
  5. package/dist/attached-model.d.ts.map +1 -1
  6. package/dist/attached-model.js +8 -1
  7. package/dist/bootstrap.d.ts +8 -4
  8. package/dist/bootstrap.d.ts.map +1 -1
  9. package/dist/bootstrap.js +76 -41
  10. package/dist/collection.d.ts.map +1 -1
  11. package/dist/collection.js +1 -2
  12. package/dist/compile.d.ts.map +1 -1
  13. package/dist/compile.js +65 -19
  14. package/dist/connection.d.ts.map +1 -1
  15. package/dist/connection.js +228 -217
  16. package/dist/conversation.d.ts.map +1 -1
  17. package/dist/conversation.js +5 -2
  18. package/dist/live.d.ts.map +1 -1
  19. package/dist/live.js +2 -1
  20. package/dist/memory.d.ts +1 -1
  21. package/dist/memory.d.ts.map +1 -1
  22. package/dist/memory.js +2 -6
  23. package/dist/model.d.ts.map +1 -1
  24. package/dist/model.js +1 -1
  25. package/dist/query.d.ts +6 -0
  26. package/dist/query.d.ts.map +1 -1
  27. package/dist/reach.d.ts +6 -0
  28. package/dist/reach.d.ts.map +1 -1
  29. package/dist/reach.js +23 -6
  30. package/dist/refresh.d.ts.map +1 -1
  31. package/dist/refresh.js +1 -5
  32. package/dist/replica.d.ts +1 -2
  33. package/dist/replica.d.ts.map +1 -1
  34. package/dist/replica.js +2 -2
  35. package/dist/rows.d.ts +15 -5
  36. package/dist/rows.d.ts.map +1 -1
  37. package/dist/rows.js +102 -71
  38. package/dist/store.d.ts +4 -9
  39. package/dist/store.d.ts.map +1 -1
  40. package/dist/store.js +10 -7
  41. package/dist/stored.d.ts +10 -3
  42. package/dist/stored.d.ts.map +1 -1
  43. package/dist/stored.js +110 -70
  44. package/dist/tables.d.ts +5 -7
  45. package/dist/tables.d.ts.map +1 -1
  46. package/dist/tables.js +5 -5
  47. package/package.json +1 -2
  48. package/AGENTS.md +0 -312
@@ -1,5 +1,5 @@
1
1
  import { escapePointer, parseFrontmatter, roomTarget, target } from '@agentshouse/mdmodel';
2
- import { attachedModel, NO_ROOM_TYPES, roomTypesOf } from './attached-model.js';
2
+ import { attachedModel, builtInModel, roomTypesOf } from './attached-model.js';
3
3
  import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
4
4
  import { observing, whole } from './observation.js';
5
5
  import { filterOf, identityOf, planOf, plain, queryKey, } from './query.js';
@@ -8,7 +8,7 @@ import { seededResult } from './seed.js';
8
8
  import { documentKey, roomOfKey } from './rows.js';
9
9
  import { searchPlanOf } from './search.js';
10
10
  import { capped, MAXIMUM_WINDOW } from './windows.js';
11
- import { bootstrapRoom } from './bootstrap.js';
11
+ import { bootstrapRoom, NOTHING_DONE } from './bootstrap.js';
12
12
  import { changeSet, mergeChangeSets } from './change-set.js';
13
13
  import { createConversationStore, } from './conversation.js';
14
14
  import { leaving as leavingIn, partnerOf, queuedFile, } from './change.js';
@@ -54,7 +54,10 @@ const CONVERSATION_LANE = 'conversations';
54
54
  const ROOM_READS = 256;
55
55
  const ROOM_ATTEMPTS = 16;
56
56
  const ERASE_BATCH = 512;
57
- const REDERIVE_BATCH = 256;
57
+ const DERIVE_BYTES = 1 << 23;
58
+ const DERIVE_FLOOR = 1 << 18;
59
+ // An index build aims at this much database work, so a Save, a query or the next part's progress waits behind at most one short build.
60
+ const DERIVE_AIM_MS = 250;
58
61
  const WALK_BYTES = 1 << 20;
59
62
  const QUERY_BYTES = 1 << 20;
60
63
  const STAGE_BYTES = 1 << 20;
@@ -62,14 +65,9 @@ const STAGE_FLOOR = 1 << 16;
62
65
  // A batch aims at this much database work, so a Save or a query waits behind at most one short write.
63
66
  const STAGE_AIM_MS = 50;
64
67
  const PARTS_HELD = 4;
68
+ const QUEUED_BYTES = 1 << 23;
65
69
  // Bytes still arriving are reported at most this often, well inside the 2 s progress bound, so readiness is not rebuilt for every chunk the network hands over.
66
70
  const ARRIVAL_REPORT_MS = 250;
67
- const INSTALL_STEPS = [
68
- 'writing',
69
- 'downloading',
70
- 'replaying',
71
- 'listing',
72
- ];
73
71
  const CLEAN_CHANGES = 2_000;
74
72
  // A Room that locks offline stays readable this long after the connection drops, so a laptop waking or changing network keeps working while a disconnected device locks soon after.
75
73
  const LOCK_GRACE_MS = 120_000;
@@ -114,9 +112,9 @@ export function composeRuntime(options) {
114
112
  const rooms = new FencedMap(isFenced);
115
113
  const counted = new Map();
116
114
  const unreadable = new Set();
117
- const isModelFenced = (key) => isFenced(key.slice(key.indexOf(' ') + 1));
118
- const models = new FencedMap(isModelFenced);
119
- const liveModels = new FencedMap(isModelFenced);
115
+ const builtIn = builtInModel(roomTypes);
116
+ const models = new FencedMap(isFenced);
117
+ const liveModels = new FencedMap(isFenced);
120
118
  const invalidModels = new Set();
121
119
  const discovered = new Map();
122
120
  const reads = doorReads(options.adapter, (locator) => discovered.get(locator), () => generation);
@@ -124,23 +122,19 @@ export function composeRuntime(options) {
124
122
  const scopedRefusals = new Map();
125
123
  const sourceRefusals = new Map();
126
124
  const declarationRefusals = new Set();
127
- function modelKey(locator, attached) {
128
- return `${attached} ${locator}`;
125
+ function modelOf(locator) {
126
+ return models.get(locator) ?? NO_MODEL;
129
127
  }
130
- function modelOf(locator, attached = 'collection') {
131
- return models.get(modelKey(locator, attached)) ?? NO_MODEL;
132
- }
133
- function savedModel(locator, attached) {
134
- return invalidModels.has(modelKey(locator, attached))
128
+ function savedModel(locator) {
129
+ return invalidModels.has(locator)
135
130
  ? { status: 'unavailable', reason: 'revoked' }
136
- : modelOf(locator, attached);
131
+ : modelOf(locator);
132
+ }
133
+ function composed(record) {
134
+ return compositionOf(record, builtIn);
137
135
  }
138
136
  function compositionFor(record) {
139
- return compositionOf({
140
- ...record,
141
- model: savedModel(record.locator, 'collection'),
142
- selectionModel: savedModel(record.locator, 'typed-selection'),
143
- });
137
+ return composed({ ...record, model: savedModel(record.locator) });
144
138
  }
145
139
  let refusal = null;
146
140
  let storageLoss = null;
@@ -218,13 +212,27 @@ export function composeRuntime(options) {
218
212
  const catching = new Set();
219
213
  const transfers = new FencedMap(isFenced);
220
214
  const installs = new Map();
215
+ const keptParts = new Map();
221
216
  const arrivals = new Map();
217
+ const underived = new Map();
222
218
  let arrivalReported = 0;
223
219
  let onScreen = options.shell.onScreen;
224
220
  const shownWaiters = [];
225
221
  let partsFree = PARTS_HELD;
222
+ let queuedBytes = 0;
226
223
  const partWaiters = [];
227
- let stageBytes = STAGE_FLOOR * 4;
224
+ const stageBudget = {
225
+ bytes: STAGE_FLOOR * 4,
226
+ floor: STAGE_FLOOR,
227
+ ceiling: STAGE_BYTES,
228
+ aim: STAGE_AIM_MS,
229
+ };
230
+ const deriveBudget = {
231
+ bytes: DERIVE_FLOOR * 4,
232
+ floor: DERIVE_FLOOR,
233
+ ceiling: DERIVE_BYTES,
234
+ aim: DERIVE_AIM_MS,
235
+ };
228
236
  let installWrites = Promise.resolve();
229
237
  const conversations = createConversationStore({
230
238
  agents: declaredAgents,
@@ -276,7 +284,7 @@ export function composeRuntime(options) {
276
284
  return undefined;
277
285
  const local = await openLocal();
278
286
  const slice = await stepped(slicing(await local.entries(held.record.key, 'visible', paths), paths));
279
- return { ...slice, collection: collectionOf(slice.documents, compositionOf(held.record)) };
287
+ return { ...slice, collection: collectionOf(slice.documents, composed(held.record)) };
280
288
  }
281
289
  function roomDraftOf(record, visible) {
282
290
  return structuredClone({
@@ -483,7 +491,7 @@ export function composeRuntime(options) {
483
491
  const governed = governs(reach, room);
484
492
  const attached = [
485
493
  ...(governed.collection ? [modelOf(room.locator)] : []),
486
- ...(governed.selection ? [modelOf(room.locator, 'typed-selection')] : []),
494
+ ...(governed.selection ? [builtIn] : []),
487
495
  ];
488
496
  return {
489
497
  locator: room.locator,
@@ -567,7 +575,10 @@ export function composeRuntime(options) {
567
575
  if (held === undefined || closed || held.indexed)
568
576
  return;
569
577
  const key = held.record.key;
570
- const done = await exclusive(async () => (await openLocal()).rederive(key, REDERIVE_BATCH));
578
+ const done = await exclusive(async () => {
579
+ const local = await openLocal();
580
+ return timed(deriveBudget, () => local.rederive(key, deriveBudget.bytes));
581
+ });
571
582
  const now = replicas.get(locator);
572
583
  if (now?.record.key !== key)
573
584
  continue;
@@ -610,7 +621,11 @@ export function composeRuntime(options) {
610
621
  })();
611
622
  }
612
623
  function roomsOfReach(reach) {
613
- return new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
624
+ return new Set([
625
+ ...reach.rooms,
626
+ ...reach.documents.map(roomOfKey),
627
+ ...reach.areas.flatMap((area) => (area.types.length === 0 ? [] : [area.room])),
628
+ ]);
614
629
  }
615
630
  function reachedRooms(plan) {
616
631
  const reached = plan.reach === null ? null : roomsOfReach(plan.reach);
@@ -729,7 +744,9 @@ export function composeRuntime(options) {
729
744
  const parts = { done: 0, total: 0 };
730
745
  const bytes = { done: 0, total: 0 };
731
746
  const received = { done: 0, total: 0 };
732
- const steps = new Set();
747
+ let installing = 0;
748
+ let unlisted = false;
749
+ let listed = false;
733
750
  let waiting = false;
734
751
  for (const { room, state } of coverage) {
735
752
  if (state === 'unadmitted')
@@ -738,30 +755,40 @@ export function composeRuntime(options) {
738
755
  if (state === 'complete')
739
756
  rooms.done += 1;
740
757
  const transfer = transfers.get(room);
741
- const held = installs.get(room);
742
- const inventory = transfer?.inventory ?? replicas.get(room)?.record.inventory ?? null;
758
+ const replica = replicas.get(room)?.record.inventory ?? null;
759
+ const inventory = transfer?.inventory ?? replica ?? installs.get(room) ?? null;
760
+ const underway = state === 'acquiring' || (installs.has(room) && state !== 'complete');
761
+ if (underway)
762
+ installing += 1;
763
+ if (underway && inventory === null)
764
+ unlisted = true;
743
765
  if (inventory !== null) {
744
- const done = transfer?.done ?? inventory;
745
- const share = held?.share ?? null;
766
+ listed = true;
767
+ const done = transfer?.done ?? (replica === null ? NOTHING_DONE : inventory);
746
768
  parts.total += inventory.parts;
747
769
  bytes.total += inventory.bytes;
748
770
  parts.done += done.parts;
749
771
  bytes.done += done.bytes;
750
- if (share !== null && share.entries > 0) {
751
- bytes.done += Math.floor((share.bytes * share.written) / share.entries);
752
- }
753
772
  received.total += inventory.bytes;
754
- received.done += done.bytes + (share?.bytes ?? 0);
755
- for (const arrived of arrivals.get(room)?.values() ?? [])
756
- received.done += arrived;
773
+ received.done += done.bytes;
774
+ for (const held of [keptParts.get(room), arrivals.get(room)]) {
775
+ for (const arrived of held?.values() ?? [])
776
+ received.done += arrived;
777
+ }
757
778
  }
758
- const installing = held !== undefined && state !== 'complete';
759
- if (installing)
760
- steps.add(held.step);
761
- waiting ||= installing || state === 'acquiring';
779
+ waiting ||= underway;
762
780
  }
781
+ const step = installing === 0
782
+ ? null
783
+ : !listed
784
+ ? 'listing'
785
+ : unlisted || received.done < received.total
786
+ ? 'downloading'
787
+ : bytes.done < bytes.total
788
+ ? 'writing'
789
+ : 'replaying';
763
790
  return {
764
- step: INSTALL_STEPS.find((step) => steps.has(step)) ?? null,
791
+ step,
765
792
  paused: waiting && !onScreen ? 'hidden' : null,
766
793
  rooms,
767
794
  parts,
@@ -1029,7 +1056,7 @@ export function composeRuntime(options) {
1029
1056
  const records = [...replicas].map(([locator, held]) => ({
1030
1057
  locator,
1031
1058
  held,
1032
- record: { ...held.record, model: revoked, selectionModel: revoked },
1059
+ record: { ...held.record, model: revoked },
1033
1060
  }));
1034
1061
  let answer = null;
1035
1062
  try {
@@ -1039,18 +1066,14 @@ export function composeRuntime(options) {
1039
1066
  locator,
1040
1067
  key: record.key,
1041
1068
  record: storedRecord(record),
1042
- composition: compositionOf(record),
1069
+ composition: composed(record),
1043
1070
  })),
1044
1071
  });
1045
1072
  }
1046
1073
  catch {
1047
1074
  journal.record('model revocation persistence failed');
1048
1075
  }
1049
- const keys = new Set(models.keys());
1050
- for (const locator of rooms.keys()) {
1051
- keys.add(modelKey(locator, 'collection'));
1052
- keys.add(modelKey(locator, 'typed-selection'));
1053
- }
1076
+ const keys = new Set([...models.keys(), ...rooms.keys()]);
1054
1077
  for (const key of keys) {
1055
1078
  invalidModels.add(key);
1056
1079
  models.set(key, revoked);
@@ -1105,7 +1128,7 @@ export function composeRuntime(options) {
1105
1128
  ? null
1106
1129
  : {
1107
1130
  roots: selection.roots,
1108
- model: modelTypes(modelOf(room.locator, 'typed-selection')),
1131
+ model: modelTypes(builtIn),
1109
1132
  },
1110
1133
  providerOwnedPaths: room.providerOwnedPaths,
1111
1134
  roomVersion: replicas.get(room.locator)?.record.roomVersion ?? null,
@@ -1194,7 +1217,7 @@ export function composeRuntime(options) {
1194
1217
  queuesWithoutReplica.delete(locator);
1195
1218
  rooms.delete(locator);
1196
1219
  roomDrafts.delete(locator);
1197
- transfers.delete(locator);
1220
+ untransferred(locator);
1198
1221
  catching.delete(locator);
1199
1222
  discovered.delete(locator);
1200
1223
  announced.delete(locator);
@@ -1205,12 +1228,9 @@ export function composeRuntime(options) {
1205
1228
  scopedRefusals.delete(locator);
1206
1229
  sourceRefusals.delete(locator);
1207
1230
  declarationRefusals.delete(locator);
1208
- for (const attached of ['collection', 'typed-selection']) {
1209
- const key = modelKey(locator, attached);
1210
- models.delete(key);
1211
- liveModels.delete(key);
1212
- invalidModels.delete(key);
1213
- }
1231
+ models.delete(locator);
1232
+ liveModels.delete(locator);
1233
+ invalidModels.delete(locator);
1214
1234
  journal.record(`room lost ${locator}`);
1215
1235
  }
1216
1236
  if (fresh.length === 0)
@@ -1258,7 +1278,14 @@ export function composeRuntime(options) {
1258
1278
  });
1259
1279
  await eraseFenced(fresh);
1260
1280
  }
1261
- function restored(key, persisted) {
1281
+ function retyped(persisted) {
1282
+ if (persisted.status === 'unavailable')
1283
+ return persisted;
1284
+ const { state } = attachedModel({ kind: 'model', snapshot: persisted.snapshot }, NO_MODEL, roomTypes);
1285
+ return state.status === 'unavailable' ? state : { ...persisted, types: state.types };
1286
+ }
1287
+ function restored(key, held) {
1288
+ const persisted = retyped(held);
1262
1289
  const prior = models.get(key);
1263
1290
  if (prior !== undefined &&
1264
1291
  prior.status !== 'unavailable' &&
@@ -1291,6 +1318,10 @@ export function composeRuntime(options) {
1291
1318
  transfers.clear();
1292
1319
  for (const [locator, record] of held.transfers)
1293
1320
  transfers.set(locator, record);
1321
+ arrivals.clear();
1322
+ keptParts.clear();
1323
+ for (const [locator, parts] of held.kept)
1324
+ keptParts.set(locator, new Map(parts));
1294
1325
  limits = held.providerLimits;
1295
1326
  capabilities = held.capabilities;
1296
1327
  member = held.member;
@@ -1316,9 +1347,7 @@ export function composeRuntime(options) {
1316
1347
  records.forEach((stored, index) => {
1317
1348
  if (fences.has(stored.locator))
1318
1349
  return;
1319
- const model = restored(modelKey(stored.locator, 'collection'), stored.model);
1320
- const selectionModel = restored(modelKey(stored.locator, 'typed-selection'), stored.selectionModel);
1321
- const record = { ...stored, model, selectionModel };
1350
+ const record = { ...stored, model: restored(stored.locator, stored.model) };
1322
1351
  replicas.set(record.locator, {
1323
1352
  record,
1324
1353
  queue: queuedBatches.get(record.locator) ?? [],
@@ -1336,13 +1365,10 @@ export function composeRuntime(options) {
1336
1365
  roomVersion: record.roomVersion,
1337
1366
  operations: record.operations,
1338
1367
  replica: record.replica,
1339
- model: modelTypes(model),
1368
+ model: modelTypes(record.model),
1340
1369
  typedSelection: record.typedSelection === null
1341
1370
  ? null
1342
- : {
1343
- roots: record.typedSelection.roots,
1344
- model: modelTypes(selectionModel),
1345
- },
1371
+ : { roots: record.typedSelection.roots, model: modelTypes(builtIn) },
1346
1372
  });
1347
1373
  }
1348
1374
  });
@@ -1529,7 +1555,7 @@ export function composeRuntime(options) {
1529
1555
  return { count: answer.counts[locator] ?? replicas.get(locator)?.count ?? 0, indexed };
1530
1556
  }
1531
1557
  function replayed(record, confirmedLayer, queue) {
1532
- return stepped(replaying(confirmedLayer, queue, record.locator, compositionOf(record)));
1558
+ return stepped(replaying(confirmedLayer, queue, record.locator, composed(record)));
1533
1559
  }
1534
1560
  async function overlayDiff(record, before, visible, touched) {
1535
1561
  const kept = before !== undefined && before.record.key === record.key
@@ -1592,7 +1618,7 @@ export function composeRuntime(options) {
1592
1618
  const confirmedLayer = overrides === undefined
1593
1619
  ? read
1594
1620
  : await stepped(slicingWith(read, overrides.set, overrides.removed));
1595
- const composition = compositionOf(record);
1621
+ const composition = composed(record);
1596
1622
  const { withdrawn, kept } = await stepped(standing(confirmedLayer, queue, withdrawable(locator), localSaves, locator, composition));
1597
1623
  const founded = kept.filter((batch) => !queue.includes(batch));
1598
1624
  const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
@@ -1698,35 +1724,25 @@ export function composeRuntime(options) {
1698
1724
  };
1699
1725
  }
1700
1726
  function adoptModels(locator, record) {
1701
- for (const [attached, model] of [
1702
- ['collection', record.model],
1703
- ['typed-selection', record.selectionModel],
1704
- ]) {
1705
- const key = modelKey(locator, attached);
1706
- models.set(key, model);
1707
- invalidModels.delete(key);
1708
- if (model.status === 'current')
1709
- liveModels.set(key, JSON.stringify(model.snapshot));
1710
- else if (model.status === 'unavailable')
1711
- liveModels.delete(key);
1712
- }
1727
+ const model = record.model;
1728
+ models.set(locator, model);
1729
+ invalidModels.delete(locator);
1730
+ if (model.status === 'current')
1731
+ liveModels.set(locator, JSON.stringify(model.snapshot));
1732
+ else if (model.status === 'unavailable')
1733
+ liveModels.delete(locator);
1713
1734
  }
1714
1735
  function remodel(locator, answers, held) {
1715
- const taken = (answer, previous, attached) => {
1716
- if (answer === undefined)
1717
- return previous;
1718
- const { state, refused } = attachedModel(answer, previous, attached === 'collection' ? roomTypes : NO_ROOM_TYPES);
1719
- if (refused !== null) {
1720
- const where = refused.location === undefined ? '' : ` ${refused.location}`;
1721
- journal.record(`model invalid ${locator} ${attached} ${refused.identity} ${refused.rule}${where}`);
1722
- }
1723
- models.set(modelKey(locator, attached), state);
1724
- return state;
1725
- };
1726
- return {
1727
- collection: taken(answers?.collection, held.collection, 'collection'),
1728
- selection: taken(answers?.selection, held.selection, 'typed-selection'),
1729
- };
1736
+ const answer = answers?.collection;
1737
+ if (answer === undefined)
1738
+ return held;
1739
+ const { state, refused } = attachedModel(answer, held.collection, roomTypes);
1740
+ if (refused !== null) {
1741
+ const where = refused.location === undefined ? '' : ` ${refused.location}`;
1742
+ journal.record(`model invalid ${locator} collection ${refused.identity} ${refused.rule}${where}`);
1743
+ }
1744
+ models.set(locator, state);
1745
+ return { collection: state };
1730
1746
  }
1731
1747
  function publish(local, room, record, landing, live) {
1732
1748
  const landed = exclusive(async () => {
@@ -1753,14 +1769,14 @@ export function composeRuntime(options) {
1753
1769
  ? {
1754
1770
  room: true,
1755
1771
  view: 'refreshed',
1756
- commit: { refreshed: true, composition: compositionOf(record) },
1772
+ commit: { refreshed: true, composition: composed(record) },
1757
1773
  }
1758
1774
  : {
1759
1775
  room: true,
1760
1776
  view: landing.refreshed ? 'refreshed' : 'confirmed',
1761
1777
  commit: {
1762
1778
  published: { from: prior?.record.key ?? null },
1763
- composition: compositionOf(record),
1779
+ composition: composed(record),
1764
1780
  ...(landing.refreshed ? { refreshed: true } : {}),
1765
1781
  },
1766
1782
  });
@@ -1772,7 +1788,7 @@ export function composeRuntime(options) {
1772
1788
  return true;
1773
1789
  }
1774
1790
  if (landing.kind === 'bootstrapped') {
1775
- transfers.delete(room.locator);
1791
+ untransferred(room.locator);
1776
1792
  catching.delete(room.locator);
1777
1793
  sweep();
1778
1794
  }
@@ -1870,7 +1886,7 @@ export function composeRuntime(options) {
1870
1886
  : (await local.referrers(record.key, 'confirmed', removed, CHECKED_LINKS)).map(({ path }) => path);
1871
1887
  const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...referring], moved);
1872
1888
  const { visible } = await replayed(record, confirmedLayer, replica.queue);
1873
- const composition = compositionOf(record);
1889
+ const composition = composed(record);
1874
1890
  return {
1875
1891
  record,
1876
1892
  confirmed: confirmedLayer,
@@ -1886,13 +1902,7 @@ export function composeRuntime(options) {
1886
1902
  return false;
1887
1903
  const local = await openLocal();
1888
1904
  const roots = record.typedSelection?.roots ?? [];
1889
- if (composition.model.status === 'unavailable' &&
1890
- (await local.typedWithin(record.key, 'visible', roots, false))) {
1891
- return true;
1892
- }
1893
- return (composition.selection !== null &&
1894
- composition.selection.model.status === 'unavailable' &&
1895
- (await local.typedWithin(record.key, 'visible', roots, true)));
1905
+ return local.typedWithin(record.key, 'visible', roots, false);
1896
1906
  }
1897
1907
  function textDiffers(left, right) {
1898
1908
  return ((left?.content ?? undefined) !== (right?.content ?? undefined) ||
@@ -1997,7 +2007,7 @@ export function composeRuntime(options) {
1997
2007
  const lookup = overriddenLookup(lookupOf(local, record.key, 'confirmed'), overrides);
1998
2008
  const clean = available(composition) &&
1999
2009
  changed.length <= CLEAN_CHANGES &&
2000
- (await changedCleanly(changed, compositionOf(record), composition, {
2010
+ (await changedCleanly(changed, composed(record), composition, {
2001
2011
  lookup,
2002
2012
  texts: textsOf(local, record.key, 'confirmed'),
2003
2013
  referrers: referrersOf(local, record.key, 'confirmed'),
@@ -2754,8 +2764,8 @@ export function composeRuntime(options) {
2754
2764
  }
2755
2765
  }
2756
2766
  async function validatedRoom(local, record, view, previous, overrides = null) {
2757
- const composition = compositionOf(record);
2758
- const modeled = record.model.status !== 'unavailable' || record.selectionModel.status !== 'unavailable';
2767
+ const composition = composed(record);
2768
+ const modeled = record.model.status !== 'unavailable' || record.typedSelection !== null;
2759
2769
  const lookup = overrides === null
2760
2770
  ? lookupOf(local, record.key, view)
2761
2771
  : overriddenLookup(lookupOf(local, record.key, view), overrides);
@@ -2789,11 +2799,11 @@ export function composeRuntime(options) {
2789
2799
  }
2790
2800
  const named = changes === null ? null : changes.map(({ path }) => path);
2791
2801
  const textChanged = changes === null || changes.length > 0 || reshaped;
2792
- const modelsChanged = previous === null || !sameComposition(compositionOf(previous.record), composition);
2802
+ const modelsChanged = previous === null || !sameComposition(composed(previous.record), composition);
2793
2803
  if (modeled && (textChanged || modelsChanged)) {
2794
2804
  const clean = previous !== null &&
2795
2805
  changes !== null &&
2796
- (await changedCleanly(changes, compositionOf(previous.record), composition, {
2806
+ (await changedCleanly(changes, composed(previous.record), composition, {
2797
2807
  lookup,
2798
2808
  texts,
2799
2809
  referrers: referrersOf(local, record.key, 'confirmed'),
@@ -2817,7 +2827,7 @@ export function composeRuntime(options) {
2817
2827
  async turn() {
2818
2828
  if (replaying === null)
2819
2829
  return;
2820
- installing(replaying, 'replaying');
2830
+ installing(replaying);
2821
2831
  await screened();
2822
2832
  },
2823
2833
  prior: async (paths) => new Map((await local.entries(key, 'refreshed', paths)).map((entry) => [entry.path, entry])),
@@ -2831,7 +2841,7 @@ export function composeRuntime(options) {
2831
2841
  const staged = await write(async () => {
2832
2842
  if (!liveness())
2833
2843
  return false;
2834
- await timed(() => local.refresh(key, last
2844
+ await timed(stageBudget, () => local.refresh(key, last
2835
2845
  ? { ...page, entries }
2836
2846
  : { entries, removals: [], folders: [], folderRemovals: [] }));
2837
2847
  return true;
@@ -2850,21 +2860,21 @@ export function composeRuntime(options) {
2850
2860
  let spent = 0;
2851
2861
  while (end < entries.length) {
2852
2862
  const size = (entries[end].content?.length ?? 0) + ROW_WEIGHT;
2853
- if (spent + size > stageBytes && end > from)
2863
+ if (spent + size > stageBudget.bytes && end > from)
2854
2864
  break;
2855
2865
  spent += size;
2856
2866
  end += 1;
2857
2867
  }
2858
2868
  return end;
2859
2869
  }
2860
- async function timed(work) {
2870
+ async function timed(budget, work) {
2861
2871
  const started = performance.now();
2862
2872
  const done = await work();
2863
2873
  const spent = performance.now() - started;
2864
- if (spent < STAGE_AIM_MS / 2)
2865
- stageBytes = Math.min(STAGE_BYTES, Math.round(stageBytes * 1.5));
2866
- else if (spent > STAGE_AIM_MS * 2) {
2867
- stageBytes = Math.max(STAGE_FLOOR, Math.round(stageBytes / 1.5));
2874
+ if (spent < budget.aim / 2)
2875
+ budget.bytes = Math.min(budget.ceiling, Math.round(budget.bytes * 1.5));
2876
+ else if (spent > budget.aim * 2) {
2877
+ budget.bytes = Math.max(budget.floor, Math.round(budget.bytes / 1.5));
2868
2878
  }
2869
2879
  return done;
2870
2880
  }
@@ -2880,25 +2890,34 @@ export function composeRuntime(options) {
2880
2890
  return new Promise((resolve) => shownWaiters.push(resolve));
2881
2891
  }
2882
2892
  async function heldPart() {
2883
- while (partsFree === 0)
2893
+ while (partsFree === 0 || queuedBytes >= QUEUED_BYTES) {
2884
2894
  await new Promise((admit) => partWaiters.push(admit));
2895
+ }
2885
2896
  partsFree -= 1;
2886
- return freed();
2897
+ return once(() => {
2898
+ partsFree += 1;
2899
+ });
2887
2900
  }
2888
- function freed() {
2901
+ function once(release) {
2889
2902
  let released = false;
2890
2903
  return () => {
2891
2904
  if (released)
2892
2905
  return;
2893
2906
  released = true;
2894
- partsFree += 1;
2895
- partWaiters.shift()?.();
2907
+ release();
2908
+ for (const admit of partWaiters.splice(0))
2909
+ admit();
2896
2910
  };
2897
2911
  }
2898
- function installing(locator, step, share = null) {
2899
- installs.set(locator, { step, share });
2912
+ function installing(locator, inventory = null) {
2913
+ installs.set(locator, inventory ?? installs.get(locator) ?? null);
2900
2914
  queueReadiness();
2901
2915
  }
2916
+ function untransferred(locator) {
2917
+ transfers.delete(locator);
2918
+ keptParts.delete(locator);
2919
+ underived.delete(locator);
2920
+ }
2902
2921
  function installed(locator) {
2903
2922
  if (installs.delete(locator))
2904
2923
  queueReadiness();
@@ -2965,11 +2984,34 @@ export function composeRuntime(options) {
2965
2984
  heldContents.set(key, made);
2966
2985
  return made;
2967
2986
  }
2987
+ async function keptTogether({ local, parts }) {
2988
+ const live = parts.filter((one) => one.live() && transfers.has(one.locator));
2989
+ await local.keepParts(live.map(({ locator, token, part }) => ({ locator, token, part })));
2990
+ for (const { locator, token, part } of live) {
2991
+ const held = keptParts.get(locator) ?? new Map();
2992
+ keptParts.set(locator, held.set(token, part.byteLength));
2993
+ arrivals.get(locator)?.delete(token);
2994
+ }
2995
+ queueReadiness();
2996
+ }
2968
2997
  function staging(local, liveness) {
2998
+ const derived = async (locator, key) => {
2999
+ underived.delete(locator);
3000
+ for (;;) {
3001
+ await screened();
3002
+ if (!liveness())
3003
+ return false;
3004
+ const done = await installWrite(async () => liveness() ? timed(deriveBudget, () => local.rederive(key, deriveBudget.bytes)) : null);
3005
+ if (done === null)
3006
+ return false;
3007
+ if (done)
3008
+ return true;
3009
+ }
3010
+ };
2969
3011
  return {
2970
3012
  held: (locator) => transfers.get(locator) ?? null,
2971
3013
  async turn(locator, step) {
2972
- installing(locator, step);
3014
+ installing(locator);
2973
3015
  for (;;) {
2974
3016
  await screened();
2975
3017
  const release = step === 'downloading' ? await heldPart() : () => { };
@@ -2978,17 +3020,17 @@ export function composeRuntime(options) {
2978
3020
  release();
2979
3021
  }
2980
3022
  },
2981
- spare() {
2982
- if (!onScreen || closed || partsFree === 0)
2983
- return null;
2984
- partsFree -= 1;
2985
- return freed();
3023
+ queued(bytes) {
3024
+ queuedBytes += bytes;
3025
+ return once(() => {
3026
+ queuedBytes -= bytes;
3027
+ });
2986
3028
  },
2987
3029
  arriving(locator) {
2988
3030
  let ended = false;
2989
3031
  return {
2990
3032
  at(token, bytes) {
2991
- if (ended)
3033
+ if (ended || keptParts.get(locator)?.has(token) === true)
2992
3034
  return;
2993
3035
  const held = arrivals.get(locator) ?? new Map();
2994
3036
  arrivals.set(locator, held.set(token, bytes));
@@ -3006,64 +3048,44 @@ export function composeRuntime(options) {
3006
3048
  };
3007
3049
  },
3008
3050
  received: (locator, token) => local.transferPart(locator, token),
3009
- keep: (locator, token, received) => installWrite(async () => {
3010
- if (liveness())
3011
- await local.keepPart(locator, token, received);
3012
- }),
3051
+ keep: (locator, token, received) => gathered('kept parts', { local, parts: [{ locator, token, part: received, live: liveness }] }, (earlier, later) => earlier.local === later.local
3052
+ ? { local: earlier.local, parts: [...earlier.parts, ...later.parts] }
3053
+ : null, keptTogether),
3013
3054
  announced(locator) {
3014
3055
  const later = announced.get(locator);
3015
3056
  return later === undefined ? [] : [later];
3016
3057
  },
3017
- async stage(record, page, composition, received) {
3058
+ async stage(record, page, composition, written) {
3018
3059
  const { locator } = record;
3019
- const whole = received?.byteLength ?? 0;
3020
- const from = record.writing?.written ?? 0;
3021
- const token = record.writing?.token ?? null;
3022
- const entries = page.entries.length;
3023
- let held = record;
3024
- if (token !== null)
3025
- arrivals.get(locator)?.delete(token);
3026
- installing(locator, 'writing', { written: from, entries, bytes: whole });
3027
- for (let written = from;;) {
3028
- await screened();
3060
+ installing(locator, record.inventory);
3061
+ await screened();
3062
+ if (!liveness())
3063
+ return null;
3064
+ const key = await installWrite(async () => {
3029
3065
  if (!liveness())
3030
3066
  return null;
3031
- const end = batchEnd(page.entries, written);
3032
- const last = end === entries;
3033
- const next = last
3034
- ? {
3035
- ...held,
3036
- writing: null,
3037
- done: {
3038
- parts: held.done.parts + (received === null ? 0 : 1),
3039
- bytes: held.done.bytes + whole,
3040
- },
3041
- }
3042
- : { ...held, writing: { token, written: end } };
3043
- const key = await installWrite(async () => {
3044
- if (!liveness())
3045
- return null;
3046
- return timed(() => local.stage(next, composition, {
3047
- entries: page.entries.slice(written, end),
3048
- folders: last ? page.folders : [],
3049
- }, last && received !== null && token !== null ? token : undefined));
3050
- });
3051
- if (key === null)
3052
- return null;
3053
- const arrived = !transfers.has(locator);
3054
- held = { ...next, key };
3055
- transfers.set(locator, held);
3056
- if (arrived)
3057
- liveQueries.recover();
3058
- if (last) {
3059
- installing(locator, held.pageToken === null ? 'replaying' : 'downloading');
3060
- liveQueries.recover();
3061
- return held;
3062
- }
3063
- installing(locator, 'writing', { written: end, entries, bytes: whole });
3064
- written = end;
3067
+ return local.stage(record, composition, page, written ?? undefined);
3068
+ });
3069
+ if (key === null || !liveness())
3070
+ return null;
3071
+ const arrived = !transfers.has(locator);
3072
+ const held = { ...record, key };
3073
+ transfers.set(locator, held);
3074
+ if (written !== null) {
3075
+ keptParts.get(locator)?.delete(written);
3076
+ arrivals.get(locator)?.delete(written);
3065
3077
  }
3078
+ if (arrived)
3079
+ liveQueries.recover();
3080
+ installing(locator);
3081
+ liveQueries.recover();
3082
+ const owed = page.entries.reduce((sum, entry) => sum + (entry.content?.length ?? 0), underived.get(locator) ?? 0);
3083
+ underived.set(locator, owed);
3084
+ if (owed < deriveBudget.bytes)
3085
+ return held;
3086
+ return (await derived(locator, key)) ? held : null;
3066
3087
  },
3088
+ derive: derived,
3067
3089
  async refreshing(locator, key) {
3068
3090
  await exclusive(() => local.unrefresh(key));
3069
3091
  return refreshingOf(local, key, liveness, locator);
@@ -3072,7 +3094,7 @@ export function composeRuntime(options) {
3072
3094
  if (!liveness())
3073
3095
  return;
3074
3096
  await local.abandon(locator);
3075
- transfers.delete(locator);
3097
+ untransferred(locator);
3076
3098
  liveQueries.recover();
3077
3099
  sweep();
3078
3100
  }),
@@ -3080,7 +3102,7 @@ export function composeRuntime(options) {
3080
3102
  if (!liveness())
3081
3103
  return;
3082
3104
  await local.abandon(locator, true);
3083
- transfers.delete(locator);
3105
+ untransferred(locator);
3084
3106
  liveQueries.recover();
3085
3107
  }),
3086
3108
  release: (key) => exclusive(async () => {
@@ -3106,10 +3128,7 @@ export function composeRuntime(options) {
3106
3128
  liveQueries.recover();
3107
3129
  }
3108
3130
  try {
3109
- const result = await bootstrapRoom(reads, authorized, room, {
3110
- collection: modelOf(room.locator),
3111
- selection: modelOf(room.locator, 'typed-selection'),
3112
- }, (models, held) => (liveness() ? remodel(room.locator, models, held) : held), liveness, staging(local, liveness), reusable(room, local));
3131
+ const result = await bootstrapRoom(reads, authorized, room, { collection: modelOf(room.locator) }, builtIn, (models, held) => (liveness() ? remodel(room.locator, models, held) : held), liveness, staging(local, liveness), reusable(room, local));
3113
3132
  const detail = result.kind === 'staged' ? result.record.roomVersion : result.kind;
3114
3133
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
3115
3134
  if (result.kind !== 'staged') {
@@ -3175,7 +3194,7 @@ export function composeRuntime(options) {
3175
3194
  options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
3176
3195
  return reached ? null : advance(room, local, authorized, live);
3177
3196
  }
3178
- await retainModel(local, room, 'collection', modelOf(room.locator));
3197
+ await retainModel(local, room, modelOf(room.locator));
3179
3198
  return null;
3180
3199
  }
3181
3200
  if (refreshed.kind !== 'beyond-horizon')
@@ -3771,9 +3790,9 @@ export function composeRuntime(options) {
3771
3790
  release();
3772
3791
  };
3773
3792
  }
3774
- async function retainModel(local, room, attached, next) {
3775
- const key = modelKey(room.locator, attached);
3776
- const previous = modelOf(room.locator, attached);
3793
+ async function retainModel(local, room, next) {
3794
+ const key = room.locator;
3795
+ const previous = modelOf(room.locator);
3777
3796
  const heldBefore = replicas.get(room.locator)?.record;
3778
3797
  if (JSON.stringify(previous) === JSON.stringify(next) &&
3779
3798
  (heldBefore === undefined ||
@@ -3797,11 +3816,11 @@ export function composeRuntime(options) {
3797
3816
  operations: room.operations,
3798
3817
  replica: room.replica,
3799
3818
  typedSelection: room.typedSelection ?? null,
3800
- ...(attached === 'collection' ? { model: next } : { selectionModel: next }),
3819
+ model: next,
3801
3820
  };
3802
3821
  let read = 'staged';
3803
3822
  if (!sameMembership(declared, held.record)) {
3804
- const verdict = await validatedCollection(walkOf(local, held.record.key, 'confirmed'), lookupOf(local, held.record.key, 'confirmed'), compositionOf(declared));
3823
+ const verdict = await validatedCollection(walkOf(local, held.record.key, 'confirmed'), lookupOf(local, held.record.key, 'confirmed'), composed(declared));
3805
3824
  read =
3806
3825
  verdict.verdict !== 'refuse'
3807
3826
  ? 'staged'
@@ -3822,7 +3841,7 @@ export function composeRuntime(options) {
3822
3841
  key: record.key,
3823
3842
  record: storedRecord(record),
3824
3843
  roots: record.typedSelection?.roots ?? [],
3825
- composition: compositionOf(record),
3844
+ composition: composed(record),
3826
3845
  overlay: diff.overlay,
3827
3846
  },
3828
3847
  ],
@@ -3869,14 +3888,14 @@ export function composeRuntime(options) {
3869
3888
  });
3870
3889
  let scopedAway = room;
3871
3890
  if (result.scope === 'model')
3872
- await retainModel(local, room, 'collection', {
3891
+ await retainModel(local, room, {
3873
3892
  status: 'unavailable',
3874
3893
  reason: 'not-readable',
3875
3894
  });
3876
3895
  else if (result.scope !== 'operation') {
3877
3896
  unreadable.add(room.locator);
3878
3897
  scopedAway = { ...room, replica: { ...room.replica, admission: 'unavailable' } };
3879
- await retainModel(local, scopedAway, 'collection', modelOf(room.locator));
3898
+ await retainModel(local, scopedAway, modelOf(room.locator));
3880
3899
  }
3881
3900
  if (!continuing())
3882
3901
  return;
@@ -3926,7 +3945,7 @@ export function composeRuntime(options) {
3926
3945
  return false;
3927
3946
  }
3928
3947
  if (prior !== undefined)
3929
- await retainModel(local, room, 'collection', modelOf(room.locator));
3948
+ await retainModel(local, room, modelOf(room.locator));
3930
3949
  if (discoveryPaused || authorization === undefined)
3931
3950
  return false;
3932
3951
  return room.replica.admission === 'admitted';
@@ -3939,10 +3958,8 @@ export function composeRuntime(options) {
3939
3958
  await withhold(withheld, room, local, progress);
3940
3959
  }
3941
3960
  function revoked(locator) {
3942
- return ['collection', 'typed-selection'].some((attached) => {
3943
- const model = modelOf(locator, attached);
3944
- return model.status === 'unavailable' && model.reason === 'revoked';
3945
- });
3961
+ const model = modelOf(locator);
3962
+ return model.status === 'unavailable' && model.reason === 'revoked';
3946
3963
  }
3947
3964
  async function loadRoom(room, local, progress, live) {
3948
3965
  if (!live() || authorization === undefined)
@@ -4000,10 +4017,7 @@ export function composeRuntime(options) {
4000
4017
  journal.record(`connection discover ${result.kind}`);
4001
4018
  if (result.kind === 'transport-uncertainty') {
4002
4019
  for (const room of rooms.values()) {
4003
- await retainModel(local, room, 'collection', cached(modelOf(room.locator)));
4004
- if (room.typedSelection !== null) {
4005
- await retainModel(local, room, 'typed-selection', cached(modelOf(room.locator, 'typed-selection')));
4006
- }
4020
+ await retainModel(local, room, cached(modelOf(room.locator)));
4007
4021
  }
4008
4022
  progress.reachable = false;
4009
4023
  progress.complete = false;
@@ -4285,12 +4299,9 @@ export function composeRuntime(options) {
4285
4299
  }
4286
4300
  }
4287
4301
  async function confirmModels(room, local) {
4288
- for (const attached of ['collection', 'typed-selection']) {
4289
- const model = modelOf(room.locator, attached);
4290
- if (model.status === 'cached') {
4291
- await retainModel(local, room, attached, { ...model, status: 'current' });
4292
- }
4293
- }
4302
+ const model = modelOf(room.locator);
4303
+ if (model.status === 'cached')
4304
+ await retainModel(local, room, { ...model, status: 'current' });
4294
4305
  }
4295
4306
  function submitLater(locator) {
4296
4307
  if (localSaves)
@@ -4872,7 +4883,7 @@ export function composeRuntime(options) {
4872
4883
  typedSelection: rooms.get(locator)?.typedSelection === undefined ||
4873
4884
  rooms.get(locator)?.typedSelection === null
4874
4885
  ? null
4875
- : modelTypes(modelOf(locator, 'typed-selection')),
4886
+ : modelTypes(builtIn),
4876
4887
  };
4877
4888
  },
4878
4889
  roomDraft: {