@agentshouse/mdruntime 0.1.0-c5f333dfa0583 → 0.1.0-c5fe3b2cddda7
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.
- package/README.md +10 -8
- package/dist/adapter.d.ts +39 -40
- package/dist/adapter.d.ts.map +1 -1
- package/dist/attached-model.d.ts +2 -1
- package/dist/attached-model.d.ts.map +1 -1
- package/dist/attached-model.js +28 -4
- package/dist/binary.d.ts +0 -5
- package/dist/binary.d.ts.map +1 -1
- package/dist/binary.js +1 -1
- package/dist/bootstrap.d.ts +16 -8
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +142 -94
- package/dist/change-set.d.ts +1 -7
- package/dist/change-set.d.ts.map +1 -1
- package/dist/change-set.js +1 -4
- package/dist/client.d.ts +6 -7
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +54 -7
- package/dist/collection.d.ts +5 -3
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +24 -19
- package/dist/compile.d.ts +6 -1
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +94 -33
- package/dist/connection.d.ts +8 -7
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +1160 -1151
- package/dist/conversation.d.ts +7 -5
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +171 -55
- package/dist/durability.d.ts +1 -5
- package/dist/durability.d.ts.map +1 -1
- package/dist/durability.js +5 -0
- package/dist/entry.d.ts +1 -0
- package/dist/entry.d.ts.map +1 -1
- package/dist/entry.js +1 -1
- package/dist/file.d.ts +2 -11
- package/dist/file.d.ts.map +1 -1
- package/dist/file.js +6 -4
- package/dist/fulltext.d.ts +27 -0
- package/dist/fulltext.d.ts.map +1 -0
- package/dist/fulltext.js +370 -0
- package/dist/host.d.ts +3 -4
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +42 -18
- package/dist/identity.d.ts +0 -1
- package/dist/identity.d.ts.map +1 -1
- package/dist/identity.js +0 -3
- package/dist/index.d.ts +10 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/live.d.ts.map +1 -1
- package/dist/live.js +2 -1
- package/dist/media.d.ts +8 -0
- package/dist/media.d.ts.map +1 -0
- package/dist/media.js +218 -0
- package/dist/memory.d.ts +3 -2
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +93 -38
- package/dist/messages.d.ts +28 -0
- package/dist/messages.d.ts.map +1 -0
- package/dist/messages.js +90 -0
- package/dist/model.d.ts +6 -1
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +7 -10
- package/dist/observation.d.ts +1 -0
- package/dist/observation.d.ts.map +1 -1
- package/dist/observation.js +1 -1
- package/dist/outcome.d.ts +13 -6
- package/dist/outcome.d.ts.map +1 -1
- package/dist/outcome.js +16 -5
- package/dist/overlay.js +1 -1
- package/dist/paths.d.ts +1 -0
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +4 -0
- package/dist/query.d.ts +10 -1
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +12 -1
- package/dist/reach.d.ts +8 -3
- package/dist/reach.d.ts.map +1 -1
- package/dist/reach.js +69 -46
- package/dist/readiness.d.ts +14 -15
- package/dist/readiness.d.ts.map +1 -1
- package/dist/readiness.js +2 -8
- package/dist/recovery.d.ts +1 -0
- package/dist/recovery.d.ts.map +1 -1
- package/dist/recovery.js +26 -0
- package/dist/refresh.d.ts +9 -4
- package/dist/refresh.d.ts.map +1 -1
- package/dist/refresh.js +64 -30
- package/dist/replica.d.ts +5 -5
- package/dist/replica.d.ts.map +1 -1
- package/dist/replica.js +4 -7
- package/dist/room-draft.d.ts +18 -2
- package/dist/room-draft.d.ts.map +1 -1
- package/dist/room-draft.js +73 -6
- package/dist/rows.d.ts +15 -6
- package/dist/rows.d.ts.map +1 -1
- package/dist/rows.js +102 -74
- package/dist/save.d.ts +2 -0
- package/dist/save.d.ts.map +1 -1
- package/dist/save.js +13 -7
- package/dist/search.d.ts +11 -43
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +0 -296
- package/dist/shared.d.ts +0 -1
- package/dist/shared.d.ts.map +1 -1
- package/dist/shared.js +0 -10
- package/dist/status.d.ts +4 -3
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +3 -1
- package/dist/store.d.ts +23 -58
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +37 -272
- package/dist/stored.d.ts +10 -23
- package/dist/stored.d.ts.map +1 -1
- package/dist/stored.js +217 -218
- package/dist/tables.d.ts +51 -46
- package/dist/tables.d.ts.map +1 -1
- package/dist/tables.js +94 -53
- package/dist/transfer.d.ts +0 -5
- package/dist/transfer.d.ts.map +1 -1
- package/dist/validation.d.ts +3 -4
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +35 -96
- package/dist/wire.d.ts +5 -5
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +2 -2
- package/dist/words.d.ts +19 -0
- package/dist/words.d.ts.map +1 -0
- package/dist/words.js +156 -0
- package/package.json +11 -19
- package/AGENTS.md +0 -312
- package/dist/chat/acts.d.ts +0 -33
- package/dist/chat/acts.d.ts.map +0 -1
- package/dist/chat/acts.js +0 -36
- package/dist/chat/context.d.ts +0 -3
- package/dist/chat/context.d.ts.map +0 -1
- package/dist/chat/context.js +0 -10
- package/dist/chat/earlier.d.ts +0 -10
- package/dist/chat/earlier.d.ts.map +0 -1
- package/dist/chat/earlier.js +0 -54
- package/dist/chat/house.d.ts +0 -25
- package/dist/chat/house.d.ts.map +0 -1
- package/dist/chat/house.js +0 -38
- package/dist/chat/input.d.ts +0 -11
- package/dist/chat/input.d.ts.map +0 -1
- package/dist/chat/input.js +0 -92
- package/dist/chat/kit.d.ts +0 -61
- package/dist/chat/kit.d.ts.map +0 -1
- package/dist/chat/kit.js +0 -123
- package/dist/chat/lasting.d.ts +0 -6
- package/dist/chat/lasting.d.ts.map +0 -1
- package/dist/chat/lasting.js +0 -19
- package/dist/chat/list.d.ts +0 -28
- package/dist/chat/list.d.ts.map +0 -1
- package/dist/chat/list.js +0 -103
- package/dist/chat/markdown.d.ts +0 -21
- package/dist/chat/markdown.d.ts.map +0 -1
- package/dist/chat/markdown.js +0 -246
- package/dist/chat/message.d.ts +0 -7
- package/dist/chat/message.d.ts.map +0 -1
- package/dist/chat/message.js +0 -254
- package/dist/chat/messages.d.ts +0 -35
- package/dist/chat/messages.d.ts.map +0 -1
- package/dist/chat/messages.js +0 -210
- package/dist/chat/name.d.ts +0 -7
- package/dist/chat/name.d.ts.map +0 -1
- package/dist/chat/name.js +0 -37
- package/dist/chat/opened.d.ts +0 -12
- package/dist/chat/opened.d.ts.map +0 -1
- package/dist/chat/opened.js +0 -19
- package/dist/chat/overlay.d.ts +0 -32
- package/dist/chat/overlay.d.ts.map +0 -1
- package/dist/chat/overlay.js +0 -103
- package/dist/chat/parts.d.ts +0 -58
- package/dist/chat/parts.d.ts.map +0 -1
- package/dist/chat/parts.js +0 -120
- package/dist/chat/pieces.d.ts +0 -62
- package/dist/chat/pieces.d.ts.map +0 -1
- package/dist/chat/pieces.js +0 -1110
- package/dist/chat/process.d.ts +0 -20
- package/dist/chat/process.d.ts.map +0 -1
- package/dist/chat/process.js +0 -32
- package/dist/chat/provider.d.ts +0 -70
- package/dist/chat/provider.d.ts.map +0 -1
- package/dist/chat/provider.js +0 -635
- package/dist/chat/question.d.ts +0 -9
- package/dist/chat/question.d.ts.map +0 -1
- package/dist/chat/question.js +0 -69
- package/dist/chat/questions.d.ts +0 -31
- package/dist/chat/questions.d.ts.map +0 -1
- package/dist/chat/questions.js +0 -111
- package/dist/chat/read.d.ts +0 -5
- package/dist/chat/read.d.ts.map +0 -1
- package/dist/chat/read.js +0 -13
- package/dist/chat/ready.d.ts +0 -37
- package/dist/chat/ready.d.ts.map +0 -1
- package/dist/chat/ready.js +0 -93
- package/dist/chat/room-file.d.ts +0 -8
- package/dist/chat/room-file.d.ts.map +0 -1
- package/dist/chat/room-file.js +0 -72
- package/dist/chat/steady.d.ts +0 -3
- package/dist/chat/steady.d.ts.map +0 -1
- package/dist/chat/steady.js +0 -26
- package/dist/chat/style.d.ts +0 -4
- package/dist/chat/style.d.ts.map +0 -1
- package/dist/chat/style.js +0 -307
- package/dist/chat.d.ts +0 -15
- package/dist/chat.d.ts.map +0 -1
- package/dist/chat.js +0 -8
- package/dist/reads.d.ts +0 -11
- package/dist/reads.d.ts.map +0 -1
- package/dist/reads.js +0 -98
package/dist/connection.js
CHANGED
|
@@ -1,51 +1,41 @@
|
|
|
1
1
|
import { escapePointer, parseFrontmatter, roomTarget, target } from '@agentshouse/mdmodel';
|
|
2
|
-
import { attachedModel,
|
|
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';
|
|
6
6
|
import { createLiveQueries } from './live.js';
|
|
7
7
|
import { seededResult } from './seed.js';
|
|
8
8
|
import { documentKey, roomOfKey } from './rows.js';
|
|
9
|
-
import {
|
|
9
|
+
import { searchPlanOf } from './search.js';
|
|
10
10
|
import { capped, MAXIMUM_WINDOW } from './windows.js';
|
|
11
11
|
import { bootstrapRoom } 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';
|
|
15
|
-
import { available, collectionOf,
|
|
15
|
+
import { available, collectionOf, composedOf, validRoot, } from './collection.js';
|
|
16
16
|
import { runtimeError } from './failure.js';
|
|
17
17
|
import { createLog } from './log.js';
|
|
18
18
|
import { binaryRecord } from './binary.js';
|
|
19
|
+
import { attachedFile } from './file.js';
|
|
19
20
|
import { openedFiles } from './opened.js';
|
|
20
|
-
import { ABSENT_TARGET, absentBinary,
|
|
21
|
+
import { ABSENT_TARGET, absentBinary, attachAnswer, binaryAnswer, binaryLinkAnswer, calledResult, failure, historyAnswer, LOCKED_UNREADABLE, lockedRoom, OFFLINE, undeclared, UNCERTAIN, UNREADABLE, } from './outcome.js';
|
|
21
22
|
import { createRecovery } from './recovery.js';
|
|
22
|
-
import { doorReads } from './reads.js';
|
|
23
23
|
import { advancement } from './refresh.js';
|
|
24
24
|
import { buildBatch, buildMove, joined, limiting, queuedDocuments, requeuing, } from './save.js';
|
|
25
|
-
import { compositionOf, entryAt, landed, movedBy, overlayOf, replaying, returning, returnedPaths,
|
|
26
|
-
import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, moveTarget, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
|
|
25
|
+
import { compositionOf, entryAt, landed, movedBy, overlayOf, replaying, returning, returnedPaths, slicing, slicingWith, standing, storedRecord, touchedBy, } from './replica.js';
|
|
26
|
+
import { binaryBase, changedParts, changesOf, gatheredParts, held, heldFile, joinedDraft, moveTarget, readParts, readSavedParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
|
|
27
27
|
import { same, saving, STARTING, transferring, } from './status.js';
|
|
28
|
-
import { detectStorageLoss, nextGeneration } from './durability.js';
|
|
29
|
-
import { readinessOf,
|
|
28
|
+
import { detectStorageLoss, nextGeneration, refusedForSpace, } from './durability.js';
|
|
29
|
+
import { readinessOf, } from './readiness.js';
|
|
30
30
|
import { FencedMap } from './fence.js';
|
|
31
|
-
import { fingerprint } from './stored.js';
|
|
32
31
|
import { sameBinary } from './overlay.js';
|
|
33
32
|
import { paced, ROW_WEIGHT, stepped, yielded } from './turn.js';
|
|
34
33
|
import { identityTime, roomDraftRevision } from './identity.js';
|
|
35
|
-
import { beneath, normalize } from './paths.js';
|
|
34
|
+
import { ancestors, beneath, normalize } from './paths.js';
|
|
36
35
|
import { openRuntimeStore, } from './store.js';
|
|
37
|
-
import {
|
|
36
|
+
import { validatedNeighbourhood } from './validation.js';
|
|
38
37
|
import { unconfirmed } from './transfer.js';
|
|
39
38
|
import { governs, holds, narrowed, planReach, } from './reach.js';
|
|
40
|
-
function roomsAlone(plan) {
|
|
41
|
-
return (plan.reach === null &&
|
|
42
|
-
plan.root.types === null &&
|
|
43
|
-
plan.root.where === null &&
|
|
44
|
-
plan.root.pathPrefix === null &&
|
|
45
|
-
plan.paths === null &&
|
|
46
|
-
plan.hop === null &&
|
|
47
|
-
plan.backlinks === null);
|
|
48
|
-
}
|
|
49
39
|
function within(root, path) {
|
|
50
40
|
return root === '' || beneath(root, path);
|
|
51
41
|
}
|
|
@@ -63,39 +53,46 @@ const OPENING_LANE = 'opening';
|
|
|
63
53
|
const CONVERSATION_LANE = 'conversations';
|
|
64
54
|
const ROOM_READS = 256;
|
|
65
55
|
const ROOM_ATTEMPTS = 16;
|
|
66
|
-
const SEARCH_BATCH = 256;
|
|
67
56
|
const ERASE_BATCH = 512;
|
|
68
|
-
const
|
|
69
|
-
const
|
|
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;
|
|
61
|
+
// A build past its floor of text stops after a second and a half of work, its full-text writes included, so a Room whose text costs ten times the usual holds a Save, a query or the next part's progress behind no longer build.
|
|
62
|
+
const DERIVE_LONGEST_MS = 1_500;
|
|
70
63
|
const QUERY_BYTES = 1 << 20;
|
|
71
64
|
const STAGE_BYTES = 1 << 20;
|
|
72
|
-
const
|
|
65
|
+
const STAGE_FLOOR = 1 << 16;
|
|
66
|
+
// A batch aims at this much database work, so a Save or a query waits behind at most one short write.
|
|
67
|
+
const STAGE_AIM_MS = 50;
|
|
68
|
+
const STREAMS_HELD = 4;
|
|
69
|
+
const NOTHING_HELD = { bytes: 0 };
|
|
70
|
+
const QUEUED_TEXT = 1 << 23;
|
|
71
|
+
// Bytes still arriving are reported at most this often, so readiness is not rebuilt for every chunk the network hands over while a slow link still moves the figure.
|
|
72
|
+
const ARRIVAL_REPORT_MS = 250;
|
|
73
73
|
// 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.
|
|
74
74
|
const LOCK_GRACE_MS = 120_000;
|
|
75
75
|
function operationOf(input) {
|
|
76
76
|
const operation = input?.operation_id;
|
|
77
77
|
return typeof operation === 'string' ? operation : null;
|
|
78
78
|
}
|
|
79
|
-
function
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
79
|
+
function readUntil(read, signal) {
|
|
80
|
+
if (signal.aborted)
|
|
81
|
+
return Promise.resolve({ kind: 'transport-uncertainty' });
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
const stop = () => resolve({ kind: 'transport-uncertainty' });
|
|
84
|
+
signal.addEventListener('abort', stop, { once: true });
|
|
85
|
+
read.then(resolve, reject).finally(() => signal.removeEventListener('abort', stop));
|
|
85
86
|
});
|
|
86
87
|
}
|
|
87
|
-
function roomLane(locator) {
|
|
88
|
-
return `room ${locator}`;
|
|
89
|
-
}
|
|
90
88
|
function pendingKey(locator, id) {
|
|
91
89
|
return `${locator}\u0000${id}`;
|
|
92
90
|
}
|
|
93
91
|
export function composeRuntime(options) {
|
|
94
92
|
const localSaves = options.adapter.localSaves === true;
|
|
95
|
-
const roomTypes = roomTypesOf(options.adapter.roomTypes);
|
|
93
|
+
const roomTypes = roomTypesOf(options.adapter.roomTypes, options.adapter.nameableRoomTypes);
|
|
96
94
|
let authorization = options.authorization;
|
|
97
95
|
let opening;
|
|
98
|
-
let store;
|
|
99
96
|
const fences = new Map();
|
|
100
97
|
const isFenced = (locator) => fences.has(locator);
|
|
101
98
|
let fencings = 0;
|
|
@@ -103,44 +100,51 @@ export function composeRuntime(options) {
|
|
|
103
100
|
const fencedSince = (locator, since) => fences.has(locator) || (lastFenced.get(locator) ?? 0) > since;
|
|
104
101
|
let discoveries = 0;
|
|
105
102
|
const replicas = new FencedMap(isFenced);
|
|
103
|
+
const reopened = new Set();
|
|
104
|
+
const usableWhenHeld = options.usableWhenHeld ?? [];
|
|
106
105
|
const queuesWithoutReplica = new FencedMap(isFenced);
|
|
107
106
|
const pendings = new Map();
|
|
108
107
|
const seeds = new Map();
|
|
109
108
|
const seedWatchers = new Map();
|
|
110
109
|
const roomDrafts = new FencedMap(isFenced);
|
|
110
|
+
const attachedFiles = new FencedMap(isFenced);
|
|
111
111
|
const presentations = new Map();
|
|
112
112
|
let limits = [];
|
|
113
113
|
let capabilities = [];
|
|
114
114
|
const rooms = new FencedMap(isFenced);
|
|
115
115
|
const counted = new Map();
|
|
116
116
|
const unreadable = new Set();
|
|
117
|
-
const
|
|
118
|
-
const models = new FencedMap(
|
|
119
|
-
const liveModels = new FencedMap(
|
|
117
|
+
const builtIn = builtInModel(roomTypes);
|
|
118
|
+
const models = new FencedMap(isFenced);
|
|
119
|
+
const liveModels = new FencedMap(isFenced);
|
|
120
120
|
const invalidModels = new Set();
|
|
121
121
|
const discovered = new Map();
|
|
122
|
-
|
|
122
|
+
let inFlight = new AbortController();
|
|
123
|
+
const refreshed = options.adapter.refresh?.bind(options.adapter);
|
|
124
|
+
const reads = {
|
|
125
|
+
bootstrap: (authorized, request) => readUntil(options.adapter.bootstrap(authorized, request), inFlight.signal),
|
|
126
|
+
...(refreshed === undefined
|
|
127
|
+
? {}
|
|
128
|
+
: {
|
|
129
|
+
refresh: (authorized, request) => readUntil(refreshed(authorized, request), inFlight.signal),
|
|
130
|
+
}),
|
|
131
|
+
reached: (locator, roomVersion, target) => options.adapter.reached(locator, roomVersion, target),
|
|
132
|
+
};
|
|
123
133
|
let roomsListed = false;
|
|
124
134
|
const scopedRefusals = new Map();
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
function modelKey(locator, attached) {
|
|
128
|
-
return `${attached} ${locator}`;
|
|
129
|
-
}
|
|
130
|
-
function modelOf(locator, attached = 'collection') {
|
|
131
|
-
return models.get(modelKey(locator, attached)) ?? NO_MODEL;
|
|
135
|
+
function modelOf(locator) {
|
|
136
|
+
return models.get(locator) ?? NO_MODEL;
|
|
132
137
|
}
|
|
133
|
-
function savedModel(locator
|
|
134
|
-
return invalidModels.has(
|
|
138
|
+
function savedModel(locator) {
|
|
139
|
+
return invalidModels.has(locator)
|
|
135
140
|
? { status: 'unavailable', reason: 'revoked' }
|
|
136
|
-
: modelOf(locator
|
|
141
|
+
: modelOf(locator);
|
|
142
|
+
}
|
|
143
|
+
function composed(record) {
|
|
144
|
+
return compositionOf(record, builtIn);
|
|
137
145
|
}
|
|
138
146
|
function compositionFor(record) {
|
|
139
|
-
return
|
|
140
|
-
...record,
|
|
141
|
-
model: savedModel(record.locator, 'collection'),
|
|
142
|
-
selectionModel: savedModel(record.locator, 'typed-selection'),
|
|
143
|
-
});
|
|
147
|
+
return composed({ ...record, model: savedModel(record.locator) });
|
|
144
148
|
}
|
|
145
149
|
let refusal = null;
|
|
146
150
|
let storageLoss = null;
|
|
@@ -150,10 +154,10 @@ export function composeRuntime(options) {
|
|
|
150
154
|
let closed = false;
|
|
151
155
|
let stopping = false;
|
|
152
156
|
let withdrawStream;
|
|
153
|
-
const announced = new Map();
|
|
154
157
|
let underway = null;
|
|
155
158
|
let streamed = null;
|
|
156
159
|
let handed = false;
|
|
160
|
+
let naming = 0;
|
|
157
161
|
const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
|
|
158
162
|
const repairing = new Set();
|
|
159
163
|
const calling = new Set();
|
|
@@ -165,14 +169,16 @@ export function composeRuntime(options) {
|
|
|
165
169
|
const stale = new Set();
|
|
166
170
|
let generation = 0;
|
|
167
171
|
let discovering = null;
|
|
168
|
-
|
|
169
|
-
const owed = new Set();
|
|
170
|
-
const reloadsDue = new Map();
|
|
172
|
+
const reloadsDue = new Set();
|
|
171
173
|
const lanes = new Map();
|
|
172
|
-
|
|
174
|
+
const roomLanes = new Map();
|
|
175
|
+
let roomWork = 0;
|
|
176
|
+
const roomReads = { free: ROOM_READS, waiting: [] };
|
|
173
177
|
const working = new Set();
|
|
174
178
|
let mutations = Promise.resolve();
|
|
179
|
+
let gathering = null;
|
|
175
180
|
const roomListeners = new Set();
|
|
181
|
+
const unlistedRooms = new Set();
|
|
176
182
|
const refusalListeners = new Set();
|
|
177
183
|
const storageLossListeners = new Set();
|
|
178
184
|
const statusListeners = new Set();
|
|
@@ -192,19 +198,53 @@ export function composeRuntime(options) {
|
|
|
192
198
|
const serviceModeListeners = new Set();
|
|
193
199
|
let member = null;
|
|
194
200
|
const memberListeners = new Set();
|
|
195
|
-
const prepared = new Set();
|
|
196
201
|
const unpreparedDemand = new Set();
|
|
197
|
-
const
|
|
202
|
+
const unpreparable = new Set();
|
|
198
203
|
let preparing = false;
|
|
199
204
|
let targets = {};
|
|
200
205
|
let persistence = options.shell.persistence;
|
|
201
|
-
let
|
|
202
|
-
|
|
203
|
-
let
|
|
206
|
+
let full = false;
|
|
207
|
+
let probing = null;
|
|
208
|
+
let testing = null;
|
|
209
|
+
const database = {
|
|
210
|
+
async ask(request) {
|
|
211
|
+
try {
|
|
212
|
+
return await options.database.ask(request);
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
if (refusedForSpace(error))
|
|
216
|
+
filled();
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
};
|
|
204
221
|
const readinessWatches = new Set();
|
|
205
222
|
let readinessQueued = false;
|
|
206
223
|
const catching = new Set();
|
|
207
224
|
const transfers = new FencedMap(isFenced);
|
|
225
|
+
const installs = new Set();
|
|
226
|
+
const arrivals = new Map();
|
|
227
|
+
const streaming = new Set();
|
|
228
|
+
const underived = new Map();
|
|
229
|
+
const unindexed = new Map();
|
|
230
|
+
const startedOver = new Map();
|
|
231
|
+
let arrivalReported = 0;
|
|
232
|
+
let streamsFree = STREAMS_HELD;
|
|
233
|
+
let queuedText = 0;
|
|
234
|
+
const installWaiters = [];
|
|
235
|
+
const stageBudget = {
|
|
236
|
+
bytes: STAGE_FLOOR * 4,
|
|
237
|
+
floor: STAGE_FLOOR,
|
|
238
|
+
ceiling: STAGE_BYTES,
|
|
239
|
+
aim: STAGE_AIM_MS,
|
|
240
|
+
};
|
|
241
|
+
const deriveBudget = {
|
|
242
|
+
bytes: DERIVE_FLOOR * 4,
|
|
243
|
+
floor: DERIVE_FLOOR,
|
|
244
|
+
ceiling: DERIVE_BYTES,
|
|
245
|
+
aim: DERIVE_AIM_MS,
|
|
246
|
+
};
|
|
247
|
+
const installTurns = { busy: false, ahead: 0, finishing: [], starting: [] };
|
|
208
248
|
const conversations = createConversationStore({
|
|
209
249
|
agents: declaredAgents,
|
|
210
250
|
documents: (declaration, after) => rowsQuery(planOf({
|
|
@@ -225,40 +265,27 @@ export function composeRuntime(options) {
|
|
|
225
265
|
stream: () => options.adapter.conversations,
|
|
226
266
|
publish: publishChangeSet,
|
|
227
267
|
online: () => connectionOf() !== 'unreachable' && refusal === null,
|
|
268
|
+
listed: () => roomsListed,
|
|
269
|
+
full: () => full,
|
|
228
270
|
closed: () => closed,
|
|
229
271
|
unanswered: resendLater,
|
|
230
272
|
expired: (operation) => pastHorizon(operation),
|
|
231
273
|
});
|
|
232
|
-
const searchIndex = createSearchIndex({
|
|
233
|
-
async *room(room) {
|
|
234
|
-
const local = await openLocal();
|
|
235
|
-
for (let after = null;;) {
|
|
236
|
-
const key = replicas.get(room)?.record.key;
|
|
237
|
-
if (key === undefined)
|
|
238
|
-
return;
|
|
239
|
-
const page = await local.searched(key, room, after, SEARCH_BATCH, null);
|
|
240
|
-
yield page.documents;
|
|
241
|
-
if (page.next === null)
|
|
242
|
-
return;
|
|
243
|
-
after = page.next;
|
|
244
|
-
}
|
|
245
|
-
},
|
|
246
|
-
async documents(room, paths) {
|
|
247
|
-
const key = replicas.get(room)?.record.key;
|
|
248
|
-
if (key === undefined || paths.length === 0)
|
|
249
|
-
return [];
|
|
250
|
-
return (await (await openLocal()).searched(key, room, null, paths.length, paths)).documents;
|
|
251
|
-
},
|
|
252
|
-
});
|
|
253
|
-
const roomsToIndex = new Set();
|
|
254
|
-
const conversationsToIndex = new Set();
|
|
255
|
-
let indexing = false;
|
|
256
274
|
const journal = createLog();
|
|
257
|
-
const keptFiles = openedFiles(
|
|
258
|
-
let
|
|
275
|
+
const keptFiles = openedFiles(database, isFenced);
|
|
276
|
+
let readyFromStore = () => { };
|
|
259
277
|
const ready = new Promise((resolve) => {
|
|
260
|
-
|
|
278
|
+
readyFromStore = resolve;
|
|
261
279
|
});
|
|
280
|
+
let notifyDiscovered = () => { };
|
|
281
|
+
const firstDiscovery = new Promise((resolve) => {
|
|
282
|
+
notifyDiscovered = resolve;
|
|
283
|
+
});
|
|
284
|
+
let roomsNamed = false;
|
|
285
|
+
function notifyReady() {
|
|
286
|
+
notifyDiscovered();
|
|
287
|
+
readyFromStore();
|
|
288
|
+
}
|
|
262
289
|
function rememberRoomDraft(record) {
|
|
263
290
|
const room = roomDrafts.get(record.locator) ?? new Map();
|
|
264
291
|
room.set(record.path, record);
|
|
@@ -278,7 +305,7 @@ export function composeRuntime(options) {
|
|
|
278
305
|
return undefined;
|
|
279
306
|
const local = await openLocal();
|
|
280
307
|
const slice = await stepped(slicing(await local.entries(held.record.key, 'visible', paths), paths));
|
|
281
|
-
return { ...slice, collection: collectionOf(slice.documents,
|
|
308
|
+
return { ...slice, collection: collectionOf(slice.documents, composed(held.record)) };
|
|
282
309
|
}
|
|
283
310
|
function roomDraftOf(record, visible) {
|
|
284
311
|
return structuredClone({
|
|
@@ -334,14 +361,40 @@ export function composeRuntime(options) {
|
|
|
334
361
|
const value = presentations.get(app);
|
|
335
362
|
return value === undefined ? null : structuredClone(value);
|
|
336
363
|
}
|
|
337
|
-
function
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
364
|
+
async function writtenRoomDraft(room, path, revision, input, next) {
|
|
365
|
+
const parts = readParts(input);
|
|
366
|
+
if (path === '' || normalize(path) !== path)
|
|
367
|
+
throw runtimeError('room-draft-malformed');
|
|
368
|
+
const local = await openLocal();
|
|
369
|
+
const current = roomDrafts.get(room)?.get(path);
|
|
370
|
+
if ((current?.revision ?? null) !== revision) {
|
|
371
|
+
return staleWrite(current === undefined ? null : ((await roomDraftsOf([current]))[0] ?? null));
|
|
372
|
+
}
|
|
373
|
+
const file = parts.file === undefined
|
|
374
|
+
? undefined
|
|
375
|
+
: await heldFile(parts.file.blob, parts.file.base === undefined
|
|
376
|
+
? binaryBase(await visibleOf(room, [path]), path)
|
|
377
|
+
: parts.file.base);
|
|
378
|
+
const record = {
|
|
379
|
+
locator: room,
|
|
380
|
+
path,
|
|
381
|
+
revision: next,
|
|
382
|
+
parts: held(current?.parts, parts, file),
|
|
383
|
+
returned: current?.returned ?? null,
|
|
384
|
+
};
|
|
385
|
+
await local.keepRoomDraft(record, parts.file === undefined || file === undefined
|
|
386
|
+
? []
|
|
387
|
+
: [{ key: file.localId, blob: parts.file.blob }]);
|
|
388
|
+
rememberRoomDraft(record);
|
|
389
|
+
publishChangeSet(changeSet(room, { drafts: [path] }));
|
|
390
|
+
return { outcome: 'written', roomDraft: (await roomDraftsOf([record]))[0] ?? null };
|
|
391
|
+
}
|
|
392
|
+
async function keptPresentation(app, next) {
|
|
393
|
+
const local = await openLocal();
|
|
394
|
+
const stored = next(presentations.get(app));
|
|
395
|
+
await local.present({ app, value: stored });
|
|
396
|
+
presentations.set(app, stored);
|
|
397
|
+
publishChangeSet(changeSet('', { presentation: app }));
|
|
345
398
|
}
|
|
346
399
|
function emitPresentation(app) {
|
|
347
400
|
if (closed)
|
|
@@ -356,6 +409,20 @@ export function composeRuntime(options) {
|
|
|
356
409
|
(queuesWithoutReplica.size > 0 ||
|
|
357
410
|
[...replicas.values()].some((replica) => replica.queue.length > 0)));
|
|
358
411
|
}
|
|
412
|
+
function unsent() {
|
|
413
|
+
const changed = new Map();
|
|
414
|
+
for (const room of [...replicas.keys(), ...queuesWithoutReplica.keys()]) {
|
|
415
|
+
for (const { batch, state } of entriesOf(room)) {
|
|
416
|
+
if (state === 'local')
|
|
417
|
+
continue;
|
|
418
|
+
for (const { path } of batch.changes)
|
|
419
|
+
changed.set(`${room}\u0000${path}`, { room, path });
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return [...changed]
|
|
423
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
424
|
+
.map(([, change]) => change);
|
|
425
|
+
}
|
|
359
426
|
function attention() {
|
|
360
427
|
const attentionRequired = [];
|
|
361
428
|
for (const record of [...roomDrafts.values()].flatMap((room) => [...room.values()])) {
|
|
@@ -367,16 +434,9 @@ export function composeRuntime(options) {
|
|
|
367
434
|
returned: structuredClone(record.returned),
|
|
368
435
|
});
|
|
369
436
|
}
|
|
370
|
-
for (const [room, refusal] of sourceRefusals)
|
|
371
|
-
attentionRequired.push({ room, refusal: structuredClone(refusal) });
|
|
372
437
|
attentionRequired.sort((left, right) => {
|
|
373
438
|
if (left.room !== right.room)
|
|
374
439
|
return left.room < right.room ? -1 : 1;
|
|
375
|
-
if ('refusal' in left || 'refusal' in right) {
|
|
376
|
-
if ('refusal' in left && 'refusal' in right)
|
|
377
|
-
return 0;
|
|
378
|
-
return 'refusal' in left ? -1 : 1;
|
|
379
|
-
}
|
|
380
440
|
if (left.path !== right.path)
|
|
381
441
|
return left.path < right.path ? -1 : 1;
|
|
382
442
|
return 0;
|
|
@@ -388,7 +448,7 @@ export function composeRuntime(options) {
|
|
|
388
448
|
if (reset === null || submission.reset < reset)
|
|
389
449
|
reset = submission.reset;
|
|
390
450
|
}
|
|
391
|
-
return { attentionRequired, reset };
|
|
451
|
+
return { attentionRequired, unsent: unsent(), reset };
|
|
392
452
|
}
|
|
393
453
|
function documentsIn(locator) {
|
|
394
454
|
return replicas.get(locator)?.count ?? 0;
|
|
@@ -461,7 +521,7 @@ export function composeRuntime(options) {
|
|
|
461
521
|
const governed = governs(reach, room);
|
|
462
522
|
const attached = [
|
|
463
523
|
...(governed.collection ? [modelOf(room.locator)] : []),
|
|
464
|
-
...(governed.selection ? [
|
|
524
|
+
...(governed.selection ? [builtIn] : []),
|
|
465
525
|
];
|
|
466
526
|
return {
|
|
467
527
|
locator: room.locator,
|
|
@@ -477,11 +537,26 @@ export function composeRuntime(options) {
|
|
|
477
537
|
const value = listed();
|
|
478
538
|
for (const listener of roomListeners)
|
|
479
539
|
listener(value);
|
|
540
|
+
emitKnownRooms();
|
|
541
|
+
}
|
|
542
|
+
function emitKnownRooms() {
|
|
543
|
+
conversations.learned();
|
|
544
|
+
if (unlistedRooms.size === 0)
|
|
545
|
+
return;
|
|
546
|
+
if (!roomsListed && rooms.size === 0 && refusal === null && !status.providerUnreachable)
|
|
547
|
+
return;
|
|
548
|
+
const value = listed();
|
|
549
|
+
for (const listener of [...unlistedRooms]) {
|
|
550
|
+
unlistedRooms.delete(listener);
|
|
551
|
+
roomListeners.add(listener);
|
|
552
|
+
listener(value);
|
|
553
|
+
}
|
|
480
554
|
}
|
|
481
555
|
function emitRefusal() {
|
|
482
556
|
queueReadiness();
|
|
483
557
|
for (const listener of refusalListeners)
|
|
484
558
|
listener(refusal);
|
|
559
|
+
emitKnownRooms();
|
|
485
560
|
}
|
|
486
561
|
function emitStorageLoss() {
|
|
487
562
|
queueReadiness();
|
|
@@ -502,6 +577,7 @@ export function composeRuntime(options) {
|
|
|
502
577
|
if (closed)
|
|
503
578
|
return;
|
|
504
579
|
queueReadiness();
|
|
580
|
+
emitKnownRooms();
|
|
505
581
|
const next = status.state === 'synced' && !conversations.current()
|
|
506
582
|
? { ...status, state: 'syncing' }
|
|
507
583
|
: status;
|
|
@@ -515,9 +591,6 @@ export function composeRuntime(options) {
|
|
|
515
591
|
if (localSaves)
|
|
516
592
|
return Object.freeze({ batch, state: 'local' });
|
|
517
593
|
const state = pendings.get(pendingKey(locator, batch.id));
|
|
518
|
-
if (state?.accepted !== undefined) {
|
|
519
|
-
return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
|
|
520
|
-
}
|
|
521
594
|
return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
|
|
522
595
|
}
|
|
523
596
|
function entriesOf(locator) {
|
|
@@ -532,23 +605,29 @@ export function composeRuntime(options) {
|
|
|
532
605
|
}
|
|
533
606
|
function applyChangeSet(set) {
|
|
534
607
|
liveQueries.touched(set);
|
|
535
|
-
searchIndex.changed(set);
|
|
536
608
|
}
|
|
537
609
|
function unpublished(error) {
|
|
538
610
|
journal.record(`publication failed ${error instanceof Error ? error.name : 'unknown'}`);
|
|
539
611
|
}
|
|
612
|
+
function indexed(locator) {
|
|
613
|
+
return replicas.get(locator)?.indexed === true;
|
|
614
|
+
}
|
|
540
615
|
async function prepareReplica(locator) {
|
|
541
616
|
for (;;) {
|
|
542
617
|
const held = replicas.get(locator);
|
|
543
|
-
if (held === undefined || closed ||
|
|
618
|
+
if (held === undefined || closed || held.indexed)
|
|
544
619
|
return;
|
|
545
620
|
const key = held.record.key;
|
|
546
|
-
const done = await exclusive(async () =>
|
|
547
|
-
|
|
621
|
+
const done = await exclusive(async () => {
|
|
622
|
+
const local = await openLocal();
|
|
623
|
+
return timed(deriveBudget, () => local.rederive(key, composed(held.record), deriveBudget.bytes, DERIVE_LONGEST_MS, DERIVE_FLOOR));
|
|
624
|
+
});
|
|
625
|
+
const now = replicas.get(locator);
|
|
626
|
+
if (now?.record.key !== key)
|
|
548
627
|
continue;
|
|
549
|
-
if (!done)
|
|
628
|
+
if (!done.done)
|
|
550
629
|
continue;
|
|
551
|
-
|
|
630
|
+
replicas.set(locator, { ...now, indexed: true });
|
|
552
631
|
publishChangeSet(changeSet(locator, { documents: null }));
|
|
553
632
|
liveQueries.recover();
|
|
554
633
|
queueReadiness();
|
|
@@ -556,11 +635,11 @@ export function composeRuntime(options) {
|
|
|
556
635
|
}
|
|
557
636
|
}
|
|
558
637
|
function unprepared() {
|
|
559
|
-
return [...unpreparedDemand, ...replicas.keys()].find((locator) => !
|
|
638
|
+
return [...unpreparedDemand, ...replicas.keys()].find((locator) => !indexed(locator) && !unpreparable.has(locator));
|
|
560
639
|
}
|
|
561
|
-
function
|
|
562
|
-
|
|
563
|
-
journal.record(`
|
|
640
|
+
function unpreparedBy(locator, error) {
|
|
641
|
+
unpreparable.add(locator);
|
|
642
|
+
journal.record(`preparation failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
|
|
564
643
|
queueReadiness();
|
|
565
644
|
}
|
|
566
645
|
function prepare(locators = []) {
|
|
@@ -576,7 +655,7 @@ export function composeRuntime(options) {
|
|
|
576
655
|
if (closed)
|
|
577
656
|
return;
|
|
578
657
|
unpreparedDemand.delete(waiting);
|
|
579
|
-
await prepareReplica(waiting).catch((error) =>
|
|
658
|
+
await prepareReplica(waiting).catch((error) => unpreparedBy(waiting, error));
|
|
580
659
|
}
|
|
581
660
|
}
|
|
582
661
|
finally {
|
|
@@ -585,7 +664,11 @@ export function composeRuntime(options) {
|
|
|
585
664
|
})();
|
|
586
665
|
}
|
|
587
666
|
function roomsOfReach(reach) {
|
|
588
|
-
return new Set([
|
|
667
|
+
return new Set([
|
|
668
|
+
...reach.rooms,
|
|
669
|
+
...reach.documents.map(roomOfKey),
|
|
670
|
+
...reach.areas.flatMap((area) => (area.types.length === 0 ? [] : [area.room])),
|
|
671
|
+
]);
|
|
589
672
|
}
|
|
590
673
|
function reachedRooms(plan) {
|
|
591
674
|
const reached = plan.reach === null ? null : roomsOfReach(plan.reach);
|
|
@@ -617,14 +700,14 @@ export function composeRuntime(options) {
|
|
|
617
700
|
pending.push({ room: locator, awaits: 'acquisition' });
|
|
618
701
|
continue;
|
|
619
702
|
}
|
|
620
|
-
if (!
|
|
703
|
+
if (!replica.indexed) {
|
|
621
704
|
pending.push({ room: locator, awaits: 'preparation' });
|
|
622
705
|
continue;
|
|
623
706
|
}
|
|
624
707
|
coverage.push({
|
|
625
708
|
room: locator,
|
|
626
709
|
generation: replica.record.transfer,
|
|
627
|
-
current: !(catching.has(locator) ||
|
|
710
|
+
current: !(catching.has(locator) || transfers.has(locator)),
|
|
628
711
|
});
|
|
629
712
|
read.push({ locator, key: replica.record.key });
|
|
630
713
|
for (const batch of replica.queue)
|
|
@@ -650,9 +733,7 @@ export function composeRuntime(options) {
|
|
|
650
733
|
return { listed: true, rooms: named.filter((room) => rooms.has(room)) };
|
|
651
734
|
return { listed: named.every((room) => replicas.has(room)), rooms: named };
|
|
652
735
|
}
|
|
653
|
-
function roomState(locator
|
|
654
|
-
if (sourceRefusals.has(locator))
|
|
655
|
-
return 'refused';
|
|
736
|
+
function roomState(locator) {
|
|
656
737
|
const listedRoom = discovered.get(locator);
|
|
657
738
|
if (roomsListed &&
|
|
658
739
|
(listedRoom === undefined ||
|
|
@@ -661,27 +742,17 @@ export function composeRuntime(options) {
|
|
|
661
742
|
return 'unadmitted';
|
|
662
743
|
if (!replicas.has(locator))
|
|
663
744
|
return 'acquiring';
|
|
664
|
-
if (!
|
|
745
|
+
if (!indexed(locator))
|
|
665
746
|
return 'preparing';
|
|
666
|
-
if (search && !searchIndex.rooms().has(locator))
|
|
667
|
-
return 'indexing';
|
|
668
747
|
return 'complete';
|
|
669
748
|
}
|
|
670
|
-
function conversationCoverage(reach
|
|
749
|
+
function conversationCoverage(reach) {
|
|
671
750
|
const declared = reach === null || reach.conversations;
|
|
672
751
|
if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
|
|
673
752
|
return 'undeclared';
|
|
674
753
|
if (!conversations.known() || !conversations.current())
|
|
675
754
|
return 'acquiring';
|
|
676
|
-
|
|
677
|
-
return 'complete';
|
|
678
|
-
const unindexed = conversations
|
|
679
|
-
.searchable()
|
|
680
|
-
.filter((one) => conversations.acquired(one) && !searchIndex.conversations().has(one));
|
|
681
|
-
if (unindexed.length === 0)
|
|
682
|
-
return 'complete';
|
|
683
|
-
index([], unindexed);
|
|
684
|
-
return 'indexing';
|
|
755
|
+
return 'complete';
|
|
685
756
|
}
|
|
686
757
|
function connectionOf() {
|
|
687
758
|
if (socketOpen)
|
|
@@ -692,58 +763,79 @@ export function composeRuntime(options) {
|
|
|
692
763
|
? 'connected'
|
|
693
764
|
: 'connecting';
|
|
694
765
|
}
|
|
695
|
-
function
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
}
|
|
766
|
+
function bytesOf(room, underway) {
|
|
767
|
+
const transfer = transfers.get(room);
|
|
768
|
+
if (transfer !== undefined) {
|
|
769
|
+
const owed = (unindexed.get(room) ?? []).reduce((sum, part) => sum + part.bytes, 0);
|
|
770
|
+
return {
|
|
771
|
+
done: transfer.done.bytes - Math.ceil(owed),
|
|
772
|
+
total: Math.max(arrivals.get(room) ?? 0, transfer.done.bytes),
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
const held = underway ? NOTHING_HELD : (replicas.get(room)?.record.inventory ?? NOTHING_HELD);
|
|
776
|
+
return { done: held.bytes, total: arrivals.get(room) ?? held.bytes };
|
|
777
|
+
}
|
|
778
|
+
function installationOf(coverage) {
|
|
779
|
+
const rooms = { done: 0, total: 0 };
|
|
780
|
+
const bytes = { done: 0, total: 0 };
|
|
781
|
+
let installing = 0;
|
|
782
|
+
let downloading = false;
|
|
783
|
+
for (const { room, state } of coverage) {
|
|
784
|
+
if (state === 'unadmitted')
|
|
785
|
+
continue;
|
|
786
|
+
rooms.total += 1;
|
|
787
|
+
if (state === 'complete')
|
|
788
|
+
rooms.done += 1;
|
|
789
|
+
const underway = state === 'acquiring' || (installs.has(room) && state !== 'complete');
|
|
790
|
+
if (underway)
|
|
791
|
+
installing += 1;
|
|
792
|
+
if (underway && (streaming.has(room) || !arrivals.has(room)))
|
|
793
|
+
downloading = true;
|
|
794
|
+
const now = bytesOf(room, underway);
|
|
795
|
+
const earlier = underway ? startedOver.get(room) : undefined;
|
|
796
|
+
bytes.done += Math.max(now.done, earlier?.done ?? 0);
|
|
797
|
+
bytes.total += Math.max(now.total, earlier?.total ?? 0);
|
|
798
|
+
}
|
|
799
|
+
const step = installing === 0
|
|
800
|
+
? null
|
|
801
|
+
: downloading
|
|
802
|
+
? 'downloading'
|
|
803
|
+
: bytes.done < bytes.total
|
|
804
|
+
? 'writing'
|
|
805
|
+
: 'replaying';
|
|
806
|
+
return { step, rooms, bytes };
|
|
710
807
|
}
|
|
711
808
|
function readinessFor(port, reach) {
|
|
712
809
|
const required = requiredRooms(reach);
|
|
713
810
|
const search = port.search === 'required';
|
|
714
|
-
const coverage = required.rooms.map((room) => ({ room, state: roomState(room
|
|
715
|
-
const
|
|
716
|
-
.filter((room) => room.state ===
|
|
811
|
+
const coverage = required.rooms.map((room) => ({ room, state: roomState(room) }));
|
|
812
|
+
const preparing = coverage
|
|
813
|
+
.filter((room) => room.state === 'preparing' && !unpreparable.has(room.room))
|
|
717
814
|
.map(({ room }) => room);
|
|
718
|
-
const preparing = building('preparing');
|
|
719
815
|
if (preparing.length > 0)
|
|
720
816
|
prepare(preparing);
|
|
721
|
-
const indexing = building('indexing');
|
|
722
|
-
if (indexing.length > 0)
|
|
723
|
-
index(indexing, []);
|
|
724
817
|
return readinessOf({
|
|
725
818
|
code: port.code,
|
|
726
819
|
coverage: {
|
|
727
820
|
listed: required.listed,
|
|
728
821
|
rooms: coverage,
|
|
729
|
-
conversations: conversationCoverage(reach
|
|
822
|
+
conversations: conversationCoverage(reach),
|
|
730
823
|
search: search ? 'required' : 'unused',
|
|
731
824
|
},
|
|
732
825
|
persistence,
|
|
733
826
|
connection: connectionOf(),
|
|
734
827
|
house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
? capacity
|
|
738
|
-
: port.capacity,
|
|
739
|
-
refused: refusedIn(required.rooms),
|
|
828
|
+
space: full ? 'full' : port.space,
|
|
829
|
+
installation: installationOf(coverage),
|
|
740
830
|
lost: storageLoss !== null,
|
|
741
|
-
established: targets[port.target]?.established === true
|
|
831
|
+
established: targets[port.target]?.established === true ||
|
|
832
|
+
(usableWhenHeld.includes(port.target) && required.rooms.some((room) => reopened.has(room))),
|
|
742
833
|
});
|
|
743
834
|
}
|
|
744
835
|
function establish(name) {
|
|
745
836
|
targets = { ...targets, [name]: { established: true } };
|
|
746
837
|
queueReadiness();
|
|
838
|
+
probeEachMinute();
|
|
747
839
|
void durably((local) => local.fact('targets', targets)).catch(() => {
|
|
748
840
|
journal.record(`target record failed ${name}`);
|
|
749
841
|
});
|
|
@@ -781,87 +873,52 @@ export function composeRuntime(options) {
|
|
|
781
873
|
readinessWatches.delete(watch);
|
|
782
874
|
};
|
|
783
875
|
}
|
|
784
|
-
function
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
}
|
|
790
|
-
function exceeded(error) {
|
|
791
|
-
return (typeof error === 'object' &&
|
|
792
|
-
error !== null &&
|
|
793
|
-
error.name === 'QuotaExceededError');
|
|
794
|
-
}
|
|
795
|
-
async function estimated() {
|
|
796
|
-
const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
|
|
797
|
-
return estimate === null ? null : Math.max(0, estimate.total - estimate.usage);
|
|
798
|
-
}
|
|
799
|
-
function pause(locator, cause) {
|
|
800
|
-
paused.add(locator);
|
|
801
|
-
capacity = cause;
|
|
802
|
-
journal.record(`capacity ${cause.cause} ${locator} ${cause.needed} ${cause.available ?? '-'}`);
|
|
876
|
+
function filled() {
|
|
877
|
+
if (full || closed)
|
|
878
|
+
return;
|
|
879
|
+
full = true;
|
|
880
|
+
journal.record('space full');
|
|
803
881
|
queueReadiness();
|
|
804
|
-
|
|
882
|
+
probeEachMinute();
|
|
883
|
+
}
|
|
884
|
+
function installedTarget() {
|
|
885
|
+
return ((usableWhenHeld.length > 0 && reopened.size > 0) ||
|
|
886
|
+
Object.values(targets).some((target) => target.established));
|
|
887
|
+
}
|
|
888
|
+
function probeEachMinute() {
|
|
889
|
+
if (!full || probing !== null || closed || !installedTarget())
|
|
805
890
|
return;
|
|
806
|
-
// A
|
|
807
|
-
|
|
808
|
-
void
|
|
891
|
+
// A device whose storage filled after its install tries a write each minute, so space freed elsewhere ends its read-only mode without a Retry.
|
|
892
|
+
probing = setInterval(() => {
|
|
893
|
+
void probed();
|
|
809
894
|
}, 60_000);
|
|
810
895
|
}
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
resume();
|
|
896
|
+
function probed() {
|
|
897
|
+
testing ??= database.ask({ kind: 'space.probe' }).then(() => {
|
|
898
|
+
testing = null;
|
|
899
|
+
spaceFreed();
|
|
900
|
+
return true;
|
|
901
|
+
}, () => {
|
|
902
|
+
testing = null;
|
|
903
|
+
return false;
|
|
904
|
+
});
|
|
905
|
+
return testing;
|
|
822
906
|
}
|
|
823
|
-
function
|
|
824
|
-
if (
|
|
825
|
-
clearInterval(rechecking);
|
|
826
|
-
rechecking = null;
|
|
827
|
-
if (paused.size === 0 && capacity === null)
|
|
907
|
+
function spaceFreed() {
|
|
908
|
+
if (!full)
|
|
828
909
|
return;
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
910
|
+
full = false;
|
|
911
|
+
if (probing !== null)
|
|
912
|
+
clearInterval(probing);
|
|
913
|
+
probing = null;
|
|
914
|
+
unpreparable.clear();
|
|
915
|
+
journal.record('space freed');
|
|
832
916
|
queueReadiness();
|
|
833
917
|
if (!closed && authorization !== undefined)
|
|
834
918
|
void sync('unstreamed');
|
|
835
919
|
}
|
|
836
|
-
async function reclaimed(local) {
|
|
837
|
-
const unreferenced = [...transfers.keys()].filter((locator) => !discovered.has(locator));
|
|
838
|
-
for (const locator of unreferenced) {
|
|
839
|
-
await local.abandon(locator);
|
|
840
|
-
transfers.delete(locator);
|
|
841
|
-
}
|
|
842
|
-
if (unreferenced.length > 0)
|
|
843
|
-
journal.record(`capacity reclaimed ${unreferenced.length}`);
|
|
844
|
-
return unreferenced.length > 0;
|
|
845
|
-
}
|
|
846
|
-
async function admitted(local, locator, needed) {
|
|
847
|
-
if (paused.has(locator))
|
|
848
|
-
return false;
|
|
849
|
-
let available = await estimated().catch(() => null);
|
|
850
|
-
if (available === null || available >= needed)
|
|
851
|
-
return true;
|
|
852
|
-
if (await reclaimed(local))
|
|
853
|
-
available = await estimated().catch(() => null);
|
|
854
|
-
if (available === null || available >= needed)
|
|
855
|
-
return true;
|
|
856
|
-
pause(locator, { cause: 'insufficient-space', needed, available });
|
|
857
|
-
return false;
|
|
858
|
-
}
|
|
859
|
-
async function exceededOn(locator, bytes) {
|
|
860
|
-
const available = await estimated().catch(() => null);
|
|
861
|
-
pause(locator, { cause: 'storage-exceeded', needed: bytes, available });
|
|
862
|
-
}
|
|
863
920
|
function seedRoomHeld(room, version) {
|
|
864
|
-
return
|
|
921
|
+
return indexed(room) && reachedBy(room, version);
|
|
865
922
|
}
|
|
866
923
|
function seedHeld(at) {
|
|
867
924
|
return Object.entries(at).every(([room, version]) => (roomsListed && !rooms.has(room)) || seedRoomHeld(room, version));
|
|
@@ -982,83 +1039,12 @@ export function composeRuntime(options) {
|
|
|
982
1039
|
const held = answered.cover;
|
|
983
1040
|
return { ...answered.rows, coverage: held.coverage };
|
|
984
1041
|
}
|
|
985
|
-
async function everyRow(plan) {
|
|
986
|
-
const window = { ...plan, limit: MAXIMUM_WINDOW, after: null, total: false };
|
|
987
|
-
const first = await rowsQuery(window);
|
|
988
|
-
if ('pending' in first || first.next === undefined)
|
|
989
|
-
return first;
|
|
990
|
-
const documents = 'documents' in first ? [...first.documents] : [];
|
|
991
|
-
const groups = 'groups' in first ? [...first.groups] : [];
|
|
992
|
-
for (let next = first.next; next !== undefined;) {
|
|
993
|
-
const values = JSON.parse(next);
|
|
994
|
-
const page = await rowsQuery({
|
|
995
|
-
...window,
|
|
996
|
-
after: { text: next, values: values.length === 0 ? null : values },
|
|
997
|
-
});
|
|
998
|
-
if ('pending' in page)
|
|
999
|
-
return page;
|
|
1000
|
-
if ('documents' in page)
|
|
1001
|
-
documents.push(...page.documents);
|
|
1002
|
-
else
|
|
1003
|
-
groups.push(...page.groups);
|
|
1004
|
-
if (JSON.stringify(page.coverage) !== JSON.stringify(first.coverage))
|
|
1005
|
-
return everyRow(plan);
|
|
1006
|
-
next = page.next;
|
|
1007
|
-
}
|
|
1008
|
-
return 'documents' in first
|
|
1009
|
-
? { documents, coverage: first.coverage }
|
|
1010
|
-
: { groups, coverage: first.coverage };
|
|
1011
|
-
}
|
|
1012
|
-
async function indexConversation(conversation) {
|
|
1013
|
-
searchIndex.indexMessages(conversation, [], false);
|
|
1014
|
-
let after = null;
|
|
1015
|
-
for (;;) {
|
|
1016
|
-
const batch = await durably((local) => local.messagesAfter(conversation, after));
|
|
1017
|
-
if (closed)
|
|
1018
|
-
return;
|
|
1019
|
-
searchIndex.indexMessages(conversation, batch.map(({ identity, text }) => ({ conversation, identity, text })), batch.length === 0);
|
|
1020
|
-
if (batch.length === 0)
|
|
1021
|
-
return;
|
|
1022
|
-
after = batch[batch.length - 1].identity;
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
function indexNext() {
|
|
1026
|
-
const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator) && !unindexable.has(locator));
|
|
1027
|
-
if (room !== undefined) {
|
|
1028
|
-
roomsToIndex.delete(room);
|
|
1029
|
-
return () => prepared.has(room)
|
|
1030
|
-
? searchIndex.indexRoom(room).catch((error) => unbuilt(room, error))
|
|
1031
|
-
: undefined;
|
|
1032
|
-
}
|
|
1033
|
-
const conversation = [...conversationsToIndex].find((one) => !searchIndex.conversations().has(one));
|
|
1034
|
-
if (conversation === undefined)
|
|
1035
|
-
return null;
|
|
1036
|
-
conversationsToIndex.delete(conversation);
|
|
1037
|
-
return () => indexConversation(conversation);
|
|
1038
|
-
}
|
|
1039
|
-
function index(rooms, named) {
|
|
1040
|
-
for (const room of rooms)
|
|
1041
|
-
roomsToIndex.add(room);
|
|
1042
|
-
for (const conversation of named)
|
|
1043
|
-
conversationsToIndex.add(conversation);
|
|
1044
|
-
if (indexing)
|
|
1045
|
-
return;
|
|
1046
|
-
indexing = true;
|
|
1047
|
-
void (async () => {
|
|
1048
|
-
try {
|
|
1049
|
-
for (let unit = indexNext(); unit !== null && !closed; unit = indexNext()) {
|
|
1050
|
-
await yielded();
|
|
1051
|
-
await unit();
|
|
1052
|
-
queueReadiness();
|
|
1053
|
-
}
|
|
1054
|
-
}
|
|
1055
|
-
finally {
|
|
1056
|
-
indexing = false;
|
|
1057
|
-
}
|
|
1058
|
-
})().catch(() => { });
|
|
1059
|
-
}
|
|
1060
1042
|
function publishChangeSet(set) {
|
|
1061
1043
|
applyChangeSet(set);
|
|
1044
|
+
for (const { locator, path } of set.documents) {
|
|
1045
|
+
if (path !== null)
|
|
1046
|
+
attachedFiles.get(locator)?.delete(path);
|
|
1047
|
+
}
|
|
1062
1048
|
if (set.rooms.length > 0 ||
|
|
1063
1049
|
[...new Set(set.documents.map(({ locator }) => locator))].some((locator) => rooms.has(locator) && counted.get(locator) !== documentsIn(locator)))
|
|
1064
1050
|
emitRooms();
|
|
@@ -1077,6 +1063,8 @@ export function composeRuntime(options) {
|
|
|
1077
1063
|
}
|
|
1078
1064
|
async function refuse(reason, provider = null) {
|
|
1079
1065
|
discoveryPaused = true;
|
|
1066
|
+
inFlight.abort();
|
|
1067
|
+
inFlight = new AbortController();
|
|
1080
1068
|
try {
|
|
1081
1069
|
if (reason === 'revoked-authorization') {
|
|
1082
1070
|
await exclusive(async () => {
|
|
@@ -1084,27 +1072,24 @@ export function composeRuntime(options) {
|
|
|
1084
1072
|
const records = [...replicas].map(([locator, held]) => ({
|
|
1085
1073
|
locator,
|
|
1086
1074
|
held,
|
|
1087
|
-
record: { ...held.record, model: revoked
|
|
1075
|
+
record: { ...held.record, model: revoked },
|
|
1088
1076
|
}));
|
|
1077
|
+
let answer = null;
|
|
1089
1078
|
try {
|
|
1090
1079
|
const local = await openLocal();
|
|
1091
|
-
await local.commit({
|
|
1080
|
+
answer = await local.commit({
|
|
1092
1081
|
rooms: records.map(({ locator, record }) => ({
|
|
1093
1082
|
locator,
|
|
1094
1083
|
key: record.key,
|
|
1095
1084
|
record: storedRecord(record),
|
|
1096
|
-
|
|
1085
|
+
composed: composedOf(composed(record)),
|
|
1097
1086
|
})),
|
|
1098
1087
|
});
|
|
1099
1088
|
}
|
|
1100
1089
|
catch {
|
|
1101
1090
|
journal.record('model revocation persistence failed');
|
|
1102
1091
|
}
|
|
1103
|
-
const keys = new Set(models.keys());
|
|
1104
|
-
for (const locator of rooms.keys()) {
|
|
1105
|
-
keys.add(modelKey(locator, 'collection'));
|
|
1106
|
-
keys.add(modelKey(locator, 'typed-selection'));
|
|
1107
|
-
}
|
|
1092
|
+
const keys = new Set([...models.keys(), ...rooms.keys()]);
|
|
1108
1093
|
for (const key of keys) {
|
|
1109
1094
|
invalidModels.add(key);
|
|
1110
1095
|
models.set(key, revoked);
|
|
@@ -1114,8 +1099,8 @@ export function composeRuntime(options) {
|
|
|
1114
1099
|
for (const { locator, held, record } of records) {
|
|
1115
1100
|
if (replicas.get(locator) !== held)
|
|
1116
1101
|
continue;
|
|
1117
|
-
|
|
1118
|
-
replicas.set(locator, { ...held, record });
|
|
1102
|
+
const indexed = answer === null ? held.indexed : committedRoom(locator, answer).indexed;
|
|
1103
|
+
replicas.set(locator, { ...held, record, indexed });
|
|
1119
1104
|
}
|
|
1120
1105
|
for (const locator of [...rooms.keys()]) {
|
|
1121
1106
|
const room = rooms.get(locator);
|
|
@@ -1159,12 +1144,12 @@ export function composeRuntime(options) {
|
|
|
1159
1144
|
? null
|
|
1160
1145
|
: {
|
|
1161
1146
|
roots: selection.roots,
|
|
1162
|
-
model: modelTypes(
|
|
1147
|
+
model: modelTypes(builtIn),
|
|
1163
1148
|
},
|
|
1164
1149
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
1165
1150
|
roomVersion: replicas.get(room.locator)?.record.roomVersion ?? null,
|
|
1166
1151
|
synchronizationAvailable,
|
|
1167
|
-
refusal: scopedRefusals.get(room.locator) ??
|
|
1152
|
+
refusal: scopedRefusals.get(room.locator) ?? null,
|
|
1168
1153
|
});
|
|
1169
1154
|
}
|
|
1170
1155
|
async function applyDiscovery(found, started, covered) {
|
|
@@ -1248,30 +1233,23 @@ export function composeRuntime(options) {
|
|
|
1248
1233
|
queuesWithoutReplica.delete(locator);
|
|
1249
1234
|
rooms.delete(locator);
|
|
1250
1235
|
roomDrafts.delete(locator);
|
|
1251
|
-
|
|
1236
|
+
attachedFiles.delete(locator);
|
|
1237
|
+
untransferred(locator);
|
|
1238
|
+
startedOver.delete(locator);
|
|
1252
1239
|
catching.delete(locator);
|
|
1253
1240
|
discovered.delete(locator);
|
|
1254
|
-
announced.delete(locator);
|
|
1255
1241
|
unreadable.delete(locator);
|
|
1256
1242
|
stale.delete(locator);
|
|
1257
1243
|
repairable.delete(locator);
|
|
1258
|
-
|
|
1259
|
-
paused.delete(locator);
|
|
1260
|
-
prepared.delete(locator);
|
|
1244
|
+
unpreparable.delete(locator);
|
|
1261
1245
|
scopedRefusals.delete(locator);
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
const key = modelKey(locator, attached);
|
|
1266
|
-
models.delete(key);
|
|
1267
|
-
liveModels.delete(key);
|
|
1268
|
-
invalidModels.delete(key);
|
|
1269
|
-
}
|
|
1246
|
+
models.delete(locator);
|
|
1247
|
+
liveModels.delete(locator);
|
|
1248
|
+
invalidModels.delete(locator);
|
|
1270
1249
|
journal.record(`room lost ${locator}`);
|
|
1271
1250
|
}
|
|
1272
1251
|
if (fresh.length === 0)
|
|
1273
1252
|
return fresh;
|
|
1274
|
-
searchIndex.forget(fresh);
|
|
1275
1253
|
publishChangeSet(mergeChangeSets(removed.map(({ locator, drafts }) => changeSet(locator, { documents: null, room: true, queue: true, drafts }))));
|
|
1276
1254
|
liveQueries.recover();
|
|
1277
1255
|
moveTo({ ...status, ...attention() });
|
|
@@ -1315,7 +1293,14 @@ export function composeRuntime(options) {
|
|
|
1315
1293
|
});
|
|
1316
1294
|
await eraseFenced(fresh);
|
|
1317
1295
|
}
|
|
1318
|
-
function
|
|
1296
|
+
function retyped(persisted) {
|
|
1297
|
+
if (persisted.status === 'unavailable')
|
|
1298
|
+
return persisted;
|
|
1299
|
+
const { state } = attachedModel({ kind: 'model', snapshot: persisted.snapshot }, NO_MODEL, roomTypes);
|
|
1300
|
+
return state.status === 'unavailable' ? state : { ...persisted, types: state.types };
|
|
1301
|
+
}
|
|
1302
|
+
function restored(key, held) {
|
|
1303
|
+
const persisted = retyped(held);
|
|
1319
1304
|
const prior = models.get(key);
|
|
1320
1305
|
if (prior !== undefined &&
|
|
1321
1306
|
prior.status !== 'unavailable' &&
|
|
@@ -1346,8 +1331,9 @@ export function composeRuntime(options) {
|
|
|
1346
1331
|
const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
|
|
1347
1332
|
targets = held.targets;
|
|
1348
1333
|
transfers.clear();
|
|
1349
|
-
|
|
1350
|
-
|
|
1334
|
+
arrivals.clear();
|
|
1335
|
+
streaming.clear();
|
|
1336
|
+
startedOver.clear();
|
|
1351
1337
|
limits = held.providerLimits;
|
|
1352
1338
|
capabilities = held.capabilities;
|
|
1353
1339
|
member = held.member;
|
|
@@ -1357,7 +1343,7 @@ export function composeRuntime(options) {
|
|
|
1357
1343
|
presentations.clear();
|
|
1358
1344
|
for (const record of held.presentation)
|
|
1359
1345
|
presentations.set(record.app, record.value);
|
|
1360
|
-
conversations.hydrate(heldConversations, chatDrafts);
|
|
1346
|
+
conversations.hydrate(heldConversations, chatDrafts, held.conversationList);
|
|
1361
1347
|
pendings.clear();
|
|
1362
1348
|
for (const queue of queues) {
|
|
1363
1349
|
for (const { batch, state } of queue.pending) {
|
|
@@ -1365,7 +1351,7 @@ export function composeRuntime(options) {
|
|
|
1365
1351
|
}
|
|
1366
1352
|
}
|
|
1367
1353
|
replicas.clear();
|
|
1368
|
-
|
|
1354
|
+
reopened.clear();
|
|
1369
1355
|
queuesWithoutReplica.clear();
|
|
1370
1356
|
for (const [locator, queue] of queuedBatches) {
|
|
1371
1357
|
if (!held.rooms.has(locator))
|
|
@@ -1374,39 +1360,40 @@ export function composeRuntime(options) {
|
|
|
1374
1360
|
records.forEach((stored, index) => {
|
|
1375
1361
|
if (fences.has(stored.locator))
|
|
1376
1362
|
return;
|
|
1377
|
-
const
|
|
1378
|
-
|
|
1379
|
-
const record = { ...stored, model, selectionModel };
|
|
1363
|
+
const record = { ...stored, model: restored(stored.locator, stored.model) };
|
|
1364
|
+
reopened.add(record.locator);
|
|
1380
1365
|
replicas.set(record.locator, {
|
|
1381
1366
|
record,
|
|
1382
1367
|
queue: queuedBatches.get(record.locator) ?? [],
|
|
1383
1368
|
overlaid: held.overlaid.get(record.key) ?? new Set(),
|
|
1384
1369
|
count: counts[index] ?? 0,
|
|
1385
1370
|
confirmed: 0,
|
|
1371
|
+
indexed: !held.unindexed.has(record.key),
|
|
1386
1372
|
});
|
|
1387
|
-
if (!held.unprepared.has(record.key))
|
|
1388
|
-
prepared.add(record.locator);
|
|
1389
1373
|
const existing = rooms.get(record.locator);
|
|
1390
1374
|
if (existing === undefined)
|
|
1391
|
-
list(record,
|
|
1375
|
+
list(record, record.roomVersion !== null);
|
|
1392
1376
|
else {
|
|
1393
1377
|
rooms.set(record.locator, {
|
|
1394
1378
|
...existing,
|
|
1395
1379
|
roomVersion: record.roomVersion,
|
|
1396
1380
|
operations: record.operations,
|
|
1397
1381
|
replica: record.replica,
|
|
1398
|
-
model: modelTypes(model),
|
|
1382
|
+
model: modelTypes(record.model),
|
|
1399
1383
|
typedSelection: record.typedSelection === null
|
|
1400
1384
|
? null
|
|
1401
|
-
: {
|
|
1402
|
-
roots: record.typedSelection.roots,
|
|
1403
|
-
model: modelTypes(selectionModel),
|
|
1404
|
-
},
|
|
1385
|
+
: { roots: record.typedSelection.roots, model: modelTypes(builtIn) },
|
|
1405
1386
|
});
|
|
1406
1387
|
}
|
|
1407
1388
|
});
|
|
1408
|
-
|
|
1409
|
-
|
|
1389
|
+
try {
|
|
1390
|
+
for (const key of held.orphans)
|
|
1391
|
+
await opened.release(key);
|
|
1392
|
+
}
|
|
1393
|
+
catch (error) {
|
|
1394
|
+
if (!refusedForSpace(error))
|
|
1395
|
+
throw error;
|
|
1396
|
+
}
|
|
1410
1397
|
if (held.erasing || held.orphans.length > 0)
|
|
1411
1398
|
queueMicrotask(sweep);
|
|
1412
1399
|
return {
|
|
@@ -1431,8 +1418,7 @@ export function composeRuntime(options) {
|
|
|
1431
1418
|
async function open() {
|
|
1432
1419
|
if (closed)
|
|
1433
1420
|
throw new Error('runtime closed');
|
|
1434
|
-
const opened =
|
|
1435
|
-
store = opened;
|
|
1421
|
+
const opened = openRuntimeStore(isFenced, database);
|
|
1436
1422
|
const interrupted = await opened.fences();
|
|
1437
1423
|
for (const locator of interrupted) {
|
|
1438
1424
|
fences.set(locator, { at: discoveries, erased: false });
|
|
@@ -1478,10 +1464,29 @@ export function composeRuntime(options) {
|
|
|
1478
1464
|
return opening;
|
|
1479
1465
|
}
|
|
1480
1466
|
function exclusive(work) {
|
|
1467
|
+
gathering = null;
|
|
1481
1468
|
const next = mutations.then(work, work);
|
|
1482
1469
|
mutations = next.then(() => undefined, () => undefined);
|
|
1483
1470
|
return next;
|
|
1484
1471
|
}
|
|
1472
|
+
function gathered(key, input, merge, work) {
|
|
1473
|
+
const tail = gathering;
|
|
1474
|
+
if (tail !== null && tail.key === key) {
|
|
1475
|
+
const merged = merge(tail.input, input);
|
|
1476
|
+
if (merged !== null) {
|
|
1477
|
+
tail.input = merged;
|
|
1478
|
+
return tail.done;
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
const entry = { key, input, done: Promise.resolve() };
|
|
1482
|
+
entry.done = exclusive(() => {
|
|
1483
|
+
if (gathering === entry)
|
|
1484
|
+
gathering = null;
|
|
1485
|
+
return work(entry.input);
|
|
1486
|
+
});
|
|
1487
|
+
gathering = entry;
|
|
1488
|
+
return entry.done;
|
|
1489
|
+
}
|
|
1485
1490
|
function withdrawable(locator) {
|
|
1486
1491
|
return (batch) => {
|
|
1487
1492
|
const state = pendings.get(pendingKey(locator, batch.id));
|
|
@@ -1563,26 +1568,14 @@ export function composeRuntime(options) {
|
|
|
1563
1568
|
},
|
|
1564
1569
|
};
|
|
1565
1570
|
}
|
|
1566
|
-
function
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
fingerprint(compositionOf(before)) === fingerprint(compositionOf(after))) {
|
|
1570
|
-
return;
|
|
1571
|
-
}
|
|
1572
|
-
if (before !== undefined && before.key === after.key)
|
|
1573
|
-
prepared.delete(locator);
|
|
1574
|
-
}
|
|
1575
|
-
function committedRoom(locator, record, answer) {
|
|
1576
|
-
if (answer.prepared[locator] === false)
|
|
1577
|
-
prepared.delete(locator);
|
|
1578
|
-
else if (answer.prepared[locator] === true)
|
|
1579
|
-
prepared.add(locator);
|
|
1580
|
-
if (!prepared.has(locator))
|
|
1571
|
+
function committedRoom(locator, answer) {
|
|
1572
|
+
const indexed = answer.indexed[locator] ?? replicas.get(locator)?.indexed ?? false;
|
|
1573
|
+
if (!indexed)
|
|
1581
1574
|
prepare([locator]);
|
|
1582
|
-
return { count: answer.counts[locator] ?? replicas.get(locator)?.count ?? 0 };
|
|
1575
|
+
return { count: answer.counts[locator] ?? replicas.get(locator)?.count ?? 0, indexed };
|
|
1583
1576
|
}
|
|
1584
1577
|
function replayed(record, confirmedLayer, queue) {
|
|
1585
|
-
return stepped(replaying(confirmedLayer, queue, record.locator,
|
|
1578
|
+
return stepped(replaying(confirmedLayer, queue, record.locator, composed(record)));
|
|
1586
1579
|
}
|
|
1587
1580
|
async function overlayDiff(record, before, visible, touched) {
|
|
1588
1581
|
const kept = before !== undefined && before.record.key === record.key
|
|
@@ -1602,8 +1595,6 @@ export function composeRuntime(options) {
|
|
|
1602
1595
|
function unpairedIn(locator, batch) {
|
|
1603
1596
|
if (!queueIn(locator).some((held) => held.id === batch.id))
|
|
1604
1597
|
return false;
|
|
1605
|
-
if (pendings.get(pendingKey(locator, batch.id))?.accepted !== undefined)
|
|
1606
|
-
return false;
|
|
1607
1598
|
return !leavingIn(batch, locator) || withdrawable(locator)(batch);
|
|
1608
1599
|
}
|
|
1609
1600
|
async function together(local, settlements) {
|
|
@@ -1645,7 +1636,7 @@ export function composeRuntime(options) {
|
|
|
1645
1636
|
const confirmedLayer = overrides === undefined
|
|
1646
1637
|
? read
|
|
1647
1638
|
: await stepped(slicingWith(read, overrides.set, overrides.removed));
|
|
1648
|
-
const composition =
|
|
1639
|
+
const composition = composed(record);
|
|
1649
1640
|
const { withdrawn, kept } = await stepped(standing(confirmedLayer, queue, withdrawable(locator), localSaves, locator, composition));
|
|
1650
1641
|
const founded = kept.filter((batch) => !queue.includes(batch));
|
|
1651
1642
|
const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
|
|
@@ -1688,6 +1679,7 @@ export function composeRuntime(options) {
|
|
|
1688
1679
|
key: record.key,
|
|
1689
1680
|
record: storedRecord(record),
|
|
1690
1681
|
roots: record.typedSelection?.roots ?? [],
|
|
1682
|
+
composed: composedOf(composition),
|
|
1691
1683
|
...settling.commit,
|
|
1692
1684
|
...(overrides === undefined
|
|
1693
1685
|
? {}
|
|
@@ -1729,14 +1721,14 @@ export function composeRuntime(options) {
|
|
|
1729
1721
|
const confirmedChanged = settling.commit?.published !== undefined ||
|
|
1730
1722
|
settling.commit?.refreshed === true ||
|
|
1731
1723
|
overrides !== undefined;
|
|
1732
|
-
|
|
1733
|
-
const { count } = committedRoom(locator, record, answer);
|
|
1724
|
+
const { count, indexed } = committedRoom(locator, answer);
|
|
1734
1725
|
replicas.set(locator, {
|
|
1735
1726
|
record,
|
|
1736
1727
|
queue: kept,
|
|
1737
1728
|
overlaid: touched,
|
|
1738
1729
|
count,
|
|
1739
1730
|
confirmed: (before?.confirmed ?? 0) + (confirmedChanged ? 1 : 0),
|
|
1731
|
+
indexed,
|
|
1740
1732
|
});
|
|
1741
1733
|
return mergeChangeSets([
|
|
1742
1734
|
changeSet(locator, {
|
|
@@ -1751,103 +1743,96 @@ export function composeRuntime(options) {
|
|
|
1751
1743
|
};
|
|
1752
1744
|
}
|
|
1753
1745
|
function adoptModels(locator, record) {
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
if (model.status === 'current')
|
|
1762
|
-
liveModels.set(key, JSON.stringify(model.snapshot));
|
|
1763
|
-
else if (model.status === 'unavailable')
|
|
1764
|
-
liveModels.delete(key);
|
|
1765
|
-
}
|
|
1746
|
+
const model = record.model;
|
|
1747
|
+
models.set(locator, model);
|
|
1748
|
+
invalidModels.delete(locator);
|
|
1749
|
+
if (model.status === 'current')
|
|
1750
|
+
liveModels.set(locator, JSON.stringify(model.snapshot));
|
|
1751
|
+
else if (model.status === 'unavailable')
|
|
1752
|
+
liveModels.delete(locator);
|
|
1766
1753
|
}
|
|
1767
1754
|
function remodel(locator, answers, held) {
|
|
1768
|
-
const
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
};
|
|
1779
|
-
return {
|
|
1780
|
-
collection: taken(answers?.collection, held.collection, 'collection'),
|
|
1781
|
-
selection: taken(answers?.selection, held.selection, 'typed-selection'),
|
|
1782
|
-
};
|
|
1755
|
+
const answer = answers?.collection;
|
|
1756
|
+
if (answer === undefined)
|
|
1757
|
+
return held;
|
|
1758
|
+
const { state, refused } = attachedModel(answer, held.collection, roomTypes);
|
|
1759
|
+
if (refused !== null) {
|
|
1760
|
+
const where = refused.location === undefined ? '' : ` ${refused.location}`;
|
|
1761
|
+
journal.record(`model invalid ${locator} collection ${refused.identity} ${refused.rule}${where}`);
|
|
1762
|
+
}
|
|
1763
|
+
models.set(locator, state);
|
|
1764
|
+
return { collection: state };
|
|
1783
1765
|
}
|
|
1784
|
-
function publish(local, room,
|
|
1785
|
-
|
|
1766
|
+
function publish(local, room, reached, landing, live) {
|
|
1767
|
+
const landed = exclusive(async () => {
|
|
1786
1768
|
if (!live()) {
|
|
1787
1769
|
caughtUpTo(room.locator);
|
|
1788
1770
|
return true;
|
|
1789
1771
|
}
|
|
1790
1772
|
const prior = replicas.get(room.locator);
|
|
1791
|
-
if (landing.
|
|
1792
|
-
(
|
|
1793
|
-
|
|
1773
|
+
if (!stands(room.locator, landing.from)) {
|
|
1774
|
+
if (landing.kind === 'refreshed') {
|
|
1775
|
+
await local.unrefresh(reached.key);
|
|
1776
|
+
return false;
|
|
1777
|
+
}
|
|
1778
|
+
caughtUpTo(room.locator);
|
|
1779
|
+
await abandoned(local, room.locator);
|
|
1794
1780
|
return false;
|
|
1795
1781
|
}
|
|
1782
|
+
const record = landing.kind === 'refreshed' && prior !== undefined
|
|
1783
|
+
? {
|
|
1784
|
+
...prior.record,
|
|
1785
|
+
roomVersion: reached.roomVersion,
|
|
1786
|
+
...(landing.remodelled ? { model: reached.model } : {}),
|
|
1787
|
+
}
|
|
1788
|
+
: reached;
|
|
1796
1789
|
adoptModels(room.locator, record);
|
|
1797
1790
|
const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
|
|
1798
|
-
const roomVersion = record.roomVersion;
|
|
1799
|
-
const accepted = queue.filter((batch) => {
|
|
1800
|
-
const at = pendings.get(pendingKey(room.locator, batch.id))?.accepted;
|
|
1801
|
-
return at !== undefined && options.adapter.reached(room.locator, roomVersion, at);
|
|
1802
|
-
});
|
|
1803
1791
|
let set;
|
|
1804
1792
|
try {
|
|
1805
|
-
set = await settle(local, room.locator, prior, record, queue
|
|
1793
|
+
set = await settle(local, room.locator, prior, record, queue, landing.kind === 'refreshed'
|
|
1806
1794
|
? {
|
|
1807
1795
|
room: true,
|
|
1808
1796
|
view: 'refreshed',
|
|
1809
|
-
commit: { refreshed: true
|
|
1797
|
+
commit: { refreshed: true },
|
|
1810
1798
|
}
|
|
1811
1799
|
: {
|
|
1812
1800
|
room: true,
|
|
1813
1801
|
view: landing.refreshed ? 'refreshed' : 'confirmed',
|
|
1814
1802
|
commit: {
|
|
1815
1803
|
published: { from: prior?.record.key ?? null },
|
|
1816
|
-
composition: compositionOf(record),
|
|
1817
1804
|
...(landing.refreshed ? { refreshed: true } : {}),
|
|
1818
1805
|
},
|
|
1819
1806
|
});
|
|
1820
1807
|
}
|
|
1821
1808
|
catch (error) {
|
|
1822
|
-
if (!
|
|
1809
|
+
if (!refusedForSpace(error))
|
|
1823
1810
|
throw error;
|
|
1824
|
-
await exceededOn(room.locator, null);
|
|
1825
1811
|
caughtUpTo(room.locator);
|
|
1826
1812
|
return true;
|
|
1827
1813
|
}
|
|
1828
1814
|
if (landing.kind === 'bootstrapped') {
|
|
1829
|
-
|
|
1815
|
+
untransferred(room.locator);
|
|
1816
|
+
startedOver.delete(room.locator);
|
|
1830
1817
|
catching.delete(room.locator);
|
|
1831
|
-
|
|
1818
|
+
if (prior !== undefined && prior.record.key !== record.key)
|
|
1819
|
+
sweep();
|
|
1820
|
+
name(room.locator);
|
|
1832
1821
|
}
|
|
1833
|
-
for (const batch of accepted)
|
|
1834
|
-
pendings.delete(pendingKey(room.locator, batch.id));
|
|
1835
1822
|
stale.delete(room.locator);
|
|
1836
|
-
|
|
1823
|
+
unpreparable.delete(room.locator);
|
|
1837
1824
|
if (unreadable.delete(room.locator))
|
|
1838
1825
|
scopedRefusals.delete(room.locator);
|
|
1839
|
-
sourceRefusals.delete(room.locator);
|
|
1840
|
-
declarationRefusals.delete(room.locator);
|
|
1841
1826
|
list(room, true);
|
|
1842
1827
|
publishChangeSet(set);
|
|
1843
1828
|
await unpair(local);
|
|
1844
1829
|
moveTo({ ...status, ...attention() });
|
|
1845
|
-
|
|
1846
|
-
if (accepted.length > 0 && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
|
|
1830
|
+
if (landing.kind === 'bootstrapped' && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
|
|
1847
1831
|
submitLater(room.locator);
|
|
1848
1832
|
}
|
|
1849
1833
|
return true;
|
|
1850
1834
|
});
|
|
1835
|
+
return landing.kind === 'refreshed' ? landed : landed.finally(() => installed(room.locator));
|
|
1851
1836
|
}
|
|
1852
1837
|
async function savedRoom(locator) {
|
|
1853
1838
|
const local = await openLocal();
|
|
@@ -1872,12 +1857,7 @@ export function composeRuntime(options) {
|
|
|
1872
1857
|
return committed(locator, target, saved, new Set(saved.map(({ path }) => path)), writtenBy);
|
|
1873
1858
|
}
|
|
1874
1859
|
async function saveParts(locator, named, writtenBy) {
|
|
1875
|
-
const read = named
|
|
1876
|
-
if (read.some(({ path }, at) => path === '' ||
|
|
1877
|
-
normalize(path) !== path ||
|
|
1878
|
-
read.findIndex((one) => one.path === path) !== at)) {
|
|
1879
|
-
throw runtimeError('room-draft-malformed');
|
|
1880
|
-
}
|
|
1860
|
+
const read = readSavedParts(named);
|
|
1881
1861
|
const target = await savedRoom(locator);
|
|
1882
1862
|
const saved = [];
|
|
1883
1863
|
const files = [];
|
|
@@ -1886,7 +1866,7 @@ export function composeRuntime(options) {
|
|
|
1886
1866
|
for (const { path, parts } of read) {
|
|
1887
1867
|
const file = parts.file === undefined
|
|
1888
1868
|
? undefined
|
|
1889
|
-
: await heldFile(parts.file.blob, binaryBase(visible, path));
|
|
1869
|
+
: await heldFile(parts.file.blob, parts.file.base === undefined ? binaryBase(visible, path) : parts.file.base);
|
|
1890
1870
|
if (parts.file !== undefined && file !== undefined) {
|
|
1891
1871
|
files.push({ key: file.localId, blob: parts.file.blob });
|
|
1892
1872
|
}
|
|
@@ -1921,13 +1901,14 @@ export function composeRuntime(options) {
|
|
|
1921
1901
|
const referring = removed.length === 0
|
|
1922
1902
|
? []
|
|
1923
1903
|
: (await local.referrers(record.key, 'confirmed', removed, CHECKED_LINKS)).map(({ path }) => path);
|
|
1924
|
-
const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...referring], moved);
|
|
1904
|
+
const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...paths.flatMap(ancestors), ...referring], moved);
|
|
1925
1905
|
const { visible } = await replayed(record, confirmedLayer, replica.queue);
|
|
1926
|
-
const composition =
|
|
1906
|
+
const composition = composed(record);
|
|
1927
1907
|
return {
|
|
1928
1908
|
record,
|
|
1929
1909
|
confirmed: confirmedLayer,
|
|
1930
1910
|
visible,
|
|
1911
|
+
folders: new Set(await local.folders(record.key, 'visible', paths)),
|
|
1931
1912
|
*visibleOver(queue) {
|
|
1932
1913
|
return (yield* replaying(confirmedLayer, queue, record.locator, composition)).visible;
|
|
1933
1914
|
},
|
|
@@ -1939,13 +1920,7 @@ export function composeRuntime(options) {
|
|
|
1939
1920
|
return false;
|
|
1940
1921
|
const local = await openLocal();
|
|
1941
1922
|
const roots = record.typedSelection?.roots ?? [];
|
|
1942
|
-
|
|
1943
|
-
(await local.typedWithin(record.key, 'visible', roots, false))) {
|
|
1944
|
-
return true;
|
|
1945
|
-
}
|
|
1946
|
-
return (composition.selection !== null &&
|
|
1947
|
-
composition.selection.model.status === 'unavailable' &&
|
|
1948
|
-
(await local.typedWithin(record.key, 'visible', roots, true)));
|
|
1923
|
+
return local.typedWithin(record.key, 'visible', roots, false);
|
|
1949
1924
|
}
|
|
1950
1925
|
function textDiffers(left, right) {
|
|
1951
1926
|
return ((left?.content ?? undefined) !== (right?.content ?? undefined) ||
|
|
@@ -1956,18 +1931,6 @@ export function composeRuntime(options) {
|
|
|
1956
1931
|
return undefined;
|
|
1957
1932
|
return entry.binary !== null ? null : entry.content;
|
|
1958
1933
|
}
|
|
1959
|
-
function walkOf(local, key, view) {
|
|
1960
|
-
return async (after) => {
|
|
1961
|
-
const page = await local.walk(key, view, after, WALK_BYTES);
|
|
1962
|
-
return {
|
|
1963
|
-
entries: page.entries.map(({ path, content, binary }) => ({
|
|
1964
|
-
path,
|
|
1965
|
-
content: binary !== null ? null : content,
|
|
1966
|
-
})),
|
|
1967
|
-
next: page.next,
|
|
1968
|
-
};
|
|
1969
|
-
};
|
|
1970
|
-
}
|
|
1971
1934
|
function lookupOf(local, key, view) {
|
|
1972
1935
|
return async (paths) => {
|
|
1973
1936
|
if (paths.length === 0)
|
|
@@ -1984,37 +1947,6 @@ export function composeRuntime(options) {
|
|
|
1984
1947
|
const read = parseFrontmatter(entry.content ?? '');
|
|
1985
1948
|
return { type: read.ok ? (read.frontmatter.type ?? 'any') : undefined, binary: false };
|
|
1986
1949
|
}
|
|
1987
|
-
function overriddenWalk(walk, overrides) {
|
|
1988
|
-
const sorted = [...overrides.keys()].sort();
|
|
1989
|
-
const beyond = (path) => {
|
|
1990
|
-
if (path === null)
|
|
1991
|
-
return 0;
|
|
1992
|
-
let low = 0;
|
|
1993
|
-
let high = sorted.length;
|
|
1994
|
-
while (low < high) {
|
|
1995
|
-
const middle = (low + high) >>> 1;
|
|
1996
|
-
if (sorted[middle] <= path)
|
|
1997
|
-
low = middle + 1;
|
|
1998
|
-
else
|
|
1999
|
-
high = middle;
|
|
2000
|
-
}
|
|
2001
|
-
return low;
|
|
2002
|
-
};
|
|
2003
|
-
return async (after) => {
|
|
2004
|
-
const page = await walk(after);
|
|
2005
|
-
const end = page.next;
|
|
2006
|
-
const added = sorted
|
|
2007
|
-
.slice(beyond(after), end === null ? sorted.length : beyond(end))
|
|
2008
|
-
.flatMap((path) => {
|
|
2009
|
-
const entry = overrides.get(path);
|
|
2010
|
-
return entry === null || entry === undefined
|
|
2011
|
-
? []
|
|
2012
|
-
: [{ path, content: entry.binary !== null ? null : entry.content }];
|
|
2013
|
-
});
|
|
2014
|
-
const entries = [...page.entries.filter(({ path }) => !overrides.has(path)), ...added].sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0));
|
|
2015
|
-
return { entries, next: end };
|
|
2016
|
-
};
|
|
2017
|
-
}
|
|
2018
1950
|
function overriddenLookup(lookup, overrides) {
|
|
2019
1951
|
return async (paths) => {
|
|
2020
1952
|
const known = new Map(await lookup(paths.filter((path) => !overrides.has(path))));
|
|
@@ -2032,9 +1964,8 @@ export function composeRuntime(options) {
|
|
|
2032
1964
|
}
|
|
2033
1965
|
function validatorOf(record, confirmedLayer) {
|
|
2034
1966
|
return async (resulting, touched) => {
|
|
2035
|
-
const composition = compositionFor(record);
|
|
2036
1967
|
const local = await openLocal();
|
|
2037
|
-
const
|
|
1968
|
+
const changes = [];
|
|
2038
1969
|
const overrides = new Map();
|
|
2039
1970
|
const pace = paced();
|
|
2040
1971
|
for (const path of new Set([...confirmedLayer.held, ...touched])) {
|
|
@@ -2045,19 +1976,13 @@ export function composeRuntime(options) {
|
|
|
2045
1976
|
continue;
|
|
2046
1977
|
overrides.set(path, now);
|
|
2047
1978
|
if (textOf(was) !== textOf(now))
|
|
2048
|
-
|
|
1979
|
+
changes.push({ path, was: textOf(was), now: textOf(now) });
|
|
2049
1980
|
}
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
texts: textsOf(local, record.key, 'confirmed'),
|
|
2056
|
-
referrers: referrersOf(local, record.key, 'confirmed'),
|
|
2057
|
-
}));
|
|
2058
|
-
if (clean)
|
|
2059
|
-
return { verdict: 'accept' };
|
|
2060
|
-
return validatedCollection(overriddenWalk(walkOf(local, record.key, 'confirmed'), overrides), lookup, composition);
|
|
1981
|
+
return validatedNeighbourhood(changes, compositionFor(record), {
|
|
1982
|
+
lookup: overriddenLookup(lookupOf(local, record.key, 'confirmed'), overrides),
|
|
1983
|
+
texts: textsOf(local, record.key, 'confirmed'),
|
|
1984
|
+
referrers: referrersOf(local, record.key, 'confirmed'),
|
|
1985
|
+
});
|
|
2061
1986
|
};
|
|
2062
1987
|
}
|
|
2063
1988
|
function textsOf(local, key, view) {
|
|
@@ -2081,6 +2006,7 @@ export function composeRuntime(options) {
|
|
|
2081
2006
|
operations: room.operations,
|
|
2082
2007
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
2083
2008
|
visible: saving.visible,
|
|
2009
|
+
folders: saving.folders,
|
|
2084
2010
|
queued: queuedDocuments(replica.queue),
|
|
2085
2011
|
unmodeled: await unmodeledIn(record),
|
|
2086
2012
|
validate: validatorOf(record, saving.confirmed),
|
|
@@ -2101,6 +2027,7 @@ export function composeRuntime(options) {
|
|
|
2101
2027
|
locator,
|
|
2102
2028
|
key: saving.record.key,
|
|
2103
2029
|
roots: saving.record.typedSelection?.roots ?? [],
|
|
2030
|
+
composed: composedOf(composed(saving.record)),
|
|
2104
2031
|
overlay: diff.overlay,
|
|
2105
2032
|
});
|
|
2106
2033
|
}
|
|
@@ -2132,7 +2059,7 @@ export function composeRuntime(options) {
|
|
|
2132
2059
|
}
|
|
2133
2060
|
const moves = saved.map(({ path, parts }) => ({ path, to: moveTarget(parts.move) }));
|
|
2134
2061
|
const sourceSaving = await savingIn(replica, moves.map(({ path }) => ({ kind: 'remove', path })), saved.map(({ path }) => path), moves.map(({ path }) => path));
|
|
2135
|
-
const changed = saved.flatMap(({ path, parts }) => changedParts(path,
|
|
2062
|
+
const changed = saved.flatMap(({ path, parts }) => changedParts(path, parts, sourceSaving.visible));
|
|
2136
2063
|
if (changed.length > 0)
|
|
2137
2064
|
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
2138
2065
|
const target = replicas.get(destination);
|
|
@@ -2244,6 +2171,8 @@ export function composeRuntime(options) {
|
|
|
2244
2171
|
submitLater(locator);
|
|
2245
2172
|
}
|
|
2246
2173
|
async function committed(locator, target, saved, consumed, writtenBy, files = []) {
|
|
2174
|
+
if (full)
|
|
2175
|
+
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
2247
2176
|
const into = movedInto(locator, saved);
|
|
2248
2177
|
if (into !== null) {
|
|
2249
2178
|
return committedMove(locator, into, target, saved, consumed, writtenBy, files);
|
|
@@ -2255,7 +2184,7 @@ export function composeRuntime(options) {
|
|
|
2255
2184
|
...saved.map(({ path }) => path),
|
|
2256
2185
|
...parts.flatMap(({ bases }) => bases.map(({ path }) => path)),
|
|
2257
2186
|
]);
|
|
2258
|
-
const changed = saved.flatMap(({ path, parts: held }) => changedParts(path,
|
|
2187
|
+
const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, held, saving.visible));
|
|
2259
2188
|
if (changed.length > 0)
|
|
2260
2189
|
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
2261
2190
|
const requeue = await stepped(requeuing(replica.queue, changes, saving.visible, withdrawable(locator), saving.visibleOver));
|
|
@@ -2283,6 +2212,7 @@ export function composeRuntime(options) {
|
|
|
2283
2212
|
providerLimits: limits,
|
|
2284
2213
|
handle: room.handle,
|
|
2285
2214
|
visible: saving.visible,
|
|
2215
|
+
folders: saving.folders,
|
|
2286
2216
|
changes,
|
|
2287
2217
|
bases: parts.flatMap(({ bases }) => bases),
|
|
2288
2218
|
queued: queuedDocuments(replica.queue),
|
|
@@ -2433,6 +2363,9 @@ export function composeRuntime(options) {
|
|
|
2433
2363
|
async function readHistory(locator, path, query = {}) {
|
|
2434
2364
|
if (closed || refusal !== null)
|
|
2435
2365
|
return UNCERTAIN;
|
|
2366
|
+
await firstDiscovery;
|
|
2367
|
+
if (!roomsNamed)
|
|
2368
|
+
return UNCERTAIN;
|
|
2436
2369
|
try {
|
|
2437
2370
|
await ensureAuthorization();
|
|
2438
2371
|
}
|
|
@@ -2522,14 +2455,12 @@ export function composeRuntime(options) {
|
|
|
2522
2455
|
return (await openLocal()).file(localId, change?.kind === 'binary' ? change.mediaType : '');
|
|
2523
2456
|
};
|
|
2524
2457
|
}
|
|
2525
|
-
function binaryRoom(request) {
|
|
2526
|
-
if (request.kind === 'path')
|
|
2527
|
-
return request.locator;
|
|
2528
|
-
return options.adapter.referrerScope(request.referrer)?.locator ?? null;
|
|
2529
|
-
}
|
|
2530
2458
|
async function transferBinary(request, transfer, answer) {
|
|
2531
2459
|
if (closed || refusal !== null)
|
|
2532
2460
|
return UNCERTAIN;
|
|
2461
|
+
await firstDiscovery;
|
|
2462
|
+
if (!roomsNamed)
|
|
2463
|
+
return UNCERTAIN;
|
|
2533
2464
|
try {
|
|
2534
2465
|
await ensureAuthorization();
|
|
2535
2466
|
}
|
|
@@ -2539,16 +2470,13 @@ export function composeRuntime(options) {
|
|
|
2539
2470
|
if (closed || authorization === undefined || refusal !== null)
|
|
2540
2471
|
return UNCERTAIN;
|
|
2541
2472
|
const since = fencings;
|
|
2542
|
-
journal.record(request.
|
|
2543
|
-
? `transfer binary ${request.locator} ${request.path}`
|
|
2544
|
-
: `transfer binary ${request.fileRef}`);
|
|
2473
|
+
journal.record(`transfer binary ${request.locator} ${request.path}`);
|
|
2545
2474
|
try {
|
|
2546
2475
|
const result = await transfer(authorization);
|
|
2547
2476
|
const settled = result;
|
|
2548
2477
|
journal.record(`outcome binary ${settled.kind}`);
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
return failure({ kind: 'room-not-readable', locator });
|
|
2478
|
+
if (fencedSince(request.locator, since))
|
|
2479
|
+
return failure({ kind: 'room-not-readable', locator: request.locator });
|
|
2552
2480
|
if (settled.kind === 'scoped-refusal') {
|
|
2553
2481
|
const room = replicas.get(settled.locator)?.record;
|
|
2554
2482
|
if (room !== undefined)
|
|
@@ -2563,23 +2491,27 @@ export function composeRuntime(options) {
|
|
|
2563
2491
|
return UNCERTAIN;
|
|
2564
2492
|
}
|
|
2565
2493
|
}
|
|
2494
|
+
function fetchBinary(request, name) {
|
|
2495
|
+
return transferBinary(request, (authorization) => keptFiles.fetch(name, () => options.adapter.binary(authorization, request)), binaryAnswer);
|
|
2496
|
+
}
|
|
2566
2497
|
async function readBinary(request, name) {
|
|
2567
2498
|
if (closed)
|
|
2568
2499
|
return UNCERTAIN;
|
|
2569
2500
|
const held = await keptFiles.find(name);
|
|
2570
2501
|
if (held !== null)
|
|
2571
2502
|
return held;
|
|
2572
|
-
return
|
|
2503
|
+
return fetchBinary(request, name);
|
|
2573
2504
|
}
|
|
2574
2505
|
async function binaryPath(locator, path, revision) {
|
|
2575
2506
|
if (revision !== undefined)
|
|
2576
2507
|
return { kind: 'path', locator, path, revision };
|
|
2577
2508
|
const replica = replicas.get(locator);
|
|
2578
|
-
if (replica === undefined)
|
|
2579
|
-
return null;
|
|
2580
2509
|
const local = await openLocal();
|
|
2581
|
-
const [held] = await local.entries(replica.record.key, 'visible', [path]);
|
|
2582
|
-
|
|
2510
|
+
const [held] = replica === undefined ? [] : await local.entries(replica.record.key, 'visible', [path]);
|
|
2511
|
+
const attached = held === undefined ? attachedFiles.get(locator)?.get(path) : undefined;
|
|
2512
|
+
if (attached !== undefined)
|
|
2513
|
+
return { kind: 'path', locator, path, revision: attached.revision };
|
|
2514
|
+
if (replica === undefined || held?.binary === undefined || held.binary === null)
|
|
2583
2515
|
return null;
|
|
2584
2516
|
if (held.binary.state === 'pending')
|
|
2585
2517
|
return { kind: 'pending', localId: held.binary.localId };
|
|
@@ -2595,7 +2527,7 @@ export function composeRuntime(options) {
|
|
|
2595
2527
|
const holds = (at) => {
|
|
2596
2528
|
const kept = confirmedLayer.binaries[at];
|
|
2597
2529
|
return (kept?.state === 'accepted' &&
|
|
2598
|
-
kept.
|
|
2530
|
+
kept.sha256 === record.sha256 &&
|
|
2599
2531
|
confirmedLayer.revisions[at] === record.revision);
|
|
2600
2532
|
};
|
|
2601
2533
|
let confirmedAt = path;
|
|
@@ -2609,49 +2541,91 @@ export function composeRuntime(options) {
|
|
|
2609
2541
|
locator,
|
|
2610
2542
|
path: confirmedAt,
|
|
2611
2543
|
revision: record.revision,
|
|
2612
|
-
fileRef: record.fileRef,
|
|
2613
2544
|
};
|
|
2614
2545
|
}
|
|
2615
|
-
async function
|
|
2546
|
+
async function askedBinary(locator, path, revision) {
|
|
2616
2547
|
await ready;
|
|
2617
2548
|
const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
|
|
2618
|
-
|
|
2549
|
+
return drafted !== undefined && drafted.localId === revision
|
|
2619
2550
|
? { kind: 'pending', localId: revision }
|
|
2620
|
-
:
|
|
2551
|
+
: binaryPath(locator, path, revision);
|
|
2552
|
+
}
|
|
2553
|
+
function pendingBinary(locator, path, localId) {
|
|
2554
|
+
const facts = queuedFacts(locator, path, localId);
|
|
2555
|
+
return facts === null
|
|
2556
|
+
? absentBinary(path)
|
|
2557
|
+
: {
|
|
2558
|
+
kind: 'binary',
|
|
2559
|
+
file: localId,
|
|
2560
|
+
byteLength: facts.byteLength,
|
|
2561
|
+
mediaType: facts.mediaType,
|
|
2562
|
+
};
|
|
2563
|
+
}
|
|
2564
|
+
async function readBinaryPath(locator, path, revision) {
|
|
2565
|
+
const asked = await askedBinary(locator, path, revision);
|
|
2621
2566
|
if (asked === null)
|
|
2622
2567
|
return absentBinary(path);
|
|
2623
|
-
if (asked.kind === 'pending')
|
|
2624
|
-
|
|
2625
|
-
return facts === null
|
|
2626
|
-
? absentBinary(path)
|
|
2627
|
-
: {
|
|
2628
|
-
kind: 'binary',
|
|
2629
|
-
file: asked.localId,
|
|
2630
|
-
byteLength: facts.byteLength,
|
|
2631
|
-
mediaType: facts.mediaType,
|
|
2632
|
-
};
|
|
2633
|
-
}
|
|
2568
|
+
if (asked.kind === 'pending')
|
|
2569
|
+
return pendingBinary(locator, path, asked.localId);
|
|
2634
2570
|
return readBinary(asked, { locator, path: asked.path, revision: asked.revision });
|
|
2635
2571
|
}
|
|
2572
|
+
async function heldMediaType(locator, path) {
|
|
2573
|
+
const replica = replicas.get(locator);
|
|
2574
|
+
const [held] = replica === undefined
|
|
2575
|
+
? []
|
|
2576
|
+
: await (await openLocal()).entries(replica.record.key, 'visible', [path]);
|
|
2577
|
+
return held === undefined
|
|
2578
|
+
? attachedFiles.get(locator)?.get(path)?.mediaType
|
|
2579
|
+
: held.binary?.mediaType;
|
|
2580
|
+
}
|
|
2581
|
+
function linkOf(asked, held) {
|
|
2582
|
+
return transferBinary(asked, (authorization) => options.adapter.link(authorization, asked), (result) => binaryLinkAnswer(result, held));
|
|
2583
|
+
}
|
|
2636
2584
|
async function linkBinaryPath(locator, path, revision) {
|
|
2637
2585
|
await ready;
|
|
2638
2586
|
const asked = await binaryPath(locator, path, revision);
|
|
2639
2587
|
if (asked === null || asked.kind === 'pending')
|
|
2640
2588
|
return absentBinary(path);
|
|
2641
|
-
return
|
|
2589
|
+
return linkOf(asked, revision === undefined ? await heldMediaType(locator, path) : undefined);
|
|
2642
2590
|
}
|
|
2643
|
-
async function
|
|
2591
|
+
async function showBinaryPath(locator, path, revision) {
|
|
2592
|
+
const asked = await askedBinary(locator, path, revision);
|
|
2593
|
+
if (asked === null)
|
|
2594
|
+
return absentBinary(path);
|
|
2595
|
+
if (asked.kind === 'pending')
|
|
2596
|
+
return pendingBinary(locator, path, asked.localId);
|
|
2597
|
+
if (closed)
|
|
2598
|
+
return UNCERTAIN;
|
|
2599
|
+
const name = { locator, path: asked.path, revision: asked.revision };
|
|
2600
|
+
const kept = await keptFiles.find(name);
|
|
2601
|
+
if (kept !== null)
|
|
2602
|
+
return kept;
|
|
2603
|
+
const mediaType = await heldMediaType(locator, path);
|
|
2604
|
+
if (mediaType?.startsWith('image/') === true)
|
|
2605
|
+
return fetchBinary(asked, name);
|
|
2606
|
+
return linkOf(asked, revision === undefined ? mediaType : undefined);
|
|
2607
|
+
}
|
|
2608
|
+
async function attachBinary(locator, file) {
|
|
2644
2609
|
await ready;
|
|
2645
|
-
|
|
2646
|
-
if (
|
|
2647
|
-
return
|
|
2648
|
-
|
|
2610
|
+
await firstDiscovery;
|
|
2611
|
+
if (closed || !roomsNamed || connectionOf() === 'unreachable' || refusal !== null)
|
|
2612
|
+
return OFFLINE;
|
|
2613
|
+
const request = await attachedFile(locator, file);
|
|
2614
|
+
return transferBinary({ locator, path: file.name }, async (held) => {
|
|
2615
|
+
const result = await options.adapter.attach(held, request);
|
|
2616
|
+
if (result.kind === 'attached') {
|
|
2617
|
+
const landed = attachedFiles.get(locator) ?? new Map();
|
|
2618
|
+
landed.set(result.path, { revision: result.revision, mediaType: request.mediaType });
|
|
2619
|
+
attachedFiles.set(locator, landed);
|
|
2620
|
+
}
|
|
2621
|
+
return result;
|
|
2622
|
+
}, attachAnswer);
|
|
2649
2623
|
}
|
|
2650
2624
|
function readPart(file, part) {
|
|
2651
|
-
return
|
|
2625
|
+
return database.ask({ kind: 'file.part', key: file, part });
|
|
2652
2626
|
}
|
|
2653
2627
|
async function cleanUp() {
|
|
2654
|
-
const freed = await
|
|
2628
|
+
const freed = await database.ask({ kind: 'opened.clean' });
|
|
2655
2629
|
return freed + ((await options.shell.cleanUp?.()) ?? 0);
|
|
2656
2630
|
}
|
|
2657
2631
|
function declaredAgents() {
|
|
@@ -2783,169 +2757,222 @@ export function composeRuntime(options) {
|
|
|
2783
2757
|
options.adapter.reached(locator, written, roomVersion) &&
|
|
2784
2758
|
!options.adapter.reached(locator, roomVersion, written));
|
|
2785
2759
|
}
|
|
2786
|
-
function
|
|
2787
|
-
|
|
2788
|
-
const known = new Map(await texts(paths.filter((path) => !overrides.has(path))));
|
|
2789
|
-
for (const path of paths) {
|
|
2790
|
-
const entry = overrides.get(path);
|
|
2791
|
-
if (entry === undefined)
|
|
2792
|
-
continue;
|
|
2793
|
-
if (entry === null)
|
|
2794
|
-
known.delete(path);
|
|
2795
|
-
else
|
|
2796
|
-
known.set(path, entry.binary !== null ? null : entry.content);
|
|
2797
|
-
}
|
|
2798
|
-
return known;
|
|
2799
|
-
};
|
|
2800
|
-
}
|
|
2801
|
-
async function unreadableRoom(texts, walk, paths) {
|
|
2802
|
-
if (paths !== null) {
|
|
2803
|
-
const text = {};
|
|
2804
|
-
for (const [path, content] of await texts(paths))
|
|
2805
|
-
if (content !== null)
|
|
2806
|
-
text[path] = content;
|
|
2807
|
-
return stepped(unreadableIn(text, Object.keys(text)));
|
|
2808
|
-
}
|
|
2809
|
-
for (let after = null;;) {
|
|
2810
|
-
const page = await walk(after);
|
|
2811
|
-
const text = {};
|
|
2812
|
-
for (const { path, content } of page.entries)
|
|
2813
|
-
if (content !== null)
|
|
2814
|
-
text[path] = content;
|
|
2815
|
-
const refused = await stepped(unreadableIn(text, Object.keys(text)));
|
|
2816
|
-
if (refused !== null)
|
|
2817
|
-
return refused;
|
|
2818
|
-
if (page.next === null)
|
|
2819
|
-
return null;
|
|
2820
|
-
after = page.next;
|
|
2821
|
-
}
|
|
2822
|
-
}
|
|
2823
|
-
async function validatedRoom(local, record, view, previous, overrides = null) {
|
|
2824
|
-
const composition = compositionOf(record);
|
|
2825
|
-
const modeled = record.model.status !== 'unavailable' || record.selectionModel.status !== 'unavailable';
|
|
2826
|
-
const lookup = overrides === null
|
|
2827
|
-
? lookupOf(local, record.key, view)
|
|
2828
|
-
: overriddenLookup(lookupOf(local, record.key, view), overrides);
|
|
2829
|
-
const texts = overrides === null
|
|
2830
|
-
? textsOf(local, record.key, view)
|
|
2831
|
-
: overriddenTexts(textsOf(local, record.key, view), overrides);
|
|
2832
|
-
const walk = overrides === null
|
|
2833
|
-
? walkOf(local, record.key, view)
|
|
2834
|
-
: overriddenWalk(walkOf(local, record.key, view), overrides);
|
|
2835
|
-
let changes = null;
|
|
2836
|
-
let reshaped = false;
|
|
2837
|
-
if (previous !== null && previous.changed.length <= CLEAN_CHANGES) {
|
|
2838
|
-
const [was, now] = await Promise.all([
|
|
2839
|
-
local.entries(record.key, 'confirmed', previous.changed),
|
|
2840
|
-
overrides === null
|
|
2841
|
-
? local.entries(record.key, view, previous.changed)
|
|
2842
|
-
: Promise.resolve([]),
|
|
2843
|
-
]);
|
|
2844
|
-
const before = new Map(was.map((entry) => [entry.path, entry]));
|
|
2845
|
-
const after = overrides === null
|
|
2846
|
-
? new Map(now.map((entry) => [entry.path, entry]))
|
|
2847
|
-
: overrides;
|
|
2848
|
-
changes = previous.changed.flatMap((path) => {
|
|
2849
|
-
const one = before.get(path) ?? null;
|
|
2850
|
-
const other = after.get(path) ?? null;
|
|
2851
|
-
reshaped ||= textDiffers(one, other);
|
|
2852
|
-
const held = textOf(one);
|
|
2853
|
-
const next = textOf(other);
|
|
2854
|
-
return held === next ? [] : [{ path, was: held, now: next }];
|
|
2855
|
-
});
|
|
2856
|
-
}
|
|
2857
|
-
const named = changes === null ? null : changes.map(({ path }) => path);
|
|
2858
|
-
const textChanged = changes === null || changes.length > 0 || reshaped;
|
|
2859
|
-
const modelsChanged = previous === null || !sameComposition(compositionOf(previous.record), composition);
|
|
2860
|
-
if (modeled && (textChanged || modelsChanged)) {
|
|
2861
|
-
const clean = previous !== null &&
|
|
2862
|
-
changes !== null &&
|
|
2863
|
-
(await changedCleanly(changes, compositionOf(previous.record), composition, {
|
|
2864
|
-
lookup,
|
|
2865
|
-
texts,
|
|
2866
|
-
referrers: referrersOf(local, record.key, 'confirmed'),
|
|
2867
|
-
}));
|
|
2868
|
-
if (clean)
|
|
2869
|
-
return null;
|
|
2870
|
-
const verdict = await validatedCollection(walk, lookup, composition);
|
|
2871
|
-
if (verdict.verdict === 'accept')
|
|
2872
|
-
return null;
|
|
2873
|
-
if (verdict.rule !== 'model-unavailable')
|
|
2874
|
-
return { kind: 'refused', refusal: verdict };
|
|
2875
|
-
return unreadableRoom(texts, walk, named);
|
|
2876
|
-
}
|
|
2877
|
-
if (textChanged)
|
|
2878
|
-
return unreadableRoom(texts, walk, named);
|
|
2879
|
-
return null;
|
|
2880
|
-
}
|
|
2881
|
-
function refreshingOf(local, key, liveness) {
|
|
2760
|
+
function refreshingOf(local, key, composition, liveness, replaying = null) {
|
|
2761
|
+
const write = replaying === null ? exclusive : finishingWrite;
|
|
2882
2762
|
return {
|
|
2763
|
+
turn() {
|
|
2764
|
+
if (replaying !== null)
|
|
2765
|
+
installing(replaying);
|
|
2766
|
+
},
|
|
2883
2767
|
prior: async (paths) => new Map((await local.entries(key, 'refreshed', paths)).map((entry) => [entry.path, entry])),
|
|
2884
2768
|
async stage(page) {
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
const last =
|
|
2888
|
-
const
|
|
2769
|
+
for (let written = 0;;) {
|
|
2770
|
+
const end = batchEnd(page.entries, written);
|
|
2771
|
+
const last = end === page.entries.length;
|
|
2772
|
+
const entries = page.entries.slice(written, end);
|
|
2773
|
+
const staged = await write(async () => {
|
|
2889
2774
|
if (!liveness())
|
|
2890
2775
|
return false;
|
|
2891
|
-
await local.refresh(key, last
|
|
2776
|
+
await timed(stageBudget, () => local.refresh(key, composition, last
|
|
2892
2777
|
? { ...page, entries }
|
|
2893
|
-
: { entries, removals: [], folders: [], folderRemovals: [] });
|
|
2778
|
+
: { entries, removals: [], folders: [], folderRemovals: [] }));
|
|
2894
2779
|
return true;
|
|
2895
2780
|
});
|
|
2896
2781
|
if (!staged)
|
|
2897
2782
|
return false;
|
|
2783
|
+
if (last)
|
|
2784
|
+
return true;
|
|
2785
|
+
written = end;
|
|
2898
2786
|
}
|
|
2899
|
-
return true;
|
|
2900
2787
|
},
|
|
2901
2788
|
};
|
|
2902
2789
|
}
|
|
2903
|
-
function
|
|
2904
|
-
|
|
2790
|
+
function batchEnd(entries, from) {
|
|
2791
|
+
let end = from;
|
|
2905
2792
|
let spent = 0;
|
|
2906
|
-
|
|
2907
|
-
const size = (
|
|
2908
|
-
if (spent + size >
|
|
2909
|
-
|
|
2910
|
-
spent = 0;
|
|
2911
|
-
}
|
|
2912
|
-
batches.at(-1).push(entry);
|
|
2793
|
+
while (end < entries.length) {
|
|
2794
|
+
const size = (entries[end].content?.length ?? 0) + ROW_WEIGHT;
|
|
2795
|
+
if (spent + size > stageBudget.bytes && end > from)
|
|
2796
|
+
break;
|
|
2913
2797
|
spent += size;
|
|
2798
|
+
end += 1;
|
|
2799
|
+
}
|
|
2800
|
+
return end;
|
|
2801
|
+
}
|
|
2802
|
+
async function timed(budget, work) {
|
|
2803
|
+
const started = performance.now();
|
|
2804
|
+
const done = await work();
|
|
2805
|
+
const spent = performance.now() - started;
|
|
2806
|
+
if (spent < budget.aim / 2)
|
|
2807
|
+
budget.bytes = Math.min(budget.ceiling, Math.round(budget.bytes * 1.5));
|
|
2808
|
+
else if (spent > budget.aim * 2) {
|
|
2809
|
+
budget.bytes = Math.max(budget.floor, Math.round(budget.bytes / 1.5));
|
|
2810
|
+
}
|
|
2811
|
+
return done;
|
|
2812
|
+
}
|
|
2813
|
+
function installWrite(work) {
|
|
2814
|
+
return installTurn(installTurns.starting, work);
|
|
2815
|
+
}
|
|
2816
|
+
function finishingWrite(work) {
|
|
2817
|
+
return installTurn(installTurns.finishing, work);
|
|
2818
|
+
}
|
|
2819
|
+
function installTurn(queue, work) {
|
|
2820
|
+
return new Promise((resolve, reject) => {
|
|
2821
|
+
queue.push(() => void exclusive(work)
|
|
2822
|
+
.then(resolve, reject)
|
|
2823
|
+
.finally(() => {
|
|
2824
|
+
installTurns.busy = false;
|
|
2825
|
+
nextInstallTurn();
|
|
2826
|
+
}));
|
|
2827
|
+
nextInstallTurn();
|
|
2828
|
+
});
|
|
2829
|
+
}
|
|
2830
|
+
function nextInstallTurn() {
|
|
2831
|
+
if (installTurns.busy || installTurns.ahead > 0)
|
|
2832
|
+
return;
|
|
2833
|
+
const turn = installTurns.finishing.shift() ?? installTurns.starting.shift();
|
|
2834
|
+
if (turn === undefined)
|
|
2835
|
+
return;
|
|
2836
|
+
installTurns.busy = true;
|
|
2837
|
+
turn();
|
|
2838
|
+
}
|
|
2839
|
+
async function ahead(work) {
|
|
2840
|
+
let taken = false;
|
|
2841
|
+
const hold = {
|
|
2842
|
+
take() {
|
|
2843
|
+
if (taken)
|
|
2844
|
+
return;
|
|
2845
|
+
taken = true;
|
|
2846
|
+
installTurns.ahead += 1;
|
|
2847
|
+
},
|
|
2848
|
+
release() {
|
|
2849
|
+
if (!taken)
|
|
2850
|
+
return;
|
|
2851
|
+
taken = false;
|
|
2852
|
+
installTurns.ahead -= 1;
|
|
2853
|
+
resumeInstall();
|
|
2854
|
+
},
|
|
2855
|
+
};
|
|
2856
|
+
hold.take();
|
|
2857
|
+
try {
|
|
2858
|
+
return await work(hold);
|
|
2859
|
+
}
|
|
2860
|
+
finally {
|
|
2861
|
+
hold.release();
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2864
|
+
function resumeInstall() {
|
|
2865
|
+
if (installTurns.ahead > 0)
|
|
2866
|
+
return;
|
|
2867
|
+
void yielded().then(nextInstallTurn);
|
|
2868
|
+
}
|
|
2869
|
+
async function heldStream() {
|
|
2870
|
+
while (streamsFree === 0)
|
|
2871
|
+
await new Promise((admit) => installWaiters.push(admit));
|
|
2872
|
+
streamsFree -= 1;
|
|
2873
|
+
return once(() => {
|
|
2874
|
+
streamsFree += 1;
|
|
2875
|
+
});
|
|
2876
|
+
}
|
|
2877
|
+
async function unqueued() {
|
|
2878
|
+
while (queuedText >= QUEUED_TEXT) {
|
|
2879
|
+
await new Promise((admit) => installWaiters.push(admit));
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
function once(release) {
|
|
2883
|
+
let released = false;
|
|
2884
|
+
return () => {
|
|
2885
|
+
if (released)
|
|
2886
|
+
return;
|
|
2887
|
+
released = true;
|
|
2888
|
+
release();
|
|
2889
|
+
for (const admit of installWaiters.splice(0))
|
|
2890
|
+
admit();
|
|
2891
|
+
};
|
|
2892
|
+
}
|
|
2893
|
+
function installing(locator) {
|
|
2894
|
+
installs.add(locator);
|
|
2895
|
+
queueReadiness();
|
|
2896
|
+
}
|
|
2897
|
+
function untransferred(locator) {
|
|
2898
|
+
transfers.delete(locator);
|
|
2899
|
+
arrivals.delete(locator);
|
|
2900
|
+
streaming.delete(locator);
|
|
2901
|
+
underived.delete(locator);
|
|
2902
|
+
unindexed.delete(locator);
|
|
2903
|
+
}
|
|
2904
|
+
function setBack(locator) {
|
|
2905
|
+
const now = bytesOf(locator, true);
|
|
2906
|
+
const earlier = startedOver.get(locator);
|
|
2907
|
+
startedOver.set(locator, {
|
|
2908
|
+
done: Math.max(now.done, earlier?.done ?? 0),
|
|
2909
|
+
total: Math.max(now.total, earlier?.total ?? 0),
|
|
2910
|
+
});
|
|
2911
|
+
}
|
|
2912
|
+
function owedIndexes(locator, bytes, text) {
|
|
2913
|
+
if (bytes <= 0 || text <= 0)
|
|
2914
|
+
return;
|
|
2915
|
+
unindexed.set(locator, [...(unindexed.get(locator) ?? []), { bytes, text }]);
|
|
2916
|
+
}
|
|
2917
|
+
function built(locator, { done, text }) {
|
|
2918
|
+
const owed = unindexed.get(locator) ?? [];
|
|
2919
|
+
let left = text;
|
|
2920
|
+
while (!done && left > 0 && owed.length > 0) {
|
|
2921
|
+
const [first] = owed;
|
|
2922
|
+
if (left >= first.text) {
|
|
2923
|
+
left -= first.text;
|
|
2924
|
+
owed.shift();
|
|
2925
|
+
continue;
|
|
2926
|
+
}
|
|
2927
|
+
owed[0] = {
|
|
2928
|
+
bytes: (first.bytes * (first.text - left)) / first.text,
|
|
2929
|
+
text: first.text - left,
|
|
2930
|
+
};
|
|
2931
|
+
left = 0;
|
|
2914
2932
|
}
|
|
2915
|
-
|
|
2933
|
+
if (done || owed.length === 0)
|
|
2934
|
+
unindexed.delete(locator);
|
|
2935
|
+
queueReadiness();
|
|
2936
|
+
}
|
|
2937
|
+
function installed(locator) {
|
|
2938
|
+
if (installs.delete(locator))
|
|
2939
|
+
queueReadiness();
|
|
2916
2940
|
}
|
|
2917
|
-
async function pullRefresh(authorized, held, liveness) {
|
|
2941
|
+
async function pullRefresh(authorized, held, liveness, hold, refresh) {
|
|
2918
2942
|
if (!liveness())
|
|
2919
2943
|
return unconfirmed;
|
|
2920
|
-
const turn = generation;
|
|
2921
2944
|
const { record } = held;
|
|
2922
|
-
const
|
|
2945
|
+
const from = standingOf(record.locator);
|
|
2923
2946
|
const local = await openLocal();
|
|
2924
2947
|
try {
|
|
2925
2948
|
await exclusive(() => local.unrefresh(record.key));
|
|
2926
|
-
const result = await advancement(
|
|
2949
|
+
const result = await advancement(async (authorization, request) => {
|
|
2950
|
+
hold.release();
|
|
2951
|
+
try {
|
|
2952
|
+
return await refresh(authorization, request);
|
|
2953
|
+
}
|
|
2954
|
+
finally {
|
|
2955
|
+
hold.take();
|
|
2956
|
+
}
|
|
2957
|
+
}, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.key, composed(record), liveness));
|
|
2927
2958
|
if (result.kind !== 'advanced') {
|
|
2928
2959
|
journal.record(`transfer refresh ${record.locator} ${result.kind}`);
|
|
2929
2960
|
return result;
|
|
2930
2961
|
}
|
|
2931
|
-
const refused = await validatedRoom(local, result.record, 'refreshed', {
|
|
2932
|
-
record,
|
|
2933
|
-
changed: [...result.changed],
|
|
2934
|
-
});
|
|
2935
|
-
if (refused !== null) {
|
|
2936
|
-
journal.record(`transfer refresh ${record.locator} refused`);
|
|
2937
|
-
return refused;
|
|
2938
|
-
}
|
|
2939
2962
|
journal.record(`transfer refresh ${record.locator} ${result.record.roomVersion}`);
|
|
2940
|
-
return {
|
|
2963
|
+
return {
|
|
2964
|
+
kind: 'advanced',
|
|
2965
|
+
record: result.record,
|
|
2966
|
+
from,
|
|
2967
|
+
remodelled: result.record.model !== record.model,
|
|
2968
|
+
};
|
|
2941
2969
|
}
|
|
2942
2970
|
catch (error) {
|
|
2971
|
+
if (refusedForSpace(error))
|
|
2972
|
+
return unconfirmed;
|
|
2943
2973
|
journal.record(`transfer refresh ${record.locator} transport-uncertainty`);
|
|
2944
2974
|
throw error;
|
|
2945
2975
|
}
|
|
2946
|
-
finally {
|
|
2947
|
-
reads.finished(record.locator, turn);
|
|
2948
|
-
}
|
|
2949
2976
|
}
|
|
2950
2977
|
const heldContents = new Map();
|
|
2951
2978
|
function heldContentOf(local, key) {
|
|
@@ -2963,63 +2990,115 @@ export function composeRuntime(options) {
|
|
|
2963
2990
|
heldContents.set(key, made);
|
|
2964
2991
|
return made;
|
|
2965
2992
|
}
|
|
2993
|
+
async function abandoned(local, locator) {
|
|
2994
|
+
await local.abandon(locator);
|
|
2995
|
+
setBack(locator);
|
|
2996
|
+
untransferred(locator);
|
|
2997
|
+
liveQueries.recover();
|
|
2998
|
+
sweep();
|
|
2999
|
+
}
|
|
2966
3000
|
function staging(local, liveness) {
|
|
3001
|
+
const derived = async (locator, key, composition) => {
|
|
3002
|
+
underived.delete(locator);
|
|
3003
|
+
for (;;) {
|
|
3004
|
+
if (!liveness())
|
|
3005
|
+
return false;
|
|
3006
|
+
const done = await finishingWrite(async () => liveness()
|
|
3007
|
+
? timed(deriveBudget, () => local.rederive(key, composition, deriveBudget.bytes, DERIVE_LONGEST_MS, DERIVE_FLOOR))
|
|
3008
|
+
: null);
|
|
3009
|
+
if (done === null)
|
|
3010
|
+
return false;
|
|
3011
|
+
built(locator, done);
|
|
3012
|
+
if (done.done)
|
|
3013
|
+
return true;
|
|
3014
|
+
}
|
|
3015
|
+
};
|
|
2967
3016
|
return {
|
|
2968
3017
|
held: (locator) => transfers.get(locator) ?? null,
|
|
3018
|
+
async turn(locator, step) {
|
|
3019
|
+
installing(locator);
|
|
3020
|
+
return step === 'downloading' ? heldStream() : () => { };
|
|
3021
|
+
},
|
|
3022
|
+
unqueued,
|
|
3023
|
+
queued(text) {
|
|
3024
|
+
queuedText += text;
|
|
3025
|
+
return once(() => {
|
|
3026
|
+
queuedText -= text;
|
|
3027
|
+
});
|
|
3028
|
+
},
|
|
3029
|
+
arriving(locator) {
|
|
3030
|
+
let ended = false;
|
|
3031
|
+
return {
|
|
3032
|
+
at(bytes) {
|
|
3033
|
+
if (ended)
|
|
3034
|
+
return;
|
|
3035
|
+
arrivals.set(locator, bytes);
|
|
3036
|
+
streaming.add(locator);
|
|
3037
|
+
const now = performance.now();
|
|
3038
|
+
if (now - arrivalReported < ARRIVAL_REPORT_MS)
|
|
3039
|
+
return;
|
|
3040
|
+
arrivalReported = now;
|
|
3041
|
+
queueReadiness();
|
|
3042
|
+
},
|
|
3043
|
+
end() {
|
|
3044
|
+
if (ended)
|
|
3045
|
+
return;
|
|
3046
|
+
ended = true;
|
|
3047
|
+
if (streaming.delete(locator))
|
|
3048
|
+
queueReadiness();
|
|
3049
|
+
},
|
|
3050
|
+
};
|
|
3051
|
+
},
|
|
3052
|
+
streamed: options.adapter.roomStream !== undefined,
|
|
2969
3053
|
async stage(record, page, composition) {
|
|
2970
|
-
const
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
if (!liveness())
|
|
2977
|
-
return null;
|
|
2978
|
-
if (index === 0 && !(await admitted(local, record.locator, bytes)))
|
|
2979
|
-
return null;
|
|
2980
|
-
try {
|
|
2981
|
-
return await local.stage(record, composition, { entries, folders: last ? page.folders : [] }, key, last);
|
|
2982
|
-
}
|
|
2983
|
-
catch (error) {
|
|
2984
|
-
if (!exceeded(error))
|
|
2985
|
-
throw error;
|
|
2986
|
-
await exceededOn(record.locator, bytes);
|
|
2987
|
-
return null;
|
|
2988
|
-
}
|
|
2989
|
-
});
|
|
2990
|
-
if (staged === null)
|
|
3054
|
+
const { locator } = record;
|
|
3055
|
+
installing(locator);
|
|
3056
|
+
if (!liveness())
|
|
3057
|
+
return null;
|
|
3058
|
+
const key = await installWrite(async () => {
|
|
3059
|
+
if (!liveness())
|
|
2991
3060
|
return null;
|
|
2992
|
-
|
|
2993
|
-
}
|
|
2994
|
-
|
|
3061
|
+
return local.stage(record, composition, page);
|
|
3062
|
+
});
|
|
3063
|
+
if (key === null || !liveness())
|
|
3064
|
+
return null;
|
|
3065
|
+
const arrived = !transfers.has(locator);
|
|
3066
|
+
const prior = transfers.get(locator)?.done.bytes ?? 0;
|
|
3067
|
+
const held = { ...record, key };
|
|
3068
|
+
transfers.set(locator, held);
|
|
3069
|
+
if (arrived)
|
|
3070
|
+
liveQueries.recover();
|
|
3071
|
+
installing(locator);
|
|
2995
3072
|
liveQueries.recover();
|
|
2996
|
-
|
|
3073
|
+
const text = page.entries.reduce((sum, entry) => sum + (entry.content?.length ?? 0), 0);
|
|
3074
|
+
owedIndexes(locator, record.done.bytes - prior, text);
|
|
3075
|
+
const owed = text + (underived.get(locator) ?? 0);
|
|
3076
|
+
underived.set(locator, owed);
|
|
3077
|
+
if (owed < deriveBudget.bytes)
|
|
3078
|
+
return held;
|
|
3079
|
+
return (await derived(locator, key, composition)) ? held : null;
|
|
2997
3080
|
},
|
|
2998
|
-
|
|
3081
|
+
derive: derived,
|
|
3082
|
+
async refreshing(locator, key, composition) {
|
|
2999
3083
|
await exclusive(() => local.unrefresh(key));
|
|
3000
|
-
return refreshingOf(local, key, liveness);
|
|
3084
|
+
return refreshingOf(local, key, composition, liveness, locator);
|
|
3001
3085
|
},
|
|
3002
3086
|
abandon: (locator) => exclusive(async () => {
|
|
3003
|
-
if (
|
|
3004
|
-
|
|
3005
|
-
await local.abandon(locator);
|
|
3006
|
-
transfers.delete(locator);
|
|
3007
|
-
liveQueries.recover();
|
|
3008
|
-
sweep();
|
|
3087
|
+
if (liveness())
|
|
3088
|
+
await abandoned(local, locator);
|
|
3009
3089
|
}),
|
|
3010
|
-
setAside:
|
|
3090
|
+
setAside: async (locator) => {
|
|
3011
3091
|
if (!liveness())
|
|
3012
3092
|
return;
|
|
3013
|
-
|
|
3014
|
-
|
|
3093
|
+
setBack(locator);
|
|
3094
|
+
untransferred(locator);
|
|
3015
3095
|
liveQueries.recover();
|
|
3016
|
-
}
|
|
3096
|
+
},
|
|
3017
3097
|
release: (key) => exclusive(async () => {
|
|
3018
3098
|
await local.release(key);
|
|
3019
3099
|
sweep();
|
|
3020
3100
|
}),
|
|
3021
3101
|
content: (key) => heldContentOf(local, key),
|
|
3022
|
-
validate: (record, view) => validatedRoom(local, record, view, null),
|
|
3023
3102
|
};
|
|
3024
3103
|
}
|
|
3025
3104
|
function reusable(room, local) {
|
|
@@ -3031,30 +3110,28 @@ export function composeRuntime(options) {
|
|
|
3031
3110
|
async function pullBootstrap(authorized, room, local, liveness) {
|
|
3032
3111
|
if (!liveness())
|
|
3033
3112
|
return unconfirmed;
|
|
3034
|
-
const turn = generation;
|
|
3035
3113
|
if (replicas.has(room.locator)) {
|
|
3036
3114
|
catching.add(room.locator);
|
|
3037
3115
|
liveQueries.recover();
|
|
3038
3116
|
}
|
|
3039
3117
|
try {
|
|
3040
|
-
const result = await bootstrapRoom(reads, authorized, room, {
|
|
3041
|
-
collection: modelOf(room.locator),
|
|
3042
|
-
selection: modelOf(room.locator, 'typed-selection'),
|
|
3043
|
-
}, (models, held) => (liveness() ? remodel(room.locator, models, held) : held), liveness, staging(local, liveness), reusable(room, local));
|
|
3118
|
+
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));
|
|
3044
3119
|
const detail = result.kind === 'staged' ? result.record.roomVersion : result.kind;
|
|
3045
3120
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
3046
|
-
if (result.kind !== 'staged')
|
|
3121
|
+
if (result.kind !== 'staged') {
|
|
3047
3122
|
caughtUpTo(room.locator);
|
|
3123
|
+
installed(room.locator);
|
|
3124
|
+
}
|
|
3048
3125
|
return result;
|
|
3049
3126
|
}
|
|
3050
3127
|
catch (error) {
|
|
3051
|
-
journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
|
|
3052
3128
|
caughtUpTo(room.locator);
|
|
3129
|
+
installed(room.locator);
|
|
3130
|
+
if (refusedForSpace(error))
|
|
3131
|
+
return unconfirmed;
|
|
3132
|
+
journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
|
|
3053
3133
|
throw error;
|
|
3054
3134
|
}
|
|
3055
|
-
finally {
|
|
3056
|
-
reads.finished(room.locator, turn);
|
|
3057
|
-
}
|
|
3058
3135
|
}
|
|
3059
3136
|
function caughtUpTo(locator) {
|
|
3060
3137
|
if (catching.delete(locator))
|
|
@@ -3062,72 +3139,82 @@ export function composeRuntime(options) {
|
|
|
3062
3139
|
}
|
|
3063
3140
|
function behind(room) {
|
|
3064
3141
|
const held = replicas.get(room.locator)?.record.roomVersion;
|
|
3065
|
-
return
|
|
3066
|
-
[announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
|
|
3142
|
+
return held !== undefined && !options.adapter.reached(room.locator, held, room.roomVersion);
|
|
3067
3143
|
}
|
|
3068
|
-
|
|
3069
|
-
const
|
|
3070
|
-
|
|
3071
|
-
if (withheld !== null || reads.carried(room.locator) === carried || !behind(room)) {
|
|
3072
|
-
return withheld;
|
|
3073
|
-
}
|
|
3074
|
-
return advanceOnce(room, local, authorized, live);
|
|
3144
|
+
function standingOf(locator) {
|
|
3145
|
+
const held = replicas.get(locator);
|
|
3146
|
+
return { key: held?.record.key ?? null, confirmed: held?.confirmed ?? 0 };
|
|
3075
3147
|
}
|
|
3076
|
-
|
|
3077
|
-
|
|
3148
|
+
function stands(locator, from) {
|
|
3149
|
+
const now = standingOf(locator);
|
|
3150
|
+
return now.key === from.key && now.confirmed === from.confirmed;
|
|
3151
|
+
}
|
|
3152
|
+
async function advance(room, local, authorized, running) {
|
|
3153
|
+
const live = () => running() && !full;
|
|
3154
|
+
if (!live())
|
|
3078
3155
|
return unconfirmed;
|
|
3079
3156
|
const held = replicas.get(room.locator);
|
|
3080
|
-
if (held !== undefined) {
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
emitRooms();
|
|
3092
|
-
}
|
|
3093
|
-
return null;
|
|
3094
|
-
}
|
|
3095
|
-
if (refreshed.kind === 'advanced') {
|
|
3096
|
-
const landing = { kind: 'refreshed', base: refreshed.base };
|
|
3097
|
-
if (!(await publish(local, room, refreshed.record, landing, live))) {
|
|
3098
|
-
const now = replicas.get(room.locator)?.record.roomVersion;
|
|
3099
|
-
const reached = now !== undefined &&
|
|
3100
|
-
options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
|
|
3101
|
-
return reached ? null : advance(room, local, authorized, live);
|
|
3102
|
-
}
|
|
3103
|
-
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
3104
|
-
return null;
|
|
3105
|
-
}
|
|
3106
|
-
if (refreshed.kind !== 'beyond-horizon')
|
|
3157
|
+
if (held !== undefined && options.adapter.roomStream !== undefined) {
|
|
3158
|
+
stale.delete(room.locator);
|
|
3159
|
+
name(room.locator);
|
|
3160
|
+
return null;
|
|
3161
|
+
}
|
|
3162
|
+
const refresh = reads.refresh;
|
|
3163
|
+
if (held !== undefined && refresh !== undefined) {
|
|
3164
|
+
const refreshed = await ahead((hold) => refreshedRoom(room, local, authorized, held, live, hold, refresh));
|
|
3165
|
+
if (refreshed === 'behind')
|
|
3166
|
+
return advance(room, local, authorized, live);
|
|
3167
|
+
if (refreshed !== 'beyond-horizon')
|
|
3107
3168
|
return refreshed;
|
|
3108
|
-
if (
|
|
3109
|
-
owed.add(room.locator);
|
|
3110
|
-
liveQueries.recover();
|
|
3111
|
-
return unconfirmed;
|
|
3112
|
-
}
|
|
3113
|
-
if (reloadsDue.get(room.locator) === generation)
|
|
3169
|
+
if (reloadsDue.has(room.locator))
|
|
3114
3170
|
return unconfirmed;
|
|
3115
3171
|
}
|
|
3172
|
+
const from = standingOf(room.locator);
|
|
3116
3173
|
const bootstrapped = await pullBootstrap(authorized, room, local, live);
|
|
3174
|
+
if (bootstrapped.kind !== 'staged')
|
|
3175
|
+
return live() ? bootstrapped : unconfirmed;
|
|
3176
|
+
const landing = { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from };
|
|
3177
|
+
if (await publish(local, room, bootstrapped.record, landing, live)) {
|
|
3178
|
+
return live() ? null : unconfirmed;
|
|
3179
|
+
}
|
|
3180
|
+
return reachedBy(room.locator, bootstrapped.record.roomVersion)
|
|
3181
|
+
? null
|
|
3182
|
+
: advance(room, local, authorized, live);
|
|
3183
|
+
}
|
|
3184
|
+
async function refreshedRoom(room, local, authorized, held, live, hold, refresh) {
|
|
3185
|
+
const refreshed = await pullRefresh(authorized, held, live, hold, refresh);
|
|
3117
3186
|
if (!live())
|
|
3118
3187
|
return unconfirmed;
|
|
3119
|
-
if (
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3188
|
+
if (refreshed.kind === 'still-current') {
|
|
3189
|
+
stale.delete(room.locator);
|
|
3190
|
+
if (unreadable.delete(room.locator)) {
|
|
3191
|
+
scopedRefusals.delete(room.locator);
|
|
3192
|
+
list(room, true);
|
|
3193
|
+
emitRooms();
|
|
3194
|
+
}
|
|
3195
|
+
return null;
|
|
3196
|
+
}
|
|
3197
|
+
if (refreshed.kind === 'advanced') {
|
|
3198
|
+
const landing = {
|
|
3199
|
+
kind: 'refreshed',
|
|
3200
|
+
from: refreshed.from,
|
|
3201
|
+
remodelled: refreshed.remodelled,
|
|
3202
|
+
};
|
|
3203
|
+
if (!(await publish(local, room, refreshed.record, landing, live))) {
|
|
3204
|
+
const now = replicas.get(room.locator)?.record.roomVersion;
|
|
3205
|
+
const reached = now !== undefined &&
|
|
3206
|
+
options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
|
|
3207
|
+
return reached ? null : 'behind';
|
|
3208
|
+
}
|
|
3209
|
+
await retainModel(local, room, modelOf(room.locator));
|
|
3210
|
+
return null;
|
|
3211
|
+
}
|
|
3212
|
+
return refreshed.kind === 'beyond-horizon' ? 'beyond-horizon' : refreshed;
|
|
3123
3213
|
}
|
|
3124
3214
|
async function withhold(withheld, room, local, progress) {
|
|
3125
3215
|
if (withheld.kind === 'unconfirmed')
|
|
3126
3216
|
return;
|
|
3127
|
-
|
|
3128
|
-
stale.delete(room.locator);
|
|
3129
|
-
else
|
|
3130
|
-
stale.add(room.locator);
|
|
3217
|
+
stale.add(room.locator);
|
|
3131
3218
|
if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
|
|
3132
3219
|
const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
|
|
3133
3220
|
repairable.set(room.locator, reset);
|
|
@@ -3149,30 +3236,10 @@ export function composeRuntime(options) {
|
|
|
3149
3236
|
list(room, false);
|
|
3150
3237
|
emitRooms();
|
|
3151
3238
|
}
|
|
3152
|
-
else if (withheld.kind === 'refused') {
|
|
3153
|
-
refusedSource(room, withheld);
|
|
3154
|
-
}
|
|
3155
3239
|
else if (withheld.kind === 'revoked-authorization') {
|
|
3156
3240
|
await refuse('revoked-authorization');
|
|
3157
3241
|
}
|
|
3158
3242
|
}
|
|
3159
|
-
function refusedSource(room, { refusal }) {
|
|
3160
|
-
sourceRefusals.set(room.locator, {
|
|
3161
|
-
code: refusal.rule,
|
|
3162
|
-
retryable: false,
|
|
3163
|
-
fields: {
|
|
3164
|
-
file: refusal.file,
|
|
3165
|
-
...(refusal.location === undefined ? {} : { location: refusal.location }),
|
|
3166
|
-
...(refusal.referrers === undefined ? {} : { referrers: refusal.referrers }),
|
|
3167
|
-
},
|
|
3168
|
-
});
|
|
3169
|
-
declarationRefusals.delete(room.locator);
|
|
3170
|
-
unreadable.add(room.locator);
|
|
3171
|
-
list(room, false);
|
|
3172
|
-
emitRooms();
|
|
3173
|
-
moveTo({ ...status, ...attention() });
|
|
3174
|
-
journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
|
|
3175
|
-
}
|
|
3176
3243
|
function leaving(batch, result) {
|
|
3177
3244
|
if (result.kind === 'expired')
|
|
3178
3245
|
return { batch, returned: returnedBy('expired') };
|
|
@@ -3194,8 +3261,8 @@ export function composeRuntime(options) {
|
|
|
3194
3261
|
function settling(acceptance) {
|
|
3195
3262
|
return acceptance !== null && 'apply' in acceptance;
|
|
3196
3263
|
}
|
|
3197
|
-
function
|
|
3198
|
-
return settling(acceptance) ?
|
|
3264
|
+
function failureOf(acceptance) {
|
|
3265
|
+
return settling(acceptance) ? null : acceptance;
|
|
3199
3266
|
}
|
|
3200
3267
|
async function acceptance(room, local, submitted, accepted) {
|
|
3201
3268
|
if (!continuing())
|
|
@@ -3218,34 +3285,6 @@ export function composeRuntime(options) {
|
|
|
3218
3285
|
? accepted.roomVersion
|
|
3219
3286
|
: held,
|
|
3220
3287
|
};
|
|
3221
|
-
const overrides = new Map([
|
|
3222
|
-
...back.set.map((entry) => [entry.path, entry]),
|
|
3223
|
-
...back.removed.map((path) => [path, null]),
|
|
3224
|
-
]);
|
|
3225
|
-
const refused = await validatedRoom(local, record, 'confirmed', { record: current.record, changed: [...overrides.keys()] }, overrides);
|
|
3226
|
-
if (refused !== null) {
|
|
3227
|
-
const key = pendingKey(room.locator, submitted.id);
|
|
3228
|
-
return {
|
|
3229
|
-
locator: room.locator,
|
|
3230
|
-
left: [],
|
|
3231
|
-
commit: {
|
|
3232
|
-
rooms: [],
|
|
3233
|
-
writes: [
|
|
3234
|
-
{
|
|
3235
|
-
locator: room.locator,
|
|
3236
|
-
accepted: [{ id: submitted.id, roomVersion: accepted.roomVersion }],
|
|
3237
|
-
},
|
|
3238
|
-
],
|
|
3239
|
-
},
|
|
3240
|
-
refused,
|
|
3241
|
-
apply() {
|
|
3242
|
-
const state = pendings.get(key);
|
|
3243
|
-
if (state !== undefined && !fences.has(room.locator))
|
|
3244
|
-
pendings.set(key, { ...state, accepted: accepted.roomVersion });
|
|
3245
|
-
return changeSet(room.locator, { queue: true });
|
|
3246
|
-
},
|
|
3247
|
-
};
|
|
3248
|
-
}
|
|
3249
3288
|
changes = back;
|
|
3250
3289
|
}
|
|
3251
3290
|
const settled = await settlement(local, room.locator, current, record, current.queue.filter((batch) => batch.id !== submitted.id), { room: true, ...(changes === undefined ? {} : { changes }) });
|
|
@@ -3255,8 +3294,6 @@ export function composeRuntime(options) {
|
|
|
3255
3294
|
const set = settled.apply(answer);
|
|
3256
3295
|
pendings.delete(pendingKey(room.locator, submitted.id));
|
|
3257
3296
|
unreadable.delete(room.locator);
|
|
3258
|
-
sourceRefusals.delete(room.locator);
|
|
3259
|
-
declarationRefusals.delete(room.locator);
|
|
3260
3297
|
list(room, true);
|
|
3261
3298
|
return set;
|
|
3262
3299
|
},
|
|
@@ -3274,13 +3311,9 @@ export function composeRuntime(options) {
|
|
|
3274
3311
|
const failed = await exclusive(async () => {
|
|
3275
3312
|
const adopting = await acceptance(room, local, submitted, accepted);
|
|
3276
3313
|
await adoptedTogether(local, [adopting]);
|
|
3277
|
-
return
|
|
3314
|
+
return failureOf(adopting);
|
|
3278
3315
|
});
|
|
3279
|
-
|
|
3280
|
-
return true;
|
|
3281
|
-
if (failed.kind === 'refused' && continuing())
|
|
3282
|
-
refusedSource(room, failed);
|
|
3283
|
-
return false;
|
|
3316
|
+
return failed === null;
|
|
3284
3317
|
}
|
|
3285
3318
|
async function adoptedMove(room, partner, local, submitted, accepted) {
|
|
3286
3319
|
const other = discovered.get(partner);
|
|
@@ -3294,18 +3327,10 @@ export function composeRuntime(options) {
|
|
|
3294
3327
|
...accepted.destination,
|
|
3295
3328
|
});
|
|
3296
3329
|
await adoptedTogether(local, [left, arrived]);
|
|
3297
|
-
|
|
3298
|
-
if (other !== undefined && refusedArrival?.kind === 'refused' && continuing()) {
|
|
3299
|
-
refusedSource(other, refusedArrival);
|
|
3300
|
-
}
|
|
3301
|
-
return refusalOf(left);
|
|
3330
|
+
return failureOf(left);
|
|
3302
3331
|
});
|
|
3303
3332
|
submitLater(partner);
|
|
3304
|
-
|
|
3305
|
-
return true;
|
|
3306
|
-
if (failed.kind === 'refused' && continuing())
|
|
3307
|
-
refusedSource(room, failed);
|
|
3308
|
-
return false;
|
|
3333
|
+
return failed === null;
|
|
3309
3334
|
}
|
|
3310
3335
|
function queueIn(locator) {
|
|
3311
3336
|
return replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
@@ -3428,8 +3453,6 @@ export function composeRuntime(options) {
|
|
|
3428
3453
|
if (replica === undefined || head === undefined)
|
|
3429
3454
|
return 'done';
|
|
3430
3455
|
const state = pendings.get(pendingKey(room.locator, head.id));
|
|
3431
|
-
if (state?.accepted !== undefined)
|
|
3432
|
-
return 'done';
|
|
3433
3456
|
const partner = partnerOf(head, room.locator);
|
|
3434
3457
|
if (partner !== null && !leavingIn(head, room.locator)) {
|
|
3435
3458
|
submitLater(partner);
|
|
@@ -3543,12 +3566,12 @@ export function composeRuntime(options) {
|
|
|
3543
3566
|
}
|
|
3544
3567
|
if (result.kind === 'expired') {
|
|
3545
3568
|
await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, partner !== null && lost));
|
|
3546
|
-
const there = other === undefined ? null : await
|
|
3569
|
+
const there = other === undefined ? null : await advanced(other, local, authorized);
|
|
3547
3570
|
if (other !== undefined && there !== null) {
|
|
3548
3571
|
await withhold(there, other, local, progress);
|
|
3549
3572
|
return 'done';
|
|
3550
3573
|
}
|
|
3551
|
-
const withheld = await
|
|
3574
|
+
const withheld = await advanced(room, local, authorized);
|
|
3552
3575
|
if (withheld !== null) {
|
|
3553
3576
|
await withhold(withheld, room, local, progress);
|
|
3554
3577
|
return 'done';
|
|
@@ -3570,8 +3593,8 @@ export function composeRuntime(options) {
|
|
|
3570
3593
|
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
3571
3594
|
const left = leaving(head, result);
|
|
3572
3595
|
const refreshed = async () => ({
|
|
3573
|
-
there: other === undefined ? null : await
|
|
3574
|
-
here: await
|
|
3596
|
+
there: other === undefined ? null : await advanced(other, local, authorized),
|
|
3597
|
+
here: await advanced(room, local, authorized),
|
|
3575
3598
|
});
|
|
3576
3599
|
const judged = result.kind === 'conflicted' && other !== undefined ? await refreshed() : null;
|
|
3577
3600
|
if (judged !== null && (judged.there !== null || judged.here !== null)) {
|
|
@@ -3642,6 +3665,7 @@ export function composeRuntime(options) {
|
|
|
3642
3665
|
locator: room.locator,
|
|
3643
3666
|
key: before.record.key,
|
|
3644
3667
|
roots: before.record.typedSelection?.roots ?? [],
|
|
3668
|
+
composed: composedOf(composed(before.record)),
|
|
3645
3669
|
overlay: { set: [], cleared: [...before.overlaid] },
|
|
3646
3670
|
},
|
|
3647
3671
|
]),
|
|
@@ -3674,39 +3698,37 @@ export function composeRuntime(options) {
|
|
|
3674
3698
|
if (signal.aborted)
|
|
3675
3699
|
return;
|
|
3676
3700
|
const progress = { complete: true, reachable: true };
|
|
3677
|
-
await roomTask(room.locator,
|
|
3678
|
-
await Promise.race([
|
|
3679
|
-
reload(room, local, progress, () => live() && !signal.aborted),
|
|
3680
|
-
abandonedOn(signal),
|
|
3681
|
-
]);
|
|
3682
|
-
});
|
|
3701
|
+
await roomTask(room.locator, (live) => reload(room, local, progress, live));
|
|
3683
3702
|
if (signal.aborted)
|
|
3684
3703
|
return;
|
|
3685
3704
|
await rest(progress);
|
|
3686
3705
|
}
|
|
3687
3706
|
}
|
|
3688
3707
|
function awaitingAnswer(locator) {
|
|
3708
|
+
answering.get(locator)?.release();
|
|
3689
3709
|
let release = () => { };
|
|
3690
|
-
const
|
|
3710
|
+
const answered = new Promise((resolve) => {
|
|
3691
3711
|
release = resolve;
|
|
3692
3712
|
});
|
|
3693
|
-
|
|
3713
|
+
const awaited = { answered, release };
|
|
3714
|
+
answering.set(locator, awaited);
|
|
3694
3715
|
return () => {
|
|
3695
|
-
if (answering.get(locator)
|
|
3716
|
+
if (answering.get(locator) === awaited)
|
|
3696
3717
|
answering.delete(locator);
|
|
3697
3718
|
release();
|
|
3698
3719
|
};
|
|
3699
3720
|
}
|
|
3700
|
-
|
|
3701
|
-
const
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3721
|
+
function declaredAsHeld(room) {
|
|
3722
|
+
const held = replicas.get(room.locator)?.record;
|
|
3723
|
+
return (held === undefined ||
|
|
3724
|
+
(JSON.stringify(held.operations) === JSON.stringify(room.operations) &&
|
|
3725
|
+
JSON.stringify(held.replica) === JSON.stringify(room.replica) &&
|
|
3726
|
+
JSON.stringify(held.typedSelection) === JSON.stringify(room.typedSelection ?? null)));
|
|
3727
|
+
}
|
|
3728
|
+
async function retainModel(local, room, next) {
|
|
3729
|
+
const key = room.locator;
|
|
3730
|
+
const previous = modelOf(room.locator);
|
|
3731
|
+
if (JSON.stringify(previous) === JSON.stringify(next) && declaredAsHeld(room))
|
|
3710
3732
|
return;
|
|
3711
3733
|
if (next.status === 'unavailable') {
|
|
3712
3734
|
invalidModels.add(key);
|
|
@@ -3718,26 +3740,13 @@ export function composeRuntime(options) {
|
|
|
3718
3740
|
const held = replicas.get(room.locator);
|
|
3719
3741
|
let changed = null;
|
|
3720
3742
|
if (held !== undefined) {
|
|
3721
|
-
const
|
|
3743
|
+
const record = {
|
|
3722
3744
|
...held.record,
|
|
3723
3745
|
operations: room.operations,
|
|
3724
3746
|
replica: room.replica,
|
|
3725
3747
|
typedSelection: room.typedSelection ?? null,
|
|
3726
|
-
|
|
3748
|
+
model: next,
|
|
3727
3749
|
};
|
|
3728
|
-
let read = 'staged';
|
|
3729
|
-
if (!sameMembership(declared, held.record)) {
|
|
3730
|
-
const verdict = await validatedCollection(walkOf(local, held.record.key, 'confirmed'), lookupOf(local, held.record.key, 'confirmed'), compositionOf(declared));
|
|
3731
|
-
read =
|
|
3732
|
-
verdict.verdict !== 'refuse'
|
|
3733
|
-
? 'staged'
|
|
3734
|
-
: verdict.rule === 'model-unavailable'
|
|
3735
|
-
? 'deferred'
|
|
3736
|
-
: { kind: 'refused', refusal: verdict };
|
|
3737
|
-
}
|
|
3738
|
-
const record = read === 'staged'
|
|
3739
|
-
? declared
|
|
3740
|
-
: { ...declared, typedSelection: held.record.typedSelection };
|
|
3741
3750
|
const confirmedLayer = await sliceAt(record, 'confirmed', held.queue, held.overlaid);
|
|
3742
3751
|
const replayedLayer = await replayed(record, confirmedLayer, held.queue);
|
|
3743
3752
|
const diff = await overlayDiff(record, held, replayedLayer.visible, replayedLayer.touched);
|
|
@@ -3748,41 +3757,33 @@ export function composeRuntime(options) {
|
|
|
3748
3757
|
key: record.key,
|
|
3749
3758
|
record: storedRecord(record),
|
|
3750
3759
|
roots: record.typedSelection?.roots ?? [],
|
|
3751
|
-
|
|
3760
|
+
composed: composedOf(composed(record)),
|
|
3752
3761
|
overlay: diff.overlay,
|
|
3753
3762
|
},
|
|
3754
3763
|
],
|
|
3755
3764
|
});
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
replicas.set(room.locator, { ...held, record, overlaid: diff.touched, count });
|
|
3765
|
+
const { count, indexed } = committedRoom(room.locator, answer);
|
|
3766
|
+
replicas.set(room.locator, { ...held, record, overlaid: diff.touched, count, indexed });
|
|
3759
3767
|
changed = changeSet(room.locator, {
|
|
3760
3768
|
documents: answer.changed[room.locator] ?? [],
|
|
3761
3769
|
room: true,
|
|
3762
3770
|
});
|
|
3763
|
-
if (read === 'staged' || read === 'deferred') {
|
|
3764
|
-
if (declarationRefusals.delete(room.locator)) {
|
|
3765
|
-
sourceRefusals.delete(room.locator);
|
|
3766
|
-
unreadable.delete(room.locator);
|
|
3767
|
-
}
|
|
3768
|
-
}
|
|
3769
|
-
else {
|
|
3770
|
-
refusedSource(room, read);
|
|
3771
|
-
declarationRefusals.add(room.locator);
|
|
3772
|
-
}
|
|
3773
|
-
}
|
|
3774
|
-
models.set(key, next);
|
|
3775
|
-
if (next.status === 'current') {
|
|
3776
|
-
liveModels.set(key, JSON.stringify(next.snapshot));
|
|
3777
|
-
invalidModels.delete(key);
|
|
3778
3771
|
}
|
|
3779
|
-
|
|
3772
|
+
heldModel(room, next);
|
|
3780
3773
|
if (changed === null)
|
|
3781
3774
|
emitRooms();
|
|
3782
3775
|
else
|
|
3783
3776
|
publishChangeSet(changed);
|
|
3784
3777
|
});
|
|
3785
3778
|
}
|
|
3779
|
+
function heldModel(room, next) {
|
|
3780
|
+
models.set(room.locator, next);
|
|
3781
|
+
if (next.status === 'current') {
|
|
3782
|
+
liveModels.set(room.locator, JSON.stringify(next.snapshot));
|
|
3783
|
+
invalidModels.delete(room.locator);
|
|
3784
|
+
}
|
|
3785
|
+
list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
3786
|
+
}
|
|
3786
3787
|
async function scoped(result, room, local) {
|
|
3787
3788
|
if (result.scope === 'content' && !result.retryable) {
|
|
3788
3789
|
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
@@ -3796,14 +3797,14 @@ export function composeRuntime(options) {
|
|
|
3796
3797
|
});
|
|
3797
3798
|
let scopedAway = room;
|
|
3798
3799
|
if (result.scope === 'model')
|
|
3799
|
-
await retainModel(local, room,
|
|
3800
|
+
await retainModel(local, room, {
|
|
3800
3801
|
status: 'unavailable',
|
|
3801
3802
|
reason: 'not-readable',
|
|
3802
3803
|
});
|
|
3803
3804
|
else if (result.scope !== 'operation') {
|
|
3804
3805
|
unreadable.add(room.locator);
|
|
3805
3806
|
scopedAway = { ...room, replica: { ...room.replica, admission: 'unavailable' } };
|
|
3806
|
-
await retainModel(local, scopedAway,
|
|
3807
|
+
await retainModel(local, scopedAway, modelOf(room.locator));
|
|
3807
3808
|
}
|
|
3808
3809
|
if (!continuing())
|
|
3809
3810
|
return;
|
|
@@ -3811,6 +3812,17 @@ export function composeRuntime(options) {
|
|
|
3811
3812
|
emitRooms();
|
|
3812
3813
|
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
3813
3814
|
}
|
|
3815
|
+
function name(locator) {
|
|
3816
|
+
const stream = options.adapter.roomStream;
|
|
3817
|
+
const record = replicas.get(locator)?.record;
|
|
3818
|
+
const room = discovered.get(locator);
|
|
3819
|
+
if (stream === undefined || record === undefined || room === undefined)
|
|
3820
|
+
return;
|
|
3821
|
+
if (!socketOpen || !handed || record.replica.selection !== room.replica.selection)
|
|
3822
|
+
return;
|
|
3823
|
+
naming += 1;
|
|
3824
|
+
stream.hold([{ locator, roomVersion: record.roomVersion }]);
|
|
3825
|
+
}
|
|
3814
3826
|
function heldRooms(listed) {
|
|
3815
3827
|
return listed.flatMap((room) => {
|
|
3816
3828
|
const record = replicas.get(room.locator)?.record;
|
|
@@ -3828,14 +3840,20 @@ export function composeRuntime(options) {
|
|
|
3828
3840
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
3829
3841
|
});
|
|
3830
3842
|
if (attempt === 'opened') {
|
|
3843
|
+
naming += 1;
|
|
3831
3844
|
options.adapter.roomStream?.hold(heldRooms(ready));
|
|
3832
3845
|
handed = true;
|
|
3833
3846
|
}
|
|
3834
3847
|
discovering?.release();
|
|
3835
|
-
const
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3848
|
+
for (const room of ready) {
|
|
3849
|
+
if (attempt !== 'unstreamed' && replicas.has(room.locator)) {
|
|
3850
|
+
if (attempt === 'authorized')
|
|
3851
|
+
name(room.locator);
|
|
3852
|
+
continue;
|
|
3853
|
+
}
|
|
3854
|
+
void roomTask(room.locator, (live) => loadRoom(room, local, progress, live)).then(() => rest(progress));
|
|
3855
|
+
}
|
|
3856
|
+
const failed = prepared.find((room) => room.status === 'rejected');
|
|
3839
3857
|
if (failed !== undefined)
|
|
3840
3858
|
throw failed.reason;
|
|
3841
3859
|
}
|
|
@@ -3853,27 +3871,29 @@ export function composeRuntime(options) {
|
|
|
3853
3871
|
return false;
|
|
3854
3872
|
}
|
|
3855
3873
|
if (prior !== undefined)
|
|
3856
|
-
await retainModel(local, room,
|
|
3874
|
+
await retainModel(local, room, modelOf(room.locator));
|
|
3857
3875
|
if (discoveryPaused || authorization === undefined)
|
|
3858
3876
|
return false;
|
|
3859
3877
|
return room.replica.admission === 'admitted';
|
|
3860
3878
|
}
|
|
3861
3879
|
async function caughtUp(room, local, progress, live) {
|
|
3862
|
-
if (
|
|
3880
|
+
if (options.adapter.roomStream !== undefined || authorization === undefined)
|
|
3881
|
+
return;
|
|
3882
|
+
if (!live() || !behind(room))
|
|
3863
3883
|
return;
|
|
3864
3884
|
const withheld = await advance(room, local, authorization, live);
|
|
3865
3885
|
if (withheld !== null)
|
|
3866
3886
|
await withhold(withheld, room, local, progress);
|
|
3867
3887
|
}
|
|
3868
3888
|
function revoked(locator) {
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
3872
|
-
});
|
|
3889
|
+
const model = modelOf(locator);
|
|
3890
|
+
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
3873
3891
|
}
|
|
3874
3892
|
async function loadRoom(room, local, progress, live) {
|
|
3875
3893
|
if (!live() || authorization === undefined)
|
|
3876
3894
|
return;
|
|
3895
|
+
if (replicas.has(room.locator) && !(await prepareRoom(room, local, progress)))
|
|
3896
|
+
return;
|
|
3877
3897
|
if (replicas.has(room.locator) && revoked(room.locator)) {
|
|
3878
3898
|
await reload(room, local, progress, live);
|
|
3879
3899
|
return;
|
|
@@ -3891,14 +3911,14 @@ export function composeRuntime(options) {
|
|
|
3891
3911
|
reloadsDue.delete(room.locator);
|
|
3892
3912
|
if (!live() || authorization === undefined)
|
|
3893
3913
|
return;
|
|
3914
|
+
const from = standingOf(room.locator);
|
|
3894
3915
|
const bootstrapped = await pullBootstrap(authorization, room, local, live);
|
|
3895
|
-
if (!live())
|
|
3896
|
-
return;
|
|
3897
3916
|
if (bootstrapped.kind !== 'staged') {
|
|
3898
|
-
|
|
3917
|
+
if (live())
|
|
3918
|
+
await withhold(bootstrapped, room, local, progress);
|
|
3899
3919
|
return;
|
|
3900
3920
|
}
|
|
3901
|
-
await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
|
|
3921
|
+
await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from }, live);
|
|
3902
3922
|
await caughtUp(room, local, progress, live);
|
|
3903
3923
|
}
|
|
3904
3924
|
finally {
|
|
@@ -3920,6 +3940,8 @@ export function composeRuntime(options) {
|
|
|
3920
3940
|
catch {
|
|
3921
3941
|
result = { kind: 'transport-uncertainty' };
|
|
3922
3942
|
}
|
|
3943
|
+
if (result.kind === 'discovery')
|
|
3944
|
+
roomsNamed = true;
|
|
3923
3945
|
if (!live()) {
|
|
3924
3946
|
progress.complete = false;
|
|
3925
3947
|
notifyReady();
|
|
@@ -3927,12 +3949,7 @@ export function composeRuntime(options) {
|
|
|
3927
3949
|
}
|
|
3928
3950
|
journal.record(`connection discover ${result.kind}`);
|
|
3929
3951
|
if (result.kind === 'transport-uncertainty') {
|
|
3930
|
-
|
|
3931
|
-
await retainModel(local, room, 'collection', cached(modelOf(room.locator)));
|
|
3932
|
-
if (room.typedSelection !== null) {
|
|
3933
|
-
await retainModel(local, room, 'typed-selection', cached(modelOf(room.locator, 'typed-selection')));
|
|
3934
|
-
}
|
|
3935
|
-
}
|
|
3952
|
+
await settleModels([...rooms.values()].map((room) => discovered.get(room.locator) ?? room), 'cached', local);
|
|
3936
3953
|
progress.reachable = false;
|
|
3937
3954
|
progress.complete = false;
|
|
3938
3955
|
notifyReady();
|
|
@@ -3987,6 +4004,7 @@ export function composeRuntime(options) {
|
|
|
3987
4004
|
limits = result.providerLimits;
|
|
3988
4005
|
await local.fact('providerLimits', limits);
|
|
3989
4006
|
await local.fact('capabilities', capabilities);
|
|
4007
|
+
conversations.declared();
|
|
3990
4008
|
}
|
|
3991
4009
|
const signedIn = result.member ?? null;
|
|
3992
4010
|
if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
|
|
@@ -4009,23 +4027,45 @@ export function composeRuntime(options) {
|
|
|
4009
4027
|
}
|
|
4010
4028
|
}
|
|
4011
4029
|
async function admittedRead(work) {
|
|
4012
|
-
|
|
4013
|
-
|
|
4030
|
+
while (roomReads.free === 0) {
|
|
4031
|
+
await new Promise((admit) => roomReads.waiting.push(admit));
|
|
4014
4032
|
}
|
|
4015
|
-
|
|
4016
|
-
while (reads.free === 0)
|
|
4017
|
-
await new Promise((admit) => reads.waiting.push(admit));
|
|
4018
|
-
reads.free -= 1;
|
|
4033
|
+
roomReads.free -= 1;
|
|
4019
4034
|
try {
|
|
4020
4035
|
await work();
|
|
4021
4036
|
}
|
|
4022
4037
|
finally {
|
|
4023
|
-
|
|
4024
|
-
|
|
4038
|
+
roomReads.free += 1;
|
|
4039
|
+
roomReads.waiting.shift()?.();
|
|
4025
4040
|
}
|
|
4026
4041
|
}
|
|
4042
|
+
function inRoom(locator, work) {
|
|
4043
|
+
roomWork += 1;
|
|
4044
|
+
const ran = (roomLanes.get(locator) ?? Promise.resolve()).then(async () => {
|
|
4045
|
+
try {
|
|
4046
|
+
return await work(continuing);
|
|
4047
|
+
}
|
|
4048
|
+
finally {
|
|
4049
|
+
roomWork -= 1;
|
|
4050
|
+
}
|
|
4051
|
+
});
|
|
4052
|
+
const chain = ran.then(() => undefined, () => undefined);
|
|
4053
|
+
roomLanes.set(locator, chain);
|
|
4054
|
+
working.add(chain);
|
|
4055
|
+
void chain.finally(() => {
|
|
4056
|
+
working.delete(chain);
|
|
4057
|
+
if (roomLanes.get(locator) === chain)
|
|
4058
|
+
roomLanes.delete(locator);
|
|
4059
|
+
});
|
|
4060
|
+
return ran;
|
|
4061
|
+
}
|
|
4027
4062
|
function roomTask(locator, work) {
|
|
4028
|
-
|
|
4063
|
+
if (stopping)
|
|
4064
|
+
return Promise.resolve();
|
|
4065
|
+
return inRoom(locator, (live) => admittedRead(() => work(live))).catch(() => undefined);
|
|
4066
|
+
}
|
|
4067
|
+
function advanced(room, local, authorized) {
|
|
4068
|
+
return inRoom(room.locator, (live) => advance(room, local, authorized, live));
|
|
4029
4069
|
}
|
|
4030
4070
|
function task(lane, work) {
|
|
4031
4071
|
if (stopping)
|
|
@@ -4044,20 +4084,17 @@ export function composeRuntime(options) {
|
|
|
4044
4084
|
function sync(attempt) {
|
|
4045
4085
|
return task(OPENING_LANE, async (live) => {
|
|
4046
4086
|
if (!live()) {
|
|
4047
|
-
|
|
4087
|
+
readyFromStore();
|
|
4048
4088
|
return;
|
|
4049
4089
|
}
|
|
4050
4090
|
const progress = { complete: true, reachable: true };
|
|
4051
4091
|
const gate = discover();
|
|
4052
|
-
|
|
4053
|
-
opened = generation;
|
|
4054
|
-
underway = attempt === 'opened' ? { generation, progress } : null;
|
|
4092
|
+
underway = attempt === 'opened' ? progress : null;
|
|
4055
4093
|
if (attempt === 'opened')
|
|
4056
4094
|
streamed = progress;
|
|
4057
4095
|
complete = false;
|
|
4058
4096
|
recorded = false;
|
|
4059
4097
|
moveTo(transferring(status));
|
|
4060
|
-
handed = false;
|
|
4061
4098
|
try {
|
|
4062
4099
|
const local = await openLocal();
|
|
4063
4100
|
if (!live())
|
|
@@ -4085,7 +4122,6 @@ export function composeRuntime(options) {
|
|
|
4085
4122
|
if (handed || closed || progress.reachable)
|
|
4086
4123
|
return;
|
|
4087
4124
|
underway = null;
|
|
4088
|
-
opened = null;
|
|
4089
4125
|
journal.record('connection reopen');
|
|
4090
4126
|
await rest(progress, live);
|
|
4091
4127
|
options.adapter.roomStream?.reopen();
|
|
@@ -4122,13 +4158,14 @@ export function composeRuntime(options) {
|
|
|
4122
4158
|
});
|
|
4123
4159
|
}
|
|
4124
4160
|
function closedSocket(revoked, lost) {
|
|
4161
|
+
notifyDiscovered();
|
|
4125
4162
|
const opened = socketOpen;
|
|
4126
4163
|
socketOpen = false;
|
|
4164
|
+
handed = false;
|
|
4165
|
+
naming = 0;
|
|
4127
4166
|
connectionLost();
|
|
4128
|
-
if (lost)
|
|
4129
|
-
reads.lost();
|
|
4167
|
+
if (lost)
|
|
4130
4168
|
conversations.lost();
|
|
4131
|
-
}
|
|
4132
4169
|
if (revoked) {
|
|
4133
4170
|
void refuse('revoked-authorization');
|
|
4134
4171
|
return;
|
|
@@ -4143,14 +4180,13 @@ export function composeRuntime(options) {
|
|
|
4143
4180
|
});
|
|
4144
4181
|
}
|
|
4145
4182
|
function applyOpening(expired) {
|
|
4146
|
-
const progress = underway
|
|
4183
|
+
const progress = underway;
|
|
4184
|
+
underway = null;
|
|
4185
|
+
naming = Math.max(0, naming - 1);
|
|
4147
4186
|
const cycle = progress ?? { complete: true, reachable: true };
|
|
4148
|
-
if (opened === generation)
|
|
4149
|
-
opened = null;
|
|
4150
4187
|
const reloading = progress === null ? [] : owedReloads(expired);
|
|
4151
4188
|
const replacing = reloadNow((room) => expired.includes(room.locator) || reloading.includes(room.locator), cycle);
|
|
4152
4189
|
return task(OPENING_LANE, async (live) => {
|
|
4153
|
-
underway = null;
|
|
4154
4190
|
if (!live())
|
|
4155
4191
|
return;
|
|
4156
4192
|
if (progress === null) {
|
|
@@ -4158,24 +4194,14 @@ export function composeRuntime(options) {
|
|
|
4158
4194
|
await rest(cycle, live);
|
|
4159
4195
|
return;
|
|
4160
4196
|
}
|
|
4161
|
-
await roomsCarried([...expired, ...reloading]);
|
|
4162
|
-
if (!live())
|
|
4163
|
-
return;
|
|
4164
4197
|
try {
|
|
4165
4198
|
const local = await openLocal();
|
|
4166
|
-
|
|
4199
|
+
await replacing;
|
|
4167
4200
|
if (progress.reachable) {
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
!reloading.includes(room.locator)) {
|
|
4172
|
-
await confirmModels(room, local);
|
|
4173
|
-
}
|
|
4174
|
-
}
|
|
4201
|
+
await settleModels([...discovered.values()].filter((room) => replicas.has(room.locator) &&
|
|
4202
|
+
!expired.includes(room.locator) &&
|
|
4203
|
+
!reloading.includes(room.locator)), 'current', local);
|
|
4175
4204
|
}
|
|
4176
|
-
const failed = reloaded.find((outcome) => outcome.status === 'rejected');
|
|
4177
|
-
if (failed !== undefined)
|
|
4178
|
-
throw failed.reason;
|
|
4179
4205
|
}
|
|
4180
4206
|
catch {
|
|
4181
4207
|
progress.complete = false;
|
|
@@ -4187,11 +4213,8 @@ export function composeRuntime(options) {
|
|
|
4187
4213
|
}
|
|
4188
4214
|
function owedReloads(expired) {
|
|
4189
4215
|
const reloading = [...discovered.values()]
|
|
4190
|
-
.filter((room) => replicas.has(room.locator) &&
|
|
4191
|
-
!expired.includes(room.locator) &&
|
|
4192
|
-
(owed.has(room.locator) || revoked(room.locator)))
|
|
4216
|
+
.filter((room) => replicas.has(room.locator) && !expired.includes(room.locator) && revoked(room.locator))
|
|
4193
4217
|
.map((room) => room.locator);
|
|
4194
|
-
owed.clear();
|
|
4195
4218
|
for (const locator of reloading)
|
|
4196
4219
|
catching.add(locator);
|
|
4197
4220
|
liveQueries.recover();
|
|
@@ -4200,24 +4223,43 @@ export function composeRuntime(options) {
|
|
|
4200
4223
|
async function reloadNow(replaced, progress) {
|
|
4201
4224
|
const reloaded = [...discovered.values()].filter((room) => replicas.has(room.locator) && replaced(room));
|
|
4202
4225
|
for (const room of reloaded)
|
|
4203
|
-
reloadsDue.
|
|
4226
|
+
reloadsDue.add(room.locator);
|
|
4204
4227
|
try {
|
|
4205
4228
|
const local = await openLocal();
|
|
4206
|
-
|
|
4229
|
+
for (const room of reloaded) {
|
|
4230
|
+
void roomTask(room.locator, (live) => reload(room, local, progress, live)).then(() => rest(progress));
|
|
4231
|
+
}
|
|
4207
4232
|
}
|
|
4208
4233
|
catch {
|
|
4209
4234
|
for (const room of reloaded)
|
|
4210
4235
|
reloadsDue.delete(room.locator);
|
|
4211
4236
|
progress.reachable = false;
|
|
4212
|
-
return [];
|
|
4213
4237
|
}
|
|
4214
4238
|
}
|
|
4215
|
-
async function
|
|
4216
|
-
|
|
4217
|
-
const model = modelOf(room.locator
|
|
4218
|
-
|
|
4219
|
-
|
|
4239
|
+
async function settleModels(held, status, local) {
|
|
4240
|
+
const owed = (room) => {
|
|
4241
|
+
const model = modelOf(room.locator);
|
|
4242
|
+
return model.status !== 'unavailable' && model.status !== status;
|
|
4243
|
+
};
|
|
4244
|
+
const settled = await exclusive(async () => {
|
|
4245
|
+
if (!continuing())
|
|
4246
|
+
return [];
|
|
4247
|
+
const unchanged = held.filter((room) => owed(room) && declaredAsHeld(room));
|
|
4248
|
+
for (const room of unchanged) {
|
|
4249
|
+
const next = { ...modelOf(room.locator), status };
|
|
4250
|
+
const replica = replicas.get(room.locator);
|
|
4251
|
+
if (replica !== undefined)
|
|
4252
|
+
replicas.set(room.locator, { ...replica, record: { ...replica.record, model: next } });
|
|
4253
|
+
heldModel(room, next);
|
|
4220
4254
|
}
|
|
4255
|
+
if (unchanged.length > 0)
|
|
4256
|
+
emitRooms();
|
|
4257
|
+
return unchanged;
|
|
4258
|
+
});
|
|
4259
|
+
for (const room of held) {
|
|
4260
|
+
if (settled.includes(room) || !owed(room))
|
|
4261
|
+
continue;
|
|
4262
|
+
await retainModel(local, room, { ...modelOf(room.locator), status });
|
|
4221
4263
|
}
|
|
4222
4264
|
}
|
|
4223
4265
|
function submitLater(locator) {
|
|
@@ -4262,14 +4304,10 @@ export function composeRuntime(options) {
|
|
|
4262
4304
|
try {
|
|
4263
4305
|
const local = await openLocal();
|
|
4264
4306
|
await roomTask(locator, async (live) => {
|
|
4265
|
-
|
|
4266
|
-
if (!current() || authorization === undefined)
|
|
4307
|
+
if (!live() || authorization === undefined)
|
|
4267
4308
|
return;
|
|
4268
|
-
const withheld = await
|
|
4269
|
-
|
|
4270
|
-
abandonedOn(signal),
|
|
4271
|
-
]);
|
|
4272
|
-
if (withheld !== null && current())
|
|
4309
|
+
const withheld = await advance(room, local, authorization, live);
|
|
4310
|
+
if (withheld !== null && live())
|
|
4273
4311
|
await withhold(withheld, room, local, progress);
|
|
4274
4312
|
});
|
|
4275
4313
|
}
|
|
@@ -4324,14 +4362,11 @@ export function composeRuntime(options) {
|
|
|
4324
4362
|
progress.complete = false;
|
|
4325
4363
|
return;
|
|
4326
4364
|
}
|
|
4327
|
-
reads.applied();
|
|
4328
4365
|
submitAll();
|
|
4329
4366
|
for (const locator of stale)
|
|
4330
4367
|
if (repairable.has(locator))
|
|
4331
4368
|
repairLater(locator, {});
|
|
4332
4369
|
await conversations.reconcile();
|
|
4333
|
-
if (status.providerUnreachable)
|
|
4334
|
-
conversations.reconnected();
|
|
4335
4370
|
const reading = { complete: true, reachable: true };
|
|
4336
4371
|
await conversations.catchUp(reading);
|
|
4337
4372
|
if (progress.reachable && reading.reachable)
|
|
@@ -4357,7 +4392,7 @@ export function composeRuntime(options) {
|
|
|
4357
4392
|
if (!live())
|
|
4358
4393
|
return;
|
|
4359
4394
|
let lastCompleted = status.lastCompleted;
|
|
4360
|
-
let synced = complete && stale.size === 0 && !pending();
|
|
4395
|
+
let synced = complete && roomWork === 0 && naming === 0 && stale.size === 0 && !pending();
|
|
4361
4396
|
if (synced && !recorded) {
|
|
4362
4397
|
const completedAt = new Date().toISOString();
|
|
4363
4398
|
try {
|
|
@@ -4372,74 +4407,79 @@ export function composeRuntime(options) {
|
|
|
4372
4407
|
}
|
|
4373
4408
|
}
|
|
4374
4409
|
moveTo({
|
|
4375
|
-
state: pending()
|
|
4410
|
+
state: pending()
|
|
4411
|
+
? 'pending'
|
|
4412
|
+
: synced && complete && roomWork === 0 && naming === 0
|
|
4413
|
+
? 'synced'
|
|
4414
|
+
: 'syncing',
|
|
4376
4415
|
lastCompleted,
|
|
4377
|
-
providerUnreachable: !progress.reachable,
|
|
4416
|
+
providerUnreachable: progress === null ? status.providerUnreachable : !progress.reachable,
|
|
4378
4417
|
...attention(),
|
|
4379
4418
|
});
|
|
4380
4419
|
}
|
|
4381
4420
|
function discover() {
|
|
4421
|
+
discovering?.release();
|
|
4382
4422
|
let release = () => { };
|
|
4383
4423
|
const done = new Promise((resolve) => {
|
|
4384
4424
|
release = resolve;
|
|
4385
4425
|
});
|
|
4386
|
-
discovering = {
|
|
4426
|
+
discovering = { done, release };
|
|
4387
4427
|
return discovering;
|
|
4388
4428
|
}
|
|
4389
|
-
function applyRoom(
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
if (!live() || authorization === undefined)
|
|
4398
|
-
return;
|
|
4399
|
-
const room = discovered.get(locator);
|
|
4400
|
-
const held = replicas.get(locator);
|
|
4401
|
-
if (room === undefined || room.replica.admission !== 'admitted')
|
|
4402
|
-
return;
|
|
4403
|
-
if (held !== undefined &&
|
|
4404
|
-
(held.record.replica.selection !== room.replica.selection ||
|
|
4405
|
-
options.adapter.reached(locator, held.record.roomVersion, roomVersion))) {
|
|
4406
|
-
return;
|
|
4407
|
-
}
|
|
4408
|
-
const during = underway?.generation === generation ? underway.progress : null;
|
|
4409
|
-
const progress = during ?? { complete: true, reachable: true };
|
|
4410
|
-
if (during === null)
|
|
4411
|
-
moveTo(transferring(status));
|
|
4429
|
+
function applyRoom(change) {
|
|
4430
|
+
const { locator } = change;
|
|
4431
|
+
if (stopping)
|
|
4432
|
+
return Promise.resolve();
|
|
4433
|
+
return inRoom(locator, async (live) => {
|
|
4434
|
+
await discovering?.done;
|
|
4435
|
+
await answering.get(locator)?.answered;
|
|
4436
|
+
const progress = underway ?? { complete: true, reachable: true };
|
|
4412
4437
|
try {
|
|
4413
4438
|
const local = await openLocal();
|
|
4414
|
-
|
|
4415
|
-
if (!live()
|
|
4416
|
-
return;
|
|
4417
|
-
const
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4439
|
+
for (;;) {
|
|
4440
|
+
if (!live())
|
|
4441
|
+
return null;
|
|
4442
|
+
const room = discovered.get(locator);
|
|
4443
|
+
const held = replicas.get(locator);
|
|
4444
|
+
if (room === undefined ||
|
|
4445
|
+
held === undefined ||
|
|
4446
|
+
room.replica.admission !== 'admitted' ||
|
|
4447
|
+
held.record.replica.selection !== room.replica.selection) {
|
|
4448
|
+
return null;
|
|
4449
|
+
}
|
|
4450
|
+
const at = held.record.roomVersion;
|
|
4451
|
+
if (options.adapter.reached(locator, at, change.roomVersion))
|
|
4452
|
+
return progress;
|
|
4453
|
+
if (!options.adapter.reached(locator, at, change.from)) {
|
|
4454
|
+
name(locator);
|
|
4455
|
+
return progress;
|
|
4456
|
+
}
|
|
4457
|
+
if (underway === null)
|
|
4458
|
+
moveTo(transferring(status));
|
|
4459
|
+
const writing = () => live() && !full;
|
|
4460
|
+
const refreshed = await ahead((hold) => pullRefresh(authorization ?? {}, held, writing, hold, () => change.read()));
|
|
4461
|
+
if (refreshed.kind !== 'advanced')
|
|
4462
|
+
return progress;
|
|
4463
|
+
const landing = {
|
|
4464
|
+
kind: 'refreshed',
|
|
4465
|
+
from: refreshed.from,
|
|
4466
|
+
remodelled: refreshed.remodelled,
|
|
4467
|
+
};
|
|
4468
|
+
if (await publish(local, room, refreshed.record, landing, writing)) {
|
|
4469
|
+
stale.delete(locator);
|
|
4470
|
+
await retainModel(local, room, modelOf(locator));
|
|
4471
|
+
return progress;
|
|
4472
|
+
}
|
|
4473
|
+
}
|
|
4421
4474
|
}
|
|
4422
4475
|
catch {
|
|
4476
|
+
if (!stale.has(locator))
|
|
4477
|
+
name(locator);
|
|
4423
4478
|
stale.add(locator);
|
|
4479
|
+
return progress;
|
|
4424
4480
|
}
|
|
4425
|
-
|
|
4426
|
-
if (during === null)
|
|
4427
|
-
await rest(progress, live);
|
|
4428
|
-
}
|
|
4429
|
-
});
|
|
4481
|
+
}).then((progress) => rest(progress), () => undefined);
|
|
4430
4482
|
}
|
|
4431
|
-
function roomsCarried(replacing) {
|
|
4432
|
-
const replaced = new Set(replacing.map(roomLane));
|
|
4433
|
-
return Promise.all([...lanes]
|
|
4434
|
-
.filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
|
|
4435
|
-
.map(([, held]) => held.chain));
|
|
4436
|
-
}
|
|
4437
|
-
const receipts = {
|
|
4438
|
-
heads: async () => (await openLocal()).receivedHeads(),
|
|
4439
|
-
parts: async (key, count) => (await openLocal()).receivedParts(key, count),
|
|
4440
|
-
keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
|
|
4441
|
-
drop: async (keys) => (await openLocal()).dropReceived(keys),
|
|
4442
|
-
};
|
|
4443
4483
|
function observeStream() {
|
|
4444
4484
|
const stream = options.adapter.roomStream;
|
|
4445
4485
|
if (stream === undefined || withdrawStream !== undefined)
|
|
@@ -4449,9 +4489,10 @@ export function composeRuntime(options) {
|
|
|
4449
4489
|
return;
|
|
4450
4490
|
if (event.kind === 'opened') {
|
|
4451
4491
|
socketOpen = true;
|
|
4492
|
+
handed = false;
|
|
4493
|
+
naming = 0;
|
|
4452
4494
|
connectionLive();
|
|
4453
4495
|
generation += 1;
|
|
4454
|
-
reads.opened();
|
|
4455
4496
|
conversations.reopened();
|
|
4456
4497
|
recovery.resume();
|
|
4457
4498
|
void sync('opened');
|
|
@@ -4463,15 +4504,8 @@ export function composeRuntime(options) {
|
|
|
4463
4504
|
else if (event.kind === 'authority-changed')
|
|
4464
4505
|
void rediscover(event.scope);
|
|
4465
4506
|
else
|
|
4466
|
-
void applyRoom(event
|
|
4467
|
-
}
|
|
4468
|
-
}
|
|
4469
|
-
async function heldRevisions(locator, paths) {
|
|
4470
|
-
const held = replicas.get(locator);
|
|
4471
|
-
if (held === undefined || paths.length === 0)
|
|
4472
|
-
return new Map();
|
|
4473
|
-
const entries = await (await openLocal()).entries(held.record.key, 'confirmed', paths);
|
|
4474
|
-
return new Map(entries.flatMap(({ path, revision }) => revision === null ? [] : [[path, revision]]));
|
|
4507
|
+
void applyRoom(event);
|
|
4508
|
+
});
|
|
4475
4509
|
}
|
|
4476
4510
|
function writesHeld() {
|
|
4477
4511
|
return serviceMode === 'write-halted' || haltedRefusal;
|
|
@@ -4541,8 +4575,9 @@ export function composeRuntime(options) {
|
|
|
4541
4575
|
if (closed)
|
|
4542
4576
|
return;
|
|
4543
4577
|
closed = true;
|
|
4544
|
-
|
|
4545
|
-
|
|
4578
|
+
notifyDiscovered();
|
|
4579
|
+
if (probing !== null)
|
|
4580
|
+
clearInterval(probing);
|
|
4546
4581
|
connectionLive();
|
|
4547
4582
|
lockListeners.clear();
|
|
4548
4583
|
readinessWatches.clear();
|
|
@@ -4553,11 +4588,8 @@ export function composeRuntime(options) {
|
|
|
4553
4588
|
withdrawServiceMode?.();
|
|
4554
4589
|
conversations.close();
|
|
4555
4590
|
void recovery.retire();
|
|
4556
|
-
store?.close();
|
|
4557
|
-
void opening?.then((opened) => {
|
|
4558
|
-
opened.close();
|
|
4559
|
-
}, () => { });
|
|
4560
4591
|
roomListeners.clear();
|
|
4592
|
+
unlistedRooms.clear();
|
|
4561
4593
|
refusalListeners.clear();
|
|
4562
4594
|
storageLossListeners.clear();
|
|
4563
4595
|
statusListeners.clear();
|
|
@@ -4570,6 +4602,7 @@ export function composeRuntime(options) {
|
|
|
4570
4602
|
}
|
|
4571
4603
|
async function stop() {
|
|
4572
4604
|
stopping = true;
|
|
4605
|
+
inFlight.abort();
|
|
4573
4606
|
await recovery.retire();
|
|
4574
4607
|
await Promise.all(working);
|
|
4575
4608
|
await mutations;
|
|
@@ -4583,9 +4616,9 @@ export function composeRuntime(options) {
|
|
|
4583
4616
|
observeStream();
|
|
4584
4617
|
void openLocal().then(() => {
|
|
4585
4618
|
if (capabilities.length > 0)
|
|
4586
|
-
|
|
4619
|
+
readyFromStore();
|
|
4587
4620
|
}, () => {
|
|
4588
|
-
|
|
4621
|
+
readyFromStore();
|
|
4589
4622
|
});
|
|
4590
4623
|
if (options.adapter.roomStream === undefined)
|
|
4591
4624
|
void sync('unstreamed');
|
|
@@ -4632,9 +4665,8 @@ export function composeRuntime(options) {
|
|
|
4632
4665
|
}
|
|
4633
4666
|
async function searched(input, reach, locked) {
|
|
4634
4667
|
const asked = searchPlanOf(input);
|
|
4635
|
-
const plan = { ...asked, limit: capped(asked.limit) };
|
|
4636
4668
|
const filter = {
|
|
4637
|
-
...filterOf(
|
|
4669
|
+
...filterOf(asked.documents ?? {}, roomFacts(reach), planReach(reach)),
|
|
4638
4670
|
locked,
|
|
4639
4671
|
};
|
|
4640
4672
|
const unlocking = (filter.rooms ?? [])
|
|
@@ -4643,65 +4675,41 @@ export function composeRuntime(options) {
|
|
|
4643
4675
|
for (;;) {
|
|
4644
4676
|
const cover = covered(filter);
|
|
4645
4677
|
awaitPreparation(cover);
|
|
4646
|
-
const pending = [
|
|
4647
|
-
...unlocking,
|
|
4648
|
-
...('pending' in cover ? cover.pending : []),
|
|
4649
|
-
];
|
|
4650
|
-
const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
|
|
4651
|
-
const held = covered({
|
|
4652
|
-
...filter,
|
|
4653
|
-
rooms: reachedRooms(filter).filter((room) => !awaited.has(room)),
|
|
4654
|
-
});
|
|
4655
|
-
const covering = 'pending' in held ? [] : held.coverage.rooms;
|
|
4656
|
-
const unindexed = covering
|
|
4657
|
-
.filter(({ room }) => !searchIndex.rooms().has(room))
|
|
4658
|
-
.map(({ room }) => room);
|
|
4659
|
-
for (const room of unindexed)
|
|
4660
|
-
pending.push({ room, awaits: 'index' });
|
|
4661
|
-
const listed = conversations.searchable();
|
|
4662
|
-
const named = reach !== null && !reach.conversations
|
|
4663
|
-
? []
|
|
4664
|
-
: (plan.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
|
|
4665
|
-
const waiting = named.filter((one) => !searchIndex.conversations().has(one));
|
|
4666
|
-
for (const conversation of waiting)
|
|
4667
|
-
pending.push({ conversation, awaits: 'index' });
|
|
4668
|
-
if (unindexed.length > 0 || waiting.length > 0)
|
|
4669
|
-
index(unindexed, waiting);
|
|
4678
|
+
const pending = [...unlocking, ...('pending' in cover ? cover.pending : [])];
|
|
4670
4679
|
const complete = pending.length === 0 && !('pending' in cover);
|
|
4680
|
+
const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
|
|
4671
4681
|
const answering = complete
|
|
4672
4682
|
? filter
|
|
4673
|
-
: {
|
|
4674
|
-
...filter,
|
|
4675
|
-
rooms: covering
|
|
4676
|
-
.filter(({ room }) => searchIndex.rooms().has(room))
|
|
4677
|
-
.map(({ room }) => room),
|
|
4678
|
-
};
|
|
4683
|
+
: { ...filter, rooms: reachedRooms(filter).filter((room) => !awaited.has(room)) };
|
|
4679
4684
|
const answered = covered(answering);
|
|
4680
4685
|
if ('pending' in answered)
|
|
4681
4686
|
continue;
|
|
4682
|
-
const
|
|
4683
|
-
const
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4687
|
+
const listed = conversations.searchable();
|
|
4688
|
+
const named = reach !== null && !reach.conversations
|
|
4689
|
+
? []
|
|
4690
|
+
: (asked.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
|
|
4691
|
+
const hits = await (await openLocal()).search({
|
|
4692
|
+
text: asked.text,
|
|
4693
|
+
limit: capped(asked.limit),
|
|
4694
|
+
after: asked.after,
|
|
4695
|
+
total: asked.total,
|
|
4696
|
+
documents: {
|
|
4697
|
+
plan: answering,
|
|
4698
|
+
rooms: answered.rooms,
|
|
4699
|
+
beneath: asked.collection
|
|
4700
|
+
? answered.rooms.map(({ locator, key }) => ({
|
|
4701
|
+
key,
|
|
4702
|
+
roots: replicas.get(locator)?.record.typedSelection?.roots ?? [],
|
|
4703
|
+
}))
|
|
4704
|
+
: [],
|
|
4705
|
+
},
|
|
4706
|
+
conversations: named,
|
|
4707
|
+
});
|
|
4708
|
+
if (JSON.stringify(covered(answering)) !== JSON.stringify(answered))
|
|
4690
4709
|
continue;
|
|
4691
|
-
const hits = searchIndex.search(plan, selected === null ? null : selected.documents, searchedConversations, answering.rooms !== null
|
|
4692
|
-
? new Set(answering.rooms)
|
|
4693
|
-
: locked.length === 0
|
|
4694
|
-
? null
|
|
4695
|
-
: new Set(reachedRooms(answering)), plan.collection
|
|
4696
|
-
? new Map(answered.coverage.rooms.map(({ room }) => [
|
|
4697
|
-
room,
|
|
4698
|
-
replicas.get(room)?.record.typedSelection?.roots ?? [],
|
|
4699
|
-
]))
|
|
4700
|
-
: null);
|
|
4701
4710
|
const coverage = {
|
|
4702
4711
|
rooms: answered.coverage.rooms,
|
|
4703
|
-
conversations:
|
|
4704
|
-
index: searchIndex.generation(),
|
|
4712
|
+
conversations: named,
|
|
4705
4713
|
overlay: answered.coverage.overlay,
|
|
4706
4714
|
};
|
|
4707
4715
|
return complete
|
|
@@ -4758,12 +4766,13 @@ export function composeRuntime(options) {
|
|
|
4758
4766
|
};
|
|
4759
4767
|
}
|
|
4760
4768
|
if (topic === 'readiness') {
|
|
4761
|
-
return watchReadiness({ target: '', code: 'stored',
|
|
4769
|
+
return watchReadiness({ target: '', code: 'stored', space: null, search: 'unused' }, null, deliver);
|
|
4762
4770
|
}
|
|
4763
4771
|
if (topic === 'rooms') {
|
|
4764
|
-
|
|
4765
|
-
|
|
4772
|
+
unlistedRooms.add(deliver);
|
|
4773
|
+
emitKnownRooms();
|
|
4766
4774
|
return () => {
|
|
4775
|
+
unlistedRooms.delete(deliver);
|
|
4767
4776
|
roomListeners.delete(deliver);
|
|
4768
4777
|
};
|
|
4769
4778
|
}
|
|
@@ -4817,7 +4826,17 @@ export function composeRuntime(options) {
|
|
|
4817
4826
|
return openLocal().then(() => undefined);
|
|
4818
4827
|
},
|
|
4819
4828
|
rooms() {
|
|
4820
|
-
|
|
4829
|
+
if (closed)
|
|
4830
|
+
return Promise.resolve([]);
|
|
4831
|
+
return new Promise((settle) => {
|
|
4832
|
+
const answer = (value) => {
|
|
4833
|
+
unlistedRooms.delete(answer);
|
|
4834
|
+
roomListeners.delete(answer);
|
|
4835
|
+
settle(value);
|
|
4836
|
+
};
|
|
4837
|
+
unlistedRooms.add(answer);
|
|
4838
|
+
emitKnownRooms();
|
|
4839
|
+
});
|
|
4821
4840
|
},
|
|
4822
4841
|
query: ((input) => rowsQuery(planned(input, null))),
|
|
4823
4842
|
search: (input) => searched(input, null, []),
|
|
@@ -4827,7 +4846,7 @@ export function composeRuntime(options) {
|
|
|
4827
4846
|
typedSelection: rooms.get(locator)?.typedSelection === undefined ||
|
|
4828
4847
|
rooms.get(locator)?.typedSelection === null
|
|
4829
4848
|
? null
|
|
4830
|
-
: modelTypes(
|
|
4849
|
+
: modelTypes(builtIn),
|
|
4831
4850
|
};
|
|
4832
4851
|
},
|
|
4833
4852
|
roomDraft: {
|
|
@@ -4844,33 +4863,17 @@ export function composeRuntime(options) {
|
|
|
4844
4863
|
const record = roomDrafts.get(room)?.get(path);
|
|
4845
4864
|
return record === undefined ? null : ((await roomDraftsOf([record]))[0] ?? null);
|
|
4846
4865
|
},
|
|
4847
|
-
write(room, path, revision, input) {
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
const
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
? undefined
|
|
4859
|
-
: await heldFile(parts.file.blob, binaryBase(await visibleOf(room, [path]), path));
|
|
4860
|
-
const record = {
|
|
4861
|
-
locator: room,
|
|
4862
|
-
path,
|
|
4863
|
-
revision: roomDraftRevision(),
|
|
4864
|
-
parts: held(current?.parts, parts, file),
|
|
4865
|
-
returned: current?.returned ?? null,
|
|
4866
|
-
};
|
|
4867
|
-
await local.keepRoomDraft(record, parts.file === undefined || file === undefined
|
|
4868
|
-
? []
|
|
4869
|
-
: [{ key: file.localId, blob: parts.file.blob }]);
|
|
4870
|
-
rememberRoomDraft(record);
|
|
4871
|
-
publishChangeSet(changeSet(room, { drafts: [path] }));
|
|
4872
|
-
return { outcome: 'written', roomDraft: (await roomDraftsOf([record]))[0] ?? null };
|
|
4873
|
-
});
|
|
4866
|
+
write(room, path, revision, input, next = roomDraftRevision()) {
|
|
4867
|
+
if (typeof next !== 'string' || next === '')
|
|
4868
|
+
return Promise.reject(runtimeError('room-draft-malformed'));
|
|
4869
|
+
return gathered(`room-draft\u0000${room}\u0000${path}`, { revision, input, next }, (earlier, later) => {
|
|
4870
|
+
if (later.revision !== earlier.next)
|
|
4871
|
+
return null;
|
|
4872
|
+
const parts = gatheredParts(earlier.input, later.input);
|
|
4873
|
+
return parts === null
|
|
4874
|
+
? null
|
|
4875
|
+
: { revision: earlier.revision, input: parts, next: later.next };
|
|
4876
|
+
}, (written) => writtenRoomDraft(room, path, written.revision, written.input, written.next));
|
|
4874
4877
|
},
|
|
4875
4878
|
discard(room, path, revision) {
|
|
4876
4879
|
return exclusive(async () => {
|
|
@@ -4897,13 +4900,13 @@ export function composeRuntime(options) {
|
|
|
4897
4900
|
});
|
|
4898
4901
|
},
|
|
4899
4902
|
write(app, value) {
|
|
4900
|
-
return
|
|
4903
|
+
return exclusive(() => keptPresentation(app, () => structuredClone(value)));
|
|
4901
4904
|
},
|
|
4902
4905
|
edit(app, members) {
|
|
4903
|
-
return
|
|
4904
|
-
...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(
|
|
4905
|
-
...Object.entries(
|
|
4906
|
-
]));
|
|
4906
|
+
return gathered(`presentation\u0000${app}`, structuredClone(members), (earlier, later) => ({ ...earlier, ...later }), (edited) => keptPresentation(app, (held) => Object.fromEntries([
|
|
4907
|
+
...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(edited, member)),
|
|
4908
|
+
...Object.entries(edited).filter(([, value]) => value !== null),
|
|
4909
|
+
])));
|
|
4907
4910
|
},
|
|
4908
4911
|
},
|
|
4909
4912
|
save(room, named, writtenBy) {
|
|
@@ -4922,7 +4925,8 @@ export function composeRuntime(options) {
|
|
|
4922
4925
|
part: readPart,
|
|
4923
4926
|
read: readBinaryPath,
|
|
4924
4927
|
link: linkBinaryPath,
|
|
4925
|
-
|
|
4928
|
+
show: showBinaryPath,
|
|
4929
|
+
attach: attachBinary,
|
|
4926
4930
|
},
|
|
4927
4931
|
async call(name, input) {
|
|
4928
4932
|
await ready;
|
|
@@ -4943,9 +4947,10 @@ export function composeRuntime(options) {
|
|
|
4943
4947
|
},
|
|
4944
4948
|
async retry() {
|
|
4945
4949
|
journal.record('retry');
|
|
4946
|
-
|
|
4950
|
+
unpreparable.clear();
|
|
4947
4951
|
queueReadiness();
|
|
4948
|
-
|
|
4952
|
+
if (full)
|
|
4953
|
+
await probed();
|
|
4949
4954
|
},
|
|
4950
4955
|
observe: observing(watch),
|
|
4951
4956
|
async reauthorize() {
|
|
@@ -4967,7 +4972,6 @@ export function composeRuntime(options) {
|
|
|
4967
4972
|
const reachable = {
|
|
4968
4973
|
md,
|
|
4969
4974
|
watch,
|
|
4970
|
-
referrerScope: (referrer) => options.adapter.referrerScope(referrer),
|
|
4971
4975
|
plan: planned,
|
|
4972
4976
|
query: (plan) => rowsQuery(plan),
|
|
4973
4977
|
watchPlan,
|
|
@@ -4977,6 +4981,7 @@ export function composeRuntime(options) {
|
|
|
4977
4981
|
lockedRooms,
|
|
4978
4982
|
watchLocks,
|
|
4979
4983
|
located,
|
|
4984
|
+
room: (locator) => listed().find((one) => one.locator === locator),
|
|
4980
4985
|
};
|
|
4981
4986
|
return {
|
|
4982
4987
|
md,
|
|
@@ -4991,6 +4996,10 @@ export function composeRuntime(options) {
|
|
|
4991
4996
|
journal.record(`persistence ${answer}`);
|
|
4992
4997
|
queueReadiness();
|
|
4993
4998
|
},
|
|
4994
|
-
within: (reach, port) =>
|
|
4999
|
+
within: (reach, port) => {
|
|
5000
|
+
if (full)
|
|
5001
|
+
void probed();
|
|
5002
|
+
return narrowed(reachable, reach, port);
|
|
5003
|
+
},
|
|
4995
5004
|
};
|
|
4996
5005
|
}
|