@agentshouse/mdruntime 0.1.0-c03e7b79830dd → 0.1.0-c069136dcb4e8
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 +47 -48
- 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 -1174
- package/dist/conversation.d.ts +6 -9
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +191 -121
- 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 +5 -3
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +96 -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 -244
- package/dist/chat/messages.d.ts +0 -30
- package/dist/chat/messages.d.ts.map +0 -1
- package/dist/chat/messages.js +0 -198
- 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 -306
- 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
|
-
import {
|
|
2
|
-
import { attachedModel,
|
|
1
|
+
import { parseFrontmatter, roomTarget, target } from '@agentshouse/mdmodel';
|
|
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}`;
|
|
135
|
+
function modelOf(locator) {
|
|
136
|
+
return models.get(locator) ?? NO_MODEL;
|
|
129
137
|
}
|
|
130
|
-
function
|
|
131
|
-
return
|
|
132
|
-
}
|
|
133
|
-
function savedModel(locator, attached) {
|
|
134
|
-
return invalidModels.has(modelKey(locator, attached))
|
|
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,73 +198,79 @@ 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
|
-
agents: declaredAgents,
|
|
210
|
-
documents: (declaration, after) => rowsQuery(planOf({
|
|
211
|
-
rooms: [declaration.locator],
|
|
212
|
-
types: [declaration.type],
|
|
213
|
-
select: {
|
|
214
|
-
fields: [
|
|
215
|
-
declaration.identity,
|
|
216
|
-
declaration.name,
|
|
217
|
-
...(declaration.retired === undefined ? [] : [declaration.retired]),
|
|
218
|
-
].map((field) => `/${escapePointer(field)}`),
|
|
219
|
-
},
|
|
220
|
-
limit: MAXIMUM_WINDOW,
|
|
221
|
-
...(after === undefined ? {} : { after }),
|
|
222
|
-
}, roomFacts())),
|
|
223
249
|
call: (name, input) => invokeAction(name, input),
|
|
224
250
|
durably,
|
|
225
251
|
stream: () => options.adapter.conversations,
|
|
226
252
|
publish: publishChangeSet,
|
|
227
253
|
online: () => connectionOf() !== 'unreachable' && refusal === null,
|
|
254
|
+
full: () => full,
|
|
228
255
|
closed: () => closed,
|
|
229
256
|
unanswered: resendLater,
|
|
230
257
|
expired: (operation) => pastHorizon(operation),
|
|
231
258
|
});
|
|
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
259
|
const journal = createLog();
|
|
257
|
-
const keptFiles = openedFiles(
|
|
258
|
-
let
|
|
260
|
+
const keptFiles = openedFiles(database, isFenced);
|
|
261
|
+
let readyFromStore = () => { };
|
|
259
262
|
const ready = new Promise((resolve) => {
|
|
260
|
-
|
|
263
|
+
readyFromStore = resolve;
|
|
261
264
|
});
|
|
265
|
+
let notifyDiscovered = () => { };
|
|
266
|
+
const firstDiscovery = new Promise((resolve) => {
|
|
267
|
+
notifyDiscovered = resolve;
|
|
268
|
+
});
|
|
269
|
+
let roomsNamed = false;
|
|
270
|
+
function notifyReady() {
|
|
271
|
+
notifyDiscovered();
|
|
272
|
+
readyFromStore();
|
|
273
|
+
}
|
|
262
274
|
function rememberRoomDraft(record) {
|
|
263
275
|
const room = roomDrafts.get(record.locator) ?? new Map();
|
|
264
276
|
room.set(record.path, record);
|
|
@@ -278,7 +290,7 @@ export function composeRuntime(options) {
|
|
|
278
290
|
return undefined;
|
|
279
291
|
const local = await openLocal();
|
|
280
292
|
const slice = await stepped(slicing(await local.entries(held.record.key, 'visible', paths), paths));
|
|
281
|
-
return { ...slice, collection: collectionOf(slice.documents,
|
|
293
|
+
return { ...slice, collection: collectionOf(slice.documents, composed(held.record)) };
|
|
282
294
|
}
|
|
283
295
|
function roomDraftOf(record, visible) {
|
|
284
296
|
return structuredClone({
|
|
@@ -334,14 +346,40 @@ export function composeRuntime(options) {
|
|
|
334
346
|
const value = presentations.get(app);
|
|
335
347
|
return value === undefined ? null : structuredClone(value);
|
|
336
348
|
}
|
|
337
|
-
function
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
349
|
+
async function writtenRoomDraft(room, path, revision, input, next) {
|
|
350
|
+
const parts = readParts(input);
|
|
351
|
+
if (path === '' || normalize(path) !== path)
|
|
352
|
+
throw runtimeError('room-draft-malformed');
|
|
353
|
+
const local = await openLocal();
|
|
354
|
+
const current = roomDrafts.get(room)?.get(path);
|
|
355
|
+
if ((current?.revision ?? null) !== revision) {
|
|
356
|
+
return staleWrite(current === undefined ? null : ((await roomDraftsOf([current]))[0] ?? null));
|
|
357
|
+
}
|
|
358
|
+
const file = parts.file === undefined
|
|
359
|
+
? undefined
|
|
360
|
+
: await heldFile(parts.file.blob, parts.file.base === undefined
|
|
361
|
+
? binaryBase(await visibleOf(room, [path]), path)
|
|
362
|
+
: parts.file.base);
|
|
363
|
+
const record = {
|
|
364
|
+
locator: room,
|
|
365
|
+
path,
|
|
366
|
+
revision: next,
|
|
367
|
+
parts: held(current?.parts, parts, file),
|
|
368
|
+
returned: current?.returned ?? null,
|
|
369
|
+
};
|
|
370
|
+
await local.keepRoomDraft(record, parts.file === undefined || file === undefined
|
|
371
|
+
? []
|
|
372
|
+
: [{ key: file.localId, blob: parts.file.blob }]);
|
|
373
|
+
rememberRoomDraft(record);
|
|
374
|
+
publishChangeSet(changeSet(room, { drafts: [path] }));
|
|
375
|
+
return { outcome: 'written', roomDraft: (await roomDraftsOf([record]))[0] ?? null };
|
|
376
|
+
}
|
|
377
|
+
async function keptPresentation(app, next) {
|
|
378
|
+
const local = await openLocal();
|
|
379
|
+
const stored = next(presentations.get(app));
|
|
380
|
+
await local.present({ app, value: stored });
|
|
381
|
+
presentations.set(app, stored);
|
|
382
|
+
publishChangeSet(changeSet('', { presentation: app }));
|
|
345
383
|
}
|
|
346
384
|
function emitPresentation(app) {
|
|
347
385
|
if (closed)
|
|
@@ -356,6 +394,20 @@ export function composeRuntime(options) {
|
|
|
356
394
|
(queuesWithoutReplica.size > 0 ||
|
|
357
395
|
[...replicas.values()].some((replica) => replica.queue.length > 0)));
|
|
358
396
|
}
|
|
397
|
+
function unsent() {
|
|
398
|
+
const changed = new Map();
|
|
399
|
+
for (const room of [...replicas.keys(), ...queuesWithoutReplica.keys()]) {
|
|
400
|
+
for (const { batch, state } of entriesOf(room)) {
|
|
401
|
+
if (state === 'local')
|
|
402
|
+
continue;
|
|
403
|
+
for (const { path } of batch.changes)
|
|
404
|
+
changed.set(`${room}\u0000${path}`, { room, path });
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return [...changed]
|
|
408
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
409
|
+
.map(([, change]) => change);
|
|
410
|
+
}
|
|
359
411
|
function attention() {
|
|
360
412
|
const attentionRequired = [];
|
|
361
413
|
for (const record of [...roomDrafts.values()].flatMap((room) => [...room.values()])) {
|
|
@@ -367,16 +419,9 @@ export function composeRuntime(options) {
|
|
|
367
419
|
returned: structuredClone(record.returned),
|
|
368
420
|
});
|
|
369
421
|
}
|
|
370
|
-
for (const [room, refusal] of sourceRefusals)
|
|
371
|
-
attentionRequired.push({ room, refusal: structuredClone(refusal) });
|
|
372
422
|
attentionRequired.sort((left, right) => {
|
|
373
423
|
if (left.room !== right.room)
|
|
374
424
|
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
425
|
if (left.path !== right.path)
|
|
381
426
|
return left.path < right.path ? -1 : 1;
|
|
382
427
|
return 0;
|
|
@@ -388,7 +433,7 @@ export function composeRuntime(options) {
|
|
|
388
433
|
if (reset === null || submission.reset < reset)
|
|
389
434
|
reset = submission.reset;
|
|
390
435
|
}
|
|
391
|
-
return { attentionRequired, reset };
|
|
436
|
+
return { attentionRequired, unsent: unsent(), reset };
|
|
392
437
|
}
|
|
393
438
|
function documentsIn(locator) {
|
|
394
439
|
return replicas.get(locator)?.count ?? 0;
|
|
@@ -461,7 +506,7 @@ export function composeRuntime(options) {
|
|
|
461
506
|
const governed = governs(reach, room);
|
|
462
507
|
const attached = [
|
|
463
508
|
...(governed.collection ? [modelOf(room.locator)] : []),
|
|
464
|
-
...(governed.selection ? [
|
|
509
|
+
...(governed.selection ? [builtIn] : []),
|
|
465
510
|
];
|
|
466
511
|
return {
|
|
467
512
|
locator: room.locator,
|
|
@@ -477,11 +522,26 @@ export function composeRuntime(options) {
|
|
|
477
522
|
const value = listed();
|
|
478
523
|
for (const listener of roomListeners)
|
|
479
524
|
listener(value);
|
|
525
|
+
emitKnownRooms();
|
|
526
|
+
}
|
|
527
|
+
function emitKnownRooms() {
|
|
528
|
+
conversations.learned();
|
|
529
|
+
if (unlistedRooms.size === 0)
|
|
530
|
+
return;
|
|
531
|
+
if (!roomsListed && rooms.size === 0 && refusal === null && !status.providerUnreachable)
|
|
532
|
+
return;
|
|
533
|
+
const value = listed();
|
|
534
|
+
for (const listener of [...unlistedRooms]) {
|
|
535
|
+
unlistedRooms.delete(listener);
|
|
536
|
+
roomListeners.add(listener);
|
|
537
|
+
listener(value);
|
|
538
|
+
}
|
|
480
539
|
}
|
|
481
540
|
function emitRefusal() {
|
|
482
541
|
queueReadiness();
|
|
483
542
|
for (const listener of refusalListeners)
|
|
484
543
|
listener(refusal);
|
|
544
|
+
emitKnownRooms();
|
|
485
545
|
}
|
|
486
546
|
function emitStorageLoss() {
|
|
487
547
|
queueReadiness();
|
|
@@ -502,6 +562,7 @@ export function composeRuntime(options) {
|
|
|
502
562
|
if (closed)
|
|
503
563
|
return;
|
|
504
564
|
queueReadiness();
|
|
565
|
+
emitKnownRooms();
|
|
505
566
|
const next = status.state === 'synced' && !conversations.current()
|
|
506
567
|
? { ...status, state: 'syncing' }
|
|
507
568
|
: status;
|
|
@@ -515,9 +576,6 @@ export function composeRuntime(options) {
|
|
|
515
576
|
if (localSaves)
|
|
516
577
|
return Object.freeze({ batch, state: 'local' });
|
|
517
578
|
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
579
|
return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
|
|
522
580
|
}
|
|
523
581
|
function entriesOf(locator) {
|
|
@@ -532,23 +590,29 @@ export function composeRuntime(options) {
|
|
|
532
590
|
}
|
|
533
591
|
function applyChangeSet(set) {
|
|
534
592
|
liveQueries.touched(set);
|
|
535
|
-
searchIndex.changed(set);
|
|
536
593
|
}
|
|
537
594
|
function unpublished(error) {
|
|
538
595
|
journal.record(`publication failed ${error instanceof Error ? error.name : 'unknown'}`);
|
|
539
596
|
}
|
|
597
|
+
function indexed(locator) {
|
|
598
|
+
return replicas.get(locator)?.indexed === true;
|
|
599
|
+
}
|
|
540
600
|
async function prepareReplica(locator) {
|
|
541
601
|
for (;;) {
|
|
542
602
|
const held = replicas.get(locator);
|
|
543
|
-
if (held === undefined || closed ||
|
|
603
|
+
if (held === undefined || closed || held.indexed)
|
|
544
604
|
return;
|
|
545
605
|
const key = held.record.key;
|
|
546
|
-
const done = await exclusive(async () =>
|
|
547
|
-
|
|
606
|
+
const done = await exclusive(async () => {
|
|
607
|
+
const local = await openLocal();
|
|
608
|
+
return timed(deriveBudget, () => local.rederive(key, composed(held.record), deriveBudget.bytes, DERIVE_LONGEST_MS, DERIVE_FLOOR));
|
|
609
|
+
});
|
|
610
|
+
const now = replicas.get(locator);
|
|
611
|
+
if (now?.record.key !== key)
|
|
548
612
|
continue;
|
|
549
|
-
if (!done)
|
|
613
|
+
if (!done.done)
|
|
550
614
|
continue;
|
|
551
|
-
|
|
615
|
+
replicas.set(locator, { ...now, indexed: true });
|
|
552
616
|
publishChangeSet(changeSet(locator, { documents: null }));
|
|
553
617
|
liveQueries.recover();
|
|
554
618
|
queueReadiness();
|
|
@@ -556,11 +620,11 @@ export function composeRuntime(options) {
|
|
|
556
620
|
}
|
|
557
621
|
}
|
|
558
622
|
function unprepared() {
|
|
559
|
-
return [...unpreparedDemand, ...replicas.keys()].find((locator) => !
|
|
623
|
+
return [...unpreparedDemand, ...replicas.keys()].find((locator) => !indexed(locator) && !unpreparable.has(locator));
|
|
560
624
|
}
|
|
561
|
-
function
|
|
562
|
-
|
|
563
|
-
journal.record(`
|
|
625
|
+
function unpreparedBy(locator, error) {
|
|
626
|
+
unpreparable.add(locator);
|
|
627
|
+
journal.record(`preparation failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
|
|
564
628
|
queueReadiness();
|
|
565
629
|
}
|
|
566
630
|
function prepare(locators = []) {
|
|
@@ -576,7 +640,7 @@ export function composeRuntime(options) {
|
|
|
576
640
|
if (closed)
|
|
577
641
|
return;
|
|
578
642
|
unpreparedDemand.delete(waiting);
|
|
579
|
-
await prepareReplica(waiting).catch((error) =>
|
|
643
|
+
await prepareReplica(waiting).catch((error) => unpreparedBy(waiting, error));
|
|
580
644
|
}
|
|
581
645
|
}
|
|
582
646
|
finally {
|
|
@@ -585,7 +649,11 @@ export function composeRuntime(options) {
|
|
|
585
649
|
})();
|
|
586
650
|
}
|
|
587
651
|
function roomsOfReach(reach) {
|
|
588
|
-
return new Set([
|
|
652
|
+
return new Set([
|
|
653
|
+
...reach.rooms,
|
|
654
|
+
...reach.documents.map(roomOfKey),
|
|
655
|
+
...reach.areas.flatMap((area) => (area.types.length === 0 ? [] : [area.room])),
|
|
656
|
+
]);
|
|
589
657
|
}
|
|
590
658
|
function reachedRooms(plan) {
|
|
591
659
|
const reached = plan.reach === null ? null : roomsOfReach(plan.reach);
|
|
@@ -617,14 +685,14 @@ export function composeRuntime(options) {
|
|
|
617
685
|
pending.push({ room: locator, awaits: 'acquisition' });
|
|
618
686
|
continue;
|
|
619
687
|
}
|
|
620
|
-
if (!
|
|
688
|
+
if (!replica.indexed) {
|
|
621
689
|
pending.push({ room: locator, awaits: 'preparation' });
|
|
622
690
|
continue;
|
|
623
691
|
}
|
|
624
692
|
coverage.push({
|
|
625
693
|
room: locator,
|
|
626
694
|
generation: replica.record.transfer,
|
|
627
|
-
current: !(catching.has(locator) ||
|
|
695
|
+
current: !(catching.has(locator) || transfers.has(locator)),
|
|
628
696
|
});
|
|
629
697
|
read.push({ locator, key: replica.record.key });
|
|
630
698
|
for (const batch of replica.queue)
|
|
@@ -650,9 +718,7 @@ export function composeRuntime(options) {
|
|
|
650
718
|
return { listed: true, rooms: named.filter((room) => rooms.has(room)) };
|
|
651
719
|
return { listed: named.every((room) => replicas.has(room)), rooms: named };
|
|
652
720
|
}
|
|
653
|
-
function roomState(locator
|
|
654
|
-
if (sourceRefusals.has(locator))
|
|
655
|
-
return 'refused';
|
|
721
|
+
function roomState(locator) {
|
|
656
722
|
const listedRoom = discovered.get(locator);
|
|
657
723
|
if (roomsListed &&
|
|
658
724
|
(listedRoom === undefined ||
|
|
@@ -661,27 +727,17 @@ export function composeRuntime(options) {
|
|
|
661
727
|
return 'unadmitted';
|
|
662
728
|
if (!replicas.has(locator))
|
|
663
729
|
return 'acquiring';
|
|
664
|
-
if (!
|
|
730
|
+
if (!indexed(locator))
|
|
665
731
|
return 'preparing';
|
|
666
|
-
if (search && !searchIndex.rooms().has(locator))
|
|
667
|
-
return 'indexing';
|
|
668
732
|
return 'complete';
|
|
669
733
|
}
|
|
670
|
-
function conversationCoverage(reach
|
|
734
|
+
function conversationCoverage(reach) {
|
|
671
735
|
const declared = reach === null || reach.conversations;
|
|
672
|
-
if (!declared || options.adapter.conversations === undefined
|
|
736
|
+
if (!declared || options.adapter.conversations === undefined)
|
|
673
737
|
return 'undeclared';
|
|
674
738
|
if (!conversations.known() || !conversations.current())
|
|
675
739
|
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';
|
|
740
|
+
return 'complete';
|
|
685
741
|
}
|
|
686
742
|
function connectionOf() {
|
|
687
743
|
if (socketOpen)
|
|
@@ -692,58 +748,79 @@ export function composeRuntime(options) {
|
|
|
692
748
|
? 'connected'
|
|
693
749
|
: 'connecting';
|
|
694
750
|
}
|
|
695
|
-
function
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
}
|
|
751
|
+
function bytesOf(room, underway) {
|
|
752
|
+
const transfer = transfers.get(room);
|
|
753
|
+
if (transfer !== undefined) {
|
|
754
|
+
const owed = (unindexed.get(room) ?? []).reduce((sum, part) => sum + part.bytes, 0);
|
|
755
|
+
return {
|
|
756
|
+
done: transfer.done.bytes - Math.ceil(owed),
|
|
757
|
+
total: Math.max(arrivals.get(room) ?? 0, transfer.done.bytes),
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
const held = underway ? NOTHING_HELD : (replicas.get(room)?.record.inventory ?? NOTHING_HELD);
|
|
761
|
+
return { done: held.bytes, total: arrivals.get(room) ?? held.bytes };
|
|
762
|
+
}
|
|
763
|
+
function installationOf(coverage) {
|
|
764
|
+
const rooms = { done: 0, total: 0 };
|
|
765
|
+
const bytes = { done: 0, total: 0 };
|
|
766
|
+
let installing = 0;
|
|
767
|
+
let downloading = false;
|
|
768
|
+
for (const { room, state } of coverage) {
|
|
769
|
+
if (state === 'unadmitted')
|
|
770
|
+
continue;
|
|
771
|
+
rooms.total += 1;
|
|
772
|
+
if (state === 'complete')
|
|
773
|
+
rooms.done += 1;
|
|
774
|
+
const underway = state === 'acquiring' || (installs.has(room) && state !== 'complete');
|
|
775
|
+
if (underway)
|
|
776
|
+
installing += 1;
|
|
777
|
+
if (underway && (streaming.has(room) || !arrivals.has(room)))
|
|
778
|
+
downloading = true;
|
|
779
|
+
const now = bytesOf(room, underway);
|
|
780
|
+
const earlier = underway ? startedOver.get(room) : undefined;
|
|
781
|
+
bytes.done += Math.max(now.done, earlier?.done ?? 0);
|
|
782
|
+
bytes.total += Math.max(now.total, earlier?.total ?? 0);
|
|
783
|
+
}
|
|
784
|
+
const step = installing === 0
|
|
785
|
+
? null
|
|
786
|
+
: downloading
|
|
787
|
+
? 'downloading'
|
|
788
|
+
: bytes.done < bytes.total
|
|
789
|
+
? 'writing'
|
|
790
|
+
: 'replaying';
|
|
791
|
+
return { step, rooms, bytes };
|
|
710
792
|
}
|
|
711
793
|
function readinessFor(port, reach) {
|
|
712
794
|
const required = requiredRooms(reach);
|
|
713
795
|
const search = port.search === 'required';
|
|
714
|
-
const coverage = required.rooms.map((room) => ({ room, state: roomState(room
|
|
715
|
-
const
|
|
716
|
-
.filter((room) => room.state ===
|
|
796
|
+
const coverage = required.rooms.map((room) => ({ room, state: roomState(room) }));
|
|
797
|
+
const preparing = coverage
|
|
798
|
+
.filter((room) => room.state === 'preparing' && !unpreparable.has(room.room))
|
|
717
799
|
.map(({ room }) => room);
|
|
718
|
-
const preparing = building('preparing');
|
|
719
800
|
if (preparing.length > 0)
|
|
720
801
|
prepare(preparing);
|
|
721
|
-
const indexing = building('indexing');
|
|
722
|
-
if (indexing.length > 0)
|
|
723
|
-
index(indexing, []);
|
|
724
802
|
return readinessOf({
|
|
725
803
|
code: port.code,
|
|
726
804
|
coverage: {
|
|
727
805
|
listed: required.listed,
|
|
728
806
|
rooms: coverage,
|
|
729
|
-
conversations: conversationCoverage(reach
|
|
807
|
+
conversations: conversationCoverage(reach),
|
|
730
808
|
search: search ? 'required' : 'unused',
|
|
731
809
|
},
|
|
732
810
|
persistence,
|
|
733
811
|
connection: connectionOf(),
|
|
734
812
|
house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
? capacity
|
|
738
|
-
: port.capacity,
|
|
739
|
-
refused: refusedIn(required.rooms),
|
|
813
|
+
space: full ? 'full' : port.space,
|
|
814
|
+
installation: installationOf(coverage),
|
|
740
815
|
lost: storageLoss !== null,
|
|
741
|
-
established: targets[port.target]?.established === true
|
|
816
|
+
established: targets[port.target]?.established === true ||
|
|
817
|
+
(usableWhenHeld.includes(port.target) && required.rooms.some((room) => reopened.has(room))),
|
|
742
818
|
});
|
|
743
819
|
}
|
|
744
820
|
function establish(name) {
|
|
745
821
|
targets = { ...targets, [name]: { established: true } };
|
|
746
822
|
queueReadiness();
|
|
823
|
+
probeEachMinute();
|
|
747
824
|
void durably((local) => local.fact('targets', targets)).catch(() => {
|
|
748
825
|
journal.record(`target record failed ${name}`);
|
|
749
826
|
});
|
|
@@ -781,87 +858,52 @@ export function composeRuntime(options) {
|
|
|
781
858
|
readinessWatches.delete(watch);
|
|
782
859
|
};
|
|
783
860
|
}
|
|
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 ?? '-'}`);
|
|
861
|
+
function filled() {
|
|
862
|
+
if (full || closed)
|
|
863
|
+
return;
|
|
864
|
+
full = true;
|
|
865
|
+
journal.record('space full');
|
|
803
866
|
queueReadiness();
|
|
804
|
-
|
|
867
|
+
probeEachMinute();
|
|
868
|
+
}
|
|
869
|
+
function installedTarget() {
|
|
870
|
+
return ((usableWhenHeld.length > 0 && reopened.size > 0) ||
|
|
871
|
+
Object.values(targets).some((target) => target.established));
|
|
872
|
+
}
|
|
873
|
+
function probeEachMinute() {
|
|
874
|
+
if (!full || probing !== null || closed || !installedTarget())
|
|
805
875
|
return;
|
|
806
|
-
// A
|
|
807
|
-
|
|
808
|
-
void
|
|
876
|
+
// 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.
|
|
877
|
+
probing = setInterval(() => {
|
|
878
|
+
void probed();
|
|
809
879
|
}, 60_000);
|
|
810
880
|
}
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
resume();
|
|
881
|
+
function probed() {
|
|
882
|
+
testing ??= database.ask({ kind: 'space.probe' }).then(() => {
|
|
883
|
+
testing = null;
|
|
884
|
+
spaceFreed();
|
|
885
|
+
return true;
|
|
886
|
+
}, () => {
|
|
887
|
+
testing = null;
|
|
888
|
+
return false;
|
|
889
|
+
});
|
|
890
|
+
return testing;
|
|
822
891
|
}
|
|
823
|
-
function
|
|
824
|
-
if (
|
|
825
|
-
clearInterval(rechecking);
|
|
826
|
-
rechecking = null;
|
|
827
|
-
if (paused.size === 0 && capacity === null)
|
|
892
|
+
function spaceFreed() {
|
|
893
|
+
if (!full)
|
|
828
894
|
return;
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
895
|
+
full = false;
|
|
896
|
+
if (probing !== null)
|
|
897
|
+
clearInterval(probing);
|
|
898
|
+
probing = null;
|
|
899
|
+
unpreparable.clear();
|
|
900
|
+
journal.record('space freed');
|
|
832
901
|
queueReadiness();
|
|
833
902
|
if (!closed && authorization !== undefined)
|
|
834
903
|
void sync('unstreamed');
|
|
835
904
|
}
|
|
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
905
|
function seedRoomHeld(room, version) {
|
|
864
|
-
return
|
|
906
|
+
return indexed(room) && reachedBy(room, version);
|
|
865
907
|
}
|
|
866
908
|
function seedHeld(at) {
|
|
867
909
|
return Object.entries(at).every(([room, version]) => (roomsListed && !rooms.has(room)) || seedRoomHeld(room, version));
|
|
@@ -982,83 +1024,12 @@ export function composeRuntime(options) {
|
|
|
982
1024
|
const held = answered.cover;
|
|
983
1025
|
return { ...answered.rows, coverage: held.coverage };
|
|
984
1026
|
}
|
|
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
1027
|
function publishChangeSet(set) {
|
|
1061
1028
|
applyChangeSet(set);
|
|
1029
|
+
for (const { locator, path } of set.documents) {
|
|
1030
|
+
if (path !== null)
|
|
1031
|
+
attachedFiles.get(locator)?.delete(path);
|
|
1032
|
+
}
|
|
1062
1033
|
if (set.rooms.length > 0 ||
|
|
1063
1034
|
[...new Set(set.documents.map(({ locator }) => locator))].some((locator) => rooms.has(locator) && counted.get(locator) !== documentsIn(locator)))
|
|
1064
1035
|
emitRooms();
|
|
@@ -1077,6 +1048,8 @@ export function composeRuntime(options) {
|
|
|
1077
1048
|
}
|
|
1078
1049
|
async function refuse(reason, provider = null) {
|
|
1079
1050
|
discoveryPaused = true;
|
|
1051
|
+
inFlight.abort();
|
|
1052
|
+
inFlight = new AbortController();
|
|
1080
1053
|
try {
|
|
1081
1054
|
if (reason === 'revoked-authorization') {
|
|
1082
1055
|
await exclusive(async () => {
|
|
@@ -1084,27 +1057,24 @@ export function composeRuntime(options) {
|
|
|
1084
1057
|
const records = [...replicas].map(([locator, held]) => ({
|
|
1085
1058
|
locator,
|
|
1086
1059
|
held,
|
|
1087
|
-
record: { ...held.record, model: revoked
|
|
1060
|
+
record: { ...held.record, model: revoked },
|
|
1088
1061
|
}));
|
|
1062
|
+
let answer = null;
|
|
1089
1063
|
try {
|
|
1090
1064
|
const local = await openLocal();
|
|
1091
|
-
await local.commit({
|
|
1065
|
+
answer = await local.commit({
|
|
1092
1066
|
rooms: records.map(({ locator, record }) => ({
|
|
1093
1067
|
locator,
|
|
1094
1068
|
key: record.key,
|
|
1095
1069
|
record: storedRecord(record),
|
|
1096
|
-
|
|
1070
|
+
composed: composedOf(composed(record)),
|
|
1097
1071
|
})),
|
|
1098
1072
|
});
|
|
1099
1073
|
}
|
|
1100
1074
|
catch {
|
|
1101
1075
|
journal.record('model revocation persistence failed');
|
|
1102
1076
|
}
|
|
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
|
-
}
|
|
1077
|
+
const keys = new Set([...models.keys(), ...rooms.keys()]);
|
|
1108
1078
|
for (const key of keys) {
|
|
1109
1079
|
invalidModels.add(key);
|
|
1110
1080
|
models.set(key, revoked);
|
|
@@ -1114,8 +1084,8 @@ export function composeRuntime(options) {
|
|
|
1114
1084
|
for (const { locator, held, record } of records) {
|
|
1115
1085
|
if (replicas.get(locator) !== held)
|
|
1116
1086
|
continue;
|
|
1117
|
-
|
|
1118
|
-
replicas.set(locator, { ...held, record });
|
|
1087
|
+
const indexed = answer === null ? held.indexed : committedRoom(locator, answer).indexed;
|
|
1088
|
+
replicas.set(locator, { ...held, record, indexed });
|
|
1119
1089
|
}
|
|
1120
1090
|
for (const locator of [...rooms.keys()]) {
|
|
1121
1091
|
const room = rooms.get(locator);
|
|
@@ -1159,12 +1129,12 @@ export function composeRuntime(options) {
|
|
|
1159
1129
|
? null
|
|
1160
1130
|
: {
|
|
1161
1131
|
roots: selection.roots,
|
|
1162
|
-
model: modelTypes(
|
|
1132
|
+
model: modelTypes(builtIn),
|
|
1163
1133
|
},
|
|
1164
1134
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
1165
1135
|
roomVersion: replicas.get(room.locator)?.record.roomVersion ?? null,
|
|
1166
1136
|
synchronizationAvailable,
|
|
1167
|
-
refusal: scopedRefusals.get(room.locator) ??
|
|
1137
|
+
refusal: scopedRefusals.get(room.locator) ?? null,
|
|
1168
1138
|
});
|
|
1169
1139
|
}
|
|
1170
1140
|
async function applyDiscovery(found, started, covered) {
|
|
@@ -1248,30 +1218,23 @@ export function composeRuntime(options) {
|
|
|
1248
1218
|
queuesWithoutReplica.delete(locator);
|
|
1249
1219
|
rooms.delete(locator);
|
|
1250
1220
|
roomDrafts.delete(locator);
|
|
1251
|
-
|
|
1221
|
+
attachedFiles.delete(locator);
|
|
1222
|
+
untransferred(locator);
|
|
1223
|
+
startedOver.delete(locator);
|
|
1252
1224
|
catching.delete(locator);
|
|
1253
1225
|
discovered.delete(locator);
|
|
1254
|
-
announced.delete(locator);
|
|
1255
1226
|
unreadable.delete(locator);
|
|
1256
1227
|
stale.delete(locator);
|
|
1257
1228
|
repairable.delete(locator);
|
|
1258
|
-
|
|
1259
|
-
paused.delete(locator);
|
|
1260
|
-
prepared.delete(locator);
|
|
1229
|
+
unpreparable.delete(locator);
|
|
1261
1230
|
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
|
-
}
|
|
1231
|
+
models.delete(locator);
|
|
1232
|
+
liveModels.delete(locator);
|
|
1233
|
+
invalidModels.delete(locator);
|
|
1270
1234
|
journal.record(`room lost ${locator}`);
|
|
1271
1235
|
}
|
|
1272
1236
|
if (fresh.length === 0)
|
|
1273
1237
|
return fresh;
|
|
1274
|
-
searchIndex.forget(fresh);
|
|
1275
1238
|
publishChangeSet(mergeChangeSets(removed.map(({ locator, drafts }) => changeSet(locator, { documents: null, room: true, queue: true, drafts }))));
|
|
1276
1239
|
liveQueries.recover();
|
|
1277
1240
|
moveTo({ ...status, ...attention() });
|
|
@@ -1315,7 +1278,14 @@ export function composeRuntime(options) {
|
|
|
1315
1278
|
});
|
|
1316
1279
|
await eraseFenced(fresh);
|
|
1317
1280
|
}
|
|
1318
|
-
function
|
|
1281
|
+
function retyped(persisted) {
|
|
1282
|
+
if (persisted.status === 'unavailable')
|
|
1283
|
+
return persisted;
|
|
1284
|
+
const { state } = attachedModel({ kind: 'model', snapshot: persisted.snapshot }, NO_MODEL, roomTypes);
|
|
1285
|
+
return state.status === 'unavailable' ? state : { ...persisted, types: state.types };
|
|
1286
|
+
}
|
|
1287
|
+
function restored(key, held) {
|
|
1288
|
+
const persisted = retyped(held);
|
|
1319
1289
|
const prior = models.get(key);
|
|
1320
1290
|
if (prior !== undefined &&
|
|
1321
1291
|
prior.status !== 'unavailable' &&
|
|
@@ -1346,8 +1316,9 @@ export function composeRuntime(options) {
|
|
|
1346
1316
|
const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
|
|
1347
1317
|
targets = held.targets;
|
|
1348
1318
|
transfers.clear();
|
|
1349
|
-
|
|
1350
|
-
|
|
1319
|
+
arrivals.clear();
|
|
1320
|
+
streaming.clear();
|
|
1321
|
+
startedOver.clear();
|
|
1351
1322
|
limits = held.providerLimits;
|
|
1352
1323
|
capabilities = held.capabilities;
|
|
1353
1324
|
member = held.member;
|
|
@@ -1357,7 +1328,7 @@ export function composeRuntime(options) {
|
|
|
1357
1328
|
presentations.clear();
|
|
1358
1329
|
for (const record of held.presentation)
|
|
1359
1330
|
presentations.set(record.app, record.value);
|
|
1360
|
-
conversations.hydrate(heldConversations, chatDrafts);
|
|
1331
|
+
conversations.hydrate(heldConversations, chatDrafts, held.conversationList);
|
|
1361
1332
|
pendings.clear();
|
|
1362
1333
|
for (const queue of queues) {
|
|
1363
1334
|
for (const { batch, state } of queue.pending) {
|
|
@@ -1365,7 +1336,7 @@ export function composeRuntime(options) {
|
|
|
1365
1336
|
}
|
|
1366
1337
|
}
|
|
1367
1338
|
replicas.clear();
|
|
1368
|
-
|
|
1339
|
+
reopened.clear();
|
|
1369
1340
|
queuesWithoutReplica.clear();
|
|
1370
1341
|
for (const [locator, queue] of queuedBatches) {
|
|
1371
1342
|
if (!held.rooms.has(locator))
|
|
@@ -1374,39 +1345,40 @@ export function composeRuntime(options) {
|
|
|
1374
1345
|
records.forEach((stored, index) => {
|
|
1375
1346
|
if (fences.has(stored.locator))
|
|
1376
1347
|
return;
|
|
1377
|
-
const
|
|
1378
|
-
|
|
1379
|
-
const record = { ...stored, model, selectionModel };
|
|
1348
|
+
const record = { ...stored, model: restored(stored.locator, stored.model) };
|
|
1349
|
+
reopened.add(record.locator);
|
|
1380
1350
|
replicas.set(record.locator, {
|
|
1381
1351
|
record,
|
|
1382
1352
|
queue: queuedBatches.get(record.locator) ?? [],
|
|
1383
1353
|
overlaid: held.overlaid.get(record.key) ?? new Set(),
|
|
1384
1354
|
count: counts[index] ?? 0,
|
|
1385
1355
|
confirmed: 0,
|
|
1356
|
+
indexed: !held.unindexed.has(record.key),
|
|
1386
1357
|
});
|
|
1387
|
-
if (!held.unprepared.has(record.key))
|
|
1388
|
-
prepared.add(record.locator);
|
|
1389
1358
|
const existing = rooms.get(record.locator);
|
|
1390
1359
|
if (existing === undefined)
|
|
1391
|
-
list(record,
|
|
1360
|
+
list(record, record.roomVersion !== null);
|
|
1392
1361
|
else {
|
|
1393
1362
|
rooms.set(record.locator, {
|
|
1394
1363
|
...existing,
|
|
1395
1364
|
roomVersion: record.roomVersion,
|
|
1396
1365
|
operations: record.operations,
|
|
1397
1366
|
replica: record.replica,
|
|
1398
|
-
model: modelTypes(model),
|
|
1367
|
+
model: modelTypes(record.model),
|
|
1399
1368
|
typedSelection: record.typedSelection === null
|
|
1400
1369
|
? null
|
|
1401
|
-
: {
|
|
1402
|
-
roots: record.typedSelection.roots,
|
|
1403
|
-
model: modelTypes(selectionModel),
|
|
1404
|
-
},
|
|
1370
|
+
: { roots: record.typedSelection.roots, model: modelTypes(builtIn) },
|
|
1405
1371
|
});
|
|
1406
1372
|
}
|
|
1407
1373
|
});
|
|
1408
|
-
|
|
1409
|
-
|
|
1374
|
+
try {
|
|
1375
|
+
for (const key of held.orphans)
|
|
1376
|
+
await opened.release(key);
|
|
1377
|
+
}
|
|
1378
|
+
catch (error) {
|
|
1379
|
+
if (!refusedForSpace(error))
|
|
1380
|
+
throw error;
|
|
1381
|
+
}
|
|
1410
1382
|
if (held.erasing || held.orphans.length > 0)
|
|
1411
1383
|
queueMicrotask(sweep);
|
|
1412
1384
|
return {
|
|
@@ -1431,8 +1403,7 @@ export function composeRuntime(options) {
|
|
|
1431
1403
|
async function open() {
|
|
1432
1404
|
if (closed)
|
|
1433
1405
|
throw new Error('runtime closed');
|
|
1434
|
-
const opened =
|
|
1435
|
-
store = opened;
|
|
1406
|
+
const opened = openRuntimeStore(isFenced, database);
|
|
1436
1407
|
const interrupted = await opened.fences();
|
|
1437
1408
|
for (const locator of interrupted) {
|
|
1438
1409
|
fences.set(locator, { at: discoveries, erased: false });
|
|
@@ -1478,10 +1449,29 @@ export function composeRuntime(options) {
|
|
|
1478
1449
|
return opening;
|
|
1479
1450
|
}
|
|
1480
1451
|
function exclusive(work) {
|
|
1452
|
+
gathering = null;
|
|
1481
1453
|
const next = mutations.then(work, work);
|
|
1482
1454
|
mutations = next.then(() => undefined, () => undefined);
|
|
1483
1455
|
return next;
|
|
1484
1456
|
}
|
|
1457
|
+
function gathered(key, input, merge, work) {
|
|
1458
|
+
const tail = gathering;
|
|
1459
|
+
if (tail !== null && tail.key === key) {
|
|
1460
|
+
const merged = merge(tail.input, input);
|
|
1461
|
+
if (merged !== null) {
|
|
1462
|
+
tail.input = merged;
|
|
1463
|
+
return tail.done;
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
const entry = { key, input, done: Promise.resolve() };
|
|
1467
|
+
entry.done = exclusive(() => {
|
|
1468
|
+
if (gathering === entry)
|
|
1469
|
+
gathering = null;
|
|
1470
|
+
return work(entry.input);
|
|
1471
|
+
});
|
|
1472
|
+
gathering = entry;
|
|
1473
|
+
return entry.done;
|
|
1474
|
+
}
|
|
1485
1475
|
function withdrawable(locator) {
|
|
1486
1476
|
return (batch) => {
|
|
1487
1477
|
const state = pendings.get(pendingKey(locator, batch.id));
|
|
@@ -1563,26 +1553,14 @@ export function composeRuntime(options) {
|
|
|
1563
1553
|
},
|
|
1564
1554
|
};
|
|
1565
1555
|
}
|
|
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))
|
|
1556
|
+
function committedRoom(locator, answer) {
|
|
1557
|
+
const indexed = answer.indexed[locator] ?? replicas.get(locator)?.indexed ?? false;
|
|
1558
|
+
if (!indexed)
|
|
1581
1559
|
prepare([locator]);
|
|
1582
|
-
return { count: answer.counts[locator] ?? replicas.get(locator)?.count ?? 0 };
|
|
1560
|
+
return { count: answer.counts[locator] ?? replicas.get(locator)?.count ?? 0, indexed };
|
|
1583
1561
|
}
|
|
1584
1562
|
function replayed(record, confirmedLayer, queue) {
|
|
1585
|
-
return stepped(replaying(confirmedLayer, queue, record.locator,
|
|
1563
|
+
return stepped(replaying(confirmedLayer, queue, record.locator, composed(record)));
|
|
1586
1564
|
}
|
|
1587
1565
|
async function overlayDiff(record, before, visible, touched) {
|
|
1588
1566
|
const kept = before !== undefined && before.record.key === record.key
|
|
@@ -1602,8 +1580,6 @@ export function composeRuntime(options) {
|
|
|
1602
1580
|
function unpairedIn(locator, batch) {
|
|
1603
1581
|
if (!queueIn(locator).some((held) => held.id === batch.id))
|
|
1604
1582
|
return false;
|
|
1605
|
-
if (pendings.get(pendingKey(locator, batch.id))?.accepted !== undefined)
|
|
1606
|
-
return false;
|
|
1607
1583
|
return !leavingIn(batch, locator) || withdrawable(locator)(batch);
|
|
1608
1584
|
}
|
|
1609
1585
|
async function together(local, settlements) {
|
|
@@ -1645,7 +1621,7 @@ export function composeRuntime(options) {
|
|
|
1645
1621
|
const confirmedLayer = overrides === undefined
|
|
1646
1622
|
? read
|
|
1647
1623
|
: await stepped(slicingWith(read, overrides.set, overrides.removed));
|
|
1648
|
-
const composition =
|
|
1624
|
+
const composition = composed(record);
|
|
1649
1625
|
const { withdrawn, kept } = await stepped(standing(confirmedLayer, queue, withdrawable(locator), localSaves, locator, composition));
|
|
1650
1626
|
const founded = kept.filter((batch) => !queue.includes(batch));
|
|
1651
1627
|
const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
|
|
@@ -1688,6 +1664,7 @@ export function composeRuntime(options) {
|
|
|
1688
1664
|
key: record.key,
|
|
1689
1665
|
record: storedRecord(record),
|
|
1690
1666
|
roots: record.typedSelection?.roots ?? [],
|
|
1667
|
+
composed: composedOf(composition),
|
|
1691
1668
|
...settling.commit,
|
|
1692
1669
|
...(overrides === undefined
|
|
1693
1670
|
? {}
|
|
@@ -1729,14 +1706,14 @@ export function composeRuntime(options) {
|
|
|
1729
1706
|
const confirmedChanged = settling.commit?.published !== undefined ||
|
|
1730
1707
|
settling.commit?.refreshed === true ||
|
|
1731
1708
|
overrides !== undefined;
|
|
1732
|
-
|
|
1733
|
-
const { count } = committedRoom(locator, record, answer);
|
|
1709
|
+
const { count, indexed } = committedRoom(locator, answer);
|
|
1734
1710
|
replicas.set(locator, {
|
|
1735
1711
|
record,
|
|
1736
1712
|
queue: kept,
|
|
1737
1713
|
overlaid: touched,
|
|
1738
1714
|
count,
|
|
1739
1715
|
confirmed: (before?.confirmed ?? 0) + (confirmedChanged ? 1 : 0),
|
|
1716
|
+
indexed,
|
|
1740
1717
|
});
|
|
1741
1718
|
return mergeChangeSets([
|
|
1742
1719
|
changeSet(locator, {
|
|
@@ -1751,103 +1728,96 @@ export function composeRuntime(options) {
|
|
|
1751
1728
|
};
|
|
1752
1729
|
}
|
|
1753
1730
|
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
|
-
}
|
|
1731
|
+
const model = record.model;
|
|
1732
|
+
models.set(locator, model);
|
|
1733
|
+
invalidModels.delete(locator);
|
|
1734
|
+
if (model.status === 'current')
|
|
1735
|
+
liveModels.set(locator, JSON.stringify(model.snapshot));
|
|
1736
|
+
else if (model.status === 'unavailable')
|
|
1737
|
+
liveModels.delete(locator);
|
|
1766
1738
|
}
|
|
1767
1739
|
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
|
-
};
|
|
1740
|
+
const answer = answers?.collection;
|
|
1741
|
+
if (answer === undefined)
|
|
1742
|
+
return held;
|
|
1743
|
+
const { state, refused } = attachedModel(answer, held.collection, roomTypes);
|
|
1744
|
+
if (refused !== null) {
|
|
1745
|
+
const where = refused.location === undefined ? '' : ` ${refused.location}`;
|
|
1746
|
+
journal.record(`model invalid ${locator} collection ${refused.identity} ${refused.rule}${where}`);
|
|
1747
|
+
}
|
|
1748
|
+
models.set(locator, state);
|
|
1749
|
+
return { collection: state };
|
|
1783
1750
|
}
|
|
1784
|
-
function publish(local, room,
|
|
1785
|
-
|
|
1751
|
+
function publish(local, room, reached, landing, live) {
|
|
1752
|
+
const landed = exclusive(async () => {
|
|
1786
1753
|
if (!live()) {
|
|
1787
1754
|
caughtUpTo(room.locator);
|
|
1788
1755
|
return true;
|
|
1789
1756
|
}
|
|
1790
1757
|
const prior = replicas.get(room.locator);
|
|
1791
|
-
if (landing.
|
|
1792
|
-
(
|
|
1793
|
-
|
|
1758
|
+
if (!stands(room.locator, landing.from)) {
|
|
1759
|
+
if (landing.kind === 'refreshed') {
|
|
1760
|
+
await local.unrefresh(reached.key);
|
|
1761
|
+
return false;
|
|
1762
|
+
}
|
|
1763
|
+
caughtUpTo(room.locator);
|
|
1764
|
+
await abandoned(local, room.locator);
|
|
1794
1765
|
return false;
|
|
1795
1766
|
}
|
|
1767
|
+
const record = landing.kind === 'refreshed' && prior !== undefined
|
|
1768
|
+
? {
|
|
1769
|
+
...prior.record,
|
|
1770
|
+
roomVersion: reached.roomVersion,
|
|
1771
|
+
...(landing.remodelled ? { model: reached.model } : {}),
|
|
1772
|
+
}
|
|
1773
|
+
: reached;
|
|
1796
1774
|
adoptModels(room.locator, record);
|
|
1797
1775
|
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
1776
|
let set;
|
|
1804
1777
|
try {
|
|
1805
|
-
set = await settle(local, room.locator, prior, record, queue
|
|
1778
|
+
set = await settle(local, room.locator, prior, record, queue, landing.kind === 'refreshed'
|
|
1806
1779
|
? {
|
|
1807
1780
|
room: true,
|
|
1808
1781
|
view: 'refreshed',
|
|
1809
|
-
commit: { refreshed: true
|
|
1782
|
+
commit: { refreshed: true },
|
|
1810
1783
|
}
|
|
1811
1784
|
: {
|
|
1812
1785
|
room: true,
|
|
1813
1786
|
view: landing.refreshed ? 'refreshed' : 'confirmed',
|
|
1814
1787
|
commit: {
|
|
1815
1788
|
published: { from: prior?.record.key ?? null },
|
|
1816
|
-
composition: compositionOf(record),
|
|
1817
1789
|
...(landing.refreshed ? { refreshed: true } : {}),
|
|
1818
1790
|
},
|
|
1819
1791
|
});
|
|
1820
1792
|
}
|
|
1821
1793
|
catch (error) {
|
|
1822
|
-
if (!
|
|
1794
|
+
if (!refusedForSpace(error))
|
|
1823
1795
|
throw error;
|
|
1824
|
-
await exceededOn(room.locator, null);
|
|
1825
1796
|
caughtUpTo(room.locator);
|
|
1826
1797
|
return true;
|
|
1827
1798
|
}
|
|
1828
1799
|
if (landing.kind === 'bootstrapped') {
|
|
1829
|
-
|
|
1800
|
+
untransferred(room.locator);
|
|
1801
|
+
startedOver.delete(room.locator);
|
|
1830
1802
|
catching.delete(room.locator);
|
|
1831
|
-
|
|
1803
|
+
if (prior !== undefined && prior.record.key !== record.key)
|
|
1804
|
+
sweep();
|
|
1805
|
+
name(room.locator);
|
|
1832
1806
|
}
|
|
1833
|
-
for (const batch of accepted)
|
|
1834
|
-
pendings.delete(pendingKey(room.locator, batch.id));
|
|
1835
1807
|
stale.delete(room.locator);
|
|
1836
|
-
|
|
1808
|
+
unpreparable.delete(room.locator);
|
|
1837
1809
|
if (unreadable.delete(room.locator))
|
|
1838
1810
|
scopedRefusals.delete(room.locator);
|
|
1839
|
-
sourceRefusals.delete(room.locator);
|
|
1840
|
-
declarationRefusals.delete(room.locator);
|
|
1841
1811
|
list(room, true);
|
|
1842
1812
|
publishChangeSet(set);
|
|
1843
1813
|
await unpair(local);
|
|
1844
1814
|
moveTo({ ...status, ...attention() });
|
|
1845
|
-
|
|
1846
|
-
if (accepted.length > 0 && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
|
|
1815
|
+
if (landing.kind === 'bootstrapped' && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
|
|
1847
1816
|
submitLater(room.locator);
|
|
1848
1817
|
}
|
|
1849
1818
|
return true;
|
|
1850
1819
|
});
|
|
1820
|
+
return landing.kind === 'refreshed' ? landed : landed.finally(() => installed(room.locator));
|
|
1851
1821
|
}
|
|
1852
1822
|
async function savedRoom(locator) {
|
|
1853
1823
|
const local = await openLocal();
|
|
@@ -1872,12 +1842,7 @@ export function composeRuntime(options) {
|
|
|
1872
1842
|
return committed(locator, target, saved, new Set(saved.map(({ path }) => path)), writtenBy);
|
|
1873
1843
|
}
|
|
1874
1844
|
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
|
-
}
|
|
1845
|
+
const read = readSavedParts(named);
|
|
1881
1846
|
const target = await savedRoom(locator);
|
|
1882
1847
|
const saved = [];
|
|
1883
1848
|
const files = [];
|
|
@@ -1886,7 +1851,7 @@ export function composeRuntime(options) {
|
|
|
1886
1851
|
for (const { path, parts } of read) {
|
|
1887
1852
|
const file = parts.file === undefined
|
|
1888
1853
|
? undefined
|
|
1889
|
-
: await heldFile(parts.file.blob, binaryBase(visible, path));
|
|
1854
|
+
: await heldFile(parts.file.blob, parts.file.base === undefined ? binaryBase(visible, path) : parts.file.base);
|
|
1890
1855
|
if (parts.file !== undefined && file !== undefined) {
|
|
1891
1856
|
files.push({ key: file.localId, blob: parts.file.blob });
|
|
1892
1857
|
}
|
|
@@ -1921,13 +1886,14 @@ export function composeRuntime(options) {
|
|
|
1921
1886
|
const referring = removed.length === 0
|
|
1922
1887
|
? []
|
|
1923
1888
|
: (await local.referrers(record.key, 'confirmed', removed, CHECKED_LINKS)).map(({ path }) => path);
|
|
1924
|
-
const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...referring], moved);
|
|
1889
|
+
const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...paths.flatMap(ancestors), ...referring], moved);
|
|
1925
1890
|
const { visible } = await replayed(record, confirmedLayer, replica.queue);
|
|
1926
|
-
const composition =
|
|
1891
|
+
const composition = composed(record);
|
|
1927
1892
|
return {
|
|
1928
1893
|
record,
|
|
1929
1894
|
confirmed: confirmedLayer,
|
|
1930
1895
|
visible,
|
|
1896
|
+
folders: new Set(await local.folders(record.key, 'visible', paths)),
|
|
1931
1897
|
*visibleOver(queue) {
|
|
1932
1898
|
return (yield* replaying(confirmedLayer, queue, record.locator, composition)).visible;
|
|
1933
1899
|
},
|
|
@@ -1939,13 +1905,7 @@ export function composeRuntime(options) {
|
|
|
1939
1905
|
return false;
|
|
1940
1906
|
const local = await openLocal();
|
|
1941
1907
|
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)));
|
|
1908
|
+
return local.typedWithin(record.key, 'visible', roots, false);
|
|
1949
1909
|
}
|
|
1950
1910
|
function textDiffers(left, right) {
|
|
1951
1911
|
return ((left?.content ?? undefined) !== (right?.content ?? undefined) ||
|
|
@@ -1956,18 +1916,6 @@ export function composeRuntime(options) {
|
|
|
1956
1916
|
return undefined;
|
|
1957
1917
|
return entry.binary !== null ? null : entry.content;
|
|
1958
1918
|
}
|
|
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
1919
|
function lookupOf(local, key, view) {
|
|
1972
1920
|
return async (paths) => {
|
|
1973
1921
|
if (paths.length === 0)
|
|
@@ -1984,37 +1932,6 @@ export function composeRuntime(options) {
|
|
|
1984
1932
|
const read = parseFrontmatter(entry.content ?? '');
|
|
1985
1933
|
return { type: read.ok ? (read.frontmatter.type ?? 'any') : undefined, binary: false };
|
|
1986
1934
|
}
|
|
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
1935
|
function overriddenLookup(lookup, overrides) {
|
|
2019
1936
|
return async (paths) => {
|
|
2020
1937
|
const known = new Map(await lookup(paths.filter((path) => !overrides.has(path))));
|
|
@@ -2032,9 +1949,8 @@ export function composeRuntime(options) {
|
|
|
2032
1949
|
}
|
|
2033
1950
|
function validatorOf(record, confirmedLayer) {
|
|
2034
1951
|
return async (resulting, touched) => {
|
|
2035
|
-
const composition = compositionFor(record);
|
|
2036
1952
|
const local = await openLocal();
|
|
2037
|
-
const
|
|
1953
|
+
const changes = [];
|
|
2038
1954
|
const overrides = new Map();
|
|
2039
1955
|
const pace = paced();
|
|
2040
1956
|
for (const path of new Set([...confirmedLayer.held, ...touched])) {
|
|
@@ -2045,19 +1961,13 @@ export function composeRuntime(options) {
|
|
|
2045
1961
|
continue;
|
|
2046
1962
|
overrides.set(path, now);
|
|
2047
1963
|
if (textOf(was) !== textOf(now))
|
|
2048
|
-
|
|
1964
|
+
changes.push({ path, was: textOf(was), now: textOf(now) });
|
|
2049
1965
|
}
|
|
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);
|
|
1966
|
+
return validatedNeighbourhood(changes, compositionFor(record), {
|
|
1967
|
+
lookup: overriddenLookup(lookupOf(local, record.key, 'confirmed'), overrides),
|
|
1968
|
+
texts: textsOf(local, record.key, 'confirmed'),
|
|
1969
|
+
referrers: referrersOf(local, record.key, 'confirmed'),
|
|
1970
|
+
});
|
|
2061
1971
|
};
|
|
2062
1972
|
}
|
|
2063
1973
|
function textsOf(local, key, view) {
|
|
@@ -2081,6 +1991,7 @@ export function composeRuntime(options) {
|
|
|
2081
1991
|
operations: room.operations,
|
|
2082
1992
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
2083
1993
|
visible: saving.visible,
|
|
1994
|
+
folders: saving.folders,
|
|
2084
1995
|
queued: queuedDocuments(replica.queue),
|
|
2085
1996
|
unmodeled: await unmodeledIn(record),
|
|
2086
1997
|
validate: validatorOf(record, saving.confirmed),
|
|
@@ -2101,6 +2012,7 @@ export function composeRuntime(options) {
|
|
|
2101
2012
|
locator,
|
|
2102
2013
|
key: saving.record.key,
|
|
2103
2014
|
roots: saving.record.typedSelection?.roots ?? [],
|
|
2015
|
+
composed: composedOf(composed(saving.record)),
|
|
2104
2016
|
overlay: diff.overlay,
|
|
2105
2017
|
});
|
|
2106
2018
|
}
|
|
@@ -2132,7 +2044,7 @@ export function composeRuntime(options) {
|
|
|
2132
2044
|
}
|
|
2133
2045
|
const moves = saved.map(({ path, parts }) => ({ path, to: moveTarget(parts.move) }));
|
|
2134
2046
|
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,
|
|
2047
|
+
const changed = saved.flatMap(({ path, parts }) => changedParts(path, parts, sourceSaving.visible));
|
|
2136
2048
|
if (changed.length > 0)
|
|
2137
2049
|
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
2138
2050
|
const target = replicas.get(destination);
|
|
@@ -2244,6 +2156,8 @@ export function composeRuntime(options) {
|
|
|
2244
2156
|
submitLater(locator);
|
|
2245
2157
|
}
|
|
2246
2158
|
async function committed(locator, target, saved, consumed, writtenBy, files = []) {
|
|
2159
|
+
if (full)
|
|
2160
|
+
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
2247
2161
|
const into = movedInto(locator, saved);
|
|
2248
2162
|
if (into !== null) {
|
|
2249
2163
|
return committedMove(locator, into, target, saved, consumed, writtenBy, files);
|
|
@@ -2255,7 +2169,7 @@ export function composeRuntime(options) {
|
|
|
2255
2169
|
...saved.map(({ path }) => path),
|
|
2256
2170
|
...parts.flatMap(({ bases }) => bases.map(({ path }) => path)),
|
|
2257
2171
|
]);
|
|
2258
|
-
const changed = saved.flatMap(({ path, parts: held }) => changedParts(path,
|
|
2172
|
+
const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, held, saving.visible));
|
|
2259
2173
|
if (changed.length > 0)
|
|
2260
2174
|
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
2261
2175
|
const requeue = await stepped(requeuing(replica.queue, changes, saving.visible, withdrawable(locator), saving.visibleOver));
|
|
@@ -2283,6 +2197,7 @@ export function composeRuntime(options) {
|
|
|
2283
2197
|
providerLimits: limits,
|
|
2284
2198
|
handle: room.handle,
|
|
2285
2199
|
visible: saving.visible,
|
|
2200
|
+
folders: saving.folders,
|
|
2286
2201
|
changes,
|
|
2287
2202
|
bases: parts.flatMap(({ bases }) => bases),
|
|
2288
2203
|
queued: queuedDocuments(replica.queue),
|
|
@@ -2433,6 +2348,9 @@ export function composeRuntime(options) {
|
|
|
2433
2348
|
async function readHistory(locator, path, query = {}) {
|
|
2434
2349
|
if (closed || refusal !== null)
|
|
2435
2350
|
return UNCERTAIN;
|
|
2351
|
+
await firstDiscovery;
|
|
2352
|
+
if (!roomsNamed)
|
|
2353
|
+
return UNCERTAIN;
|
|
2436
2354
|
try {
|
|
2437
2355
|
await ensureAuthorization();
|
|
2438
2356
|
}
|
|
@@ -2522,14 +2440,12 @@ export function composeRuntime(options) {
|
|
|
2522
2440
|
return (await openLocal()).file(localId, change?.kind === 'binary' ? change.mediaType : '');
|
|
2523
2441
|
};
|
|
2524
2442
|
}
|
|
2525
|
-
function binaryRoom(request) {
|
|
2526
|
-
if (request.kind === 'path')
|
|
2527
|
-
return request.locator;
|
|
2528
|
-
return options.adapter.referrerScope(request.referrer)?.locator ?? null;
|
|
2529
|
-
}
|
|
2530
2443
|
async function transferBinary(request, transfer, answer) {
|
|
2531
2444
|
if (closed || refusal !== null)
|
|
2532
2445
|
return UNCERTAIN;
|
|
2446
|
+
await firstDiscovery;
|
|
2447
|
+
if (!roomsNamed)
|
|
2448
|
+
return UNCERTAIN;
|
|
2533
2449
|
try {
|
|
2534
2450
|
await ensureAuthorization();
|
|
2535
2451
|
}
|
|
@@ -2539,16 +2455,13 @@ export function composeRuntime(options) {
|
|
|
2539
2455
|
if (closed || authorization === undefined || refusal !== null)
|
|
2540
2456
|
return UNCERTAIN;
|
|
2541
2457
|
const since = fencings;
|
|
2542
|
-
journal.record(request.
|
|
2543
|
-
? `transfer binary ${request.locator} ${request.path}`
|
|
2544
|
-
: `transfer binary ${request.fileRef}`);
|
|
2458
|
+
journal.record(`transfer binary ${request.locator} ${request.path}`);
|
|
2545
2459
|
try {
|
|
2546
2460
|
const result = await transfer(authorization);
|
|
2547
2461
|
const settled = result;
|
|
2548
2462
|
journal.record(`outcome binary ${settled.kind}`);
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
return failure({ kind: 'room-not-readable', locator });
|
|
2463
|
+
if (fencedSince(request.locator, since))
|
|
2464
|
+
return failure({ kind: 'room-not-readable', locator: request.locator });
|
|
2552
2465
|
if (settled.kind === 'scoped-refusal') {
|
|
2553
2466
|
const room = replicas.get(settled.locator)?.record;
|
|
2554
2467
|
if (room !== undefined)
|
|
@@ -2563,23 +2476,27 @@ export function composeRuntime(options) {
|
|
|
2563
2476
|
return UNCERTAIN;
|
|
2564
2477
|
}
|
|
2565
2478
|
}
|
|
2479
|
+
function fetchBinary(request, name) {
|
|
2480
|
+
return transferBinary(request, (authorization) => keptFiles.fetch(name, () => options.adapter.binary(authorization, request)), binaryAnswer);
|
|
2481
|
+
}
|
|
2566
2482
|
async function readBinary(request, name) {
|
|
2567
2483
|
if (closed)
|
|
2568
2484
|
return UNCERTAIN;
|
|
2569
2485
|
const held = await keptFiles.find(name);
|
|
2570
2486
|
if (held !== null)
|
|
2571
2487
|
return held;
|
|
2572
|
-
return
|
|
2488
|
+
return fetchBinary(request, name);
|
|
2573
2489
|
}
|
|
2574
2490
|
async function binaryPath(locator, path, revision) {
|
|
2575
2491
|
if (revision !== undefined)
|
|
2576
2492
|
return { kind: 'path', locator, path, revision };
|
|
2577
2493
|
const replica = replicas.get(locator);
|
|
2578
|
-
if (replica === undefined)
|
|
2579
|
-
return null;
|
|
2580
2494
|
const local = await openLocal();
|
|
2581
|
-
const [held] = await local.entries(replica.record.key, 'visible', [path]);
|
|
2582
|
-
|
|
2495
|
+
const [held] = replica === undefined ? [] : await local.entries(replica.record.key, 'visible', [path]);
|
|
2496
|
+
const attached = held === undefined ? attachedFiles.get(locator)?.get(path) : undefined;
|
|
2497
|
+
if (attached !== undefined)
|
|
2498
|
+
return { kind: 'path', locator, path, revision: attached.revision };
|
|
2499
|
+
if (replica === undefined || held?.binary === undefined || held.binary === null)
|
|
2583
2500
|
return null;
|
|
2584
2501
|
if (held.binary.state === 'pending')
|
|
2585
2502
|
return { kind: 'pending', localId: held.binary.localId };
|
|
@@ -2595,7 +2512,7 @@ export function composeRuntime(options) {
|
|
|
2595
2512
|
const holds = (at) => {
|
|
2596
2513
|
const kept = confirmedLayer.binaries[at];
|
|
2597
2514
|
return (kept?.state === 'accepted' &&
|
|
2598
|
-
kept.
|
|
2515
|
+
kept.sha256 === record.sha256 &&
|
|
2599
2516
|
confirmedLayer.revisions[at] === record.revision);
|
|
2600
2517
|
};
|
|
2601
2518
|
let confirmedAt = path;
|
|
@@ -2609,58 +2526,93 @@ export function composeRuntime(options) {
|
|
|
2609
2526
|
locator,
|
|
2610
2527
|
path: confirmedAt,
|
|
2611
2528
|
revision: record.revision,
|
|
2612
|
-
fileRef: record.fileRef,
|
|
2613
2529
|
};
|
|
2614
2530
|
}
|
|
2615
|
-
async function
|
|
2531
|
+
async function askedBinary(locator, path, revision) {
|
|
2616
2532
|
await ready;
|
|
2617
2533
|
const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
|
|
2618
|
-
|
|
2534
|
+
return drafted !== undefined && drafted.localId === revision
|
|
2619
2535
|
? { kind: 'pending', localId: revision }
|
|
2620
|
-
:
|
|
2536
|
+
: binaryPath(locator, path, revision);
|
|
2537
|
+
}
|
|
2538
|
+
function pendingBinary(locator, path, localId) {
|
|
2539
|
+
const facts = queuedFacts(locator, path, localId);
|
|
2540
|
+
return facts === null
|
|
2541
|
+
? absentBinary(path)
|
|
2542
|
+
: {
|
|
2543
|
+
kind: 'binary',
|
|
2544
|
+
file: localId,
|
|
2545
|
+
byteLength: facts.byteLength,
|
|
2546
|
+
mediaType: facts.mediaType,
|
|
2547
|
+
};
|
|
2548
|
+
}
|
|
2549
|
+
async function readBinaryPath(locator, path, revision) {
|
|
2550
|
+
const asked = await askedBinary(locator, path, revision);
|
|
2621
2551
|
if (asked === null)
|
|
2622
2552
|
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
|
-
}
|
|
2553
|
+
if (asked.kind === 'pending')
|
|
2554
|
+
return pendingBinary(locator, path, asked.localId);
|
|
2634
2555
|
return readBinary(asked, { locator, path: asked.path, revision: asked.revision });
|
|
2635
2556
|
}
|
|
2557
|
+
async function heldMediaType(locator, path) {
|
|
2558
|
+
const replica = replicas.get(locator);
|
|
2559
|
+
const [held] = replica === undefined
|
|
2560
|
+
? []
|
|
2561
|
+
: await (await openLocal()).entries(replica.record.key, 'visible', [path]);
|
|
2562
|
+
return held === undefined
|
|
2563
|
+
? attachedFiles.get(locator)?.get(path)?.mediaType
|
|
2564
|
+
: held.binary?.mediaType;
|
|
2565
|
+
}
|
|
2566
|
+
function linkOf(asked, held) {
|
|
2567
|
+
return transferBinary(asked, (authorization) => options.adapter.link(authorization, asked), (result) => binaryLinkAnswer(result, held));
|
|
2568
|
+
}
|
|
2636
2569
|
async function linkBinaryPath(locator, path, revision) {
|
|
2637
2570
|
await ready;
|
|
2638
2571
|
const asked = await binaryPath(locator, path, revision);
|
|
2639
2572
|
if (asked === null || asked.kind === 'pending')
|
|
2640
2573
|
return absentBinary(path);
|
|
2641
|
-
return
|
|
2574
|
+
return linkOf(asked, revision === undefined ? await heldMediaType(locator, path) : undefined);
|
|
2642
2575
|
}
|
|
2643
|
-
async function
|
|
2576
|
+
async function showBinaryPath(locator, path, revision) {
|
|
2577
|
+
const asked = await askedBinary(locator, path, revision);
|
|
2578
|
+
if (asked === null)
|
|
2579
|
+
return absentBinary(path);
|
|
2580
|
+
if (asked.kind === 'pending')
|
|
2581
|
+
return pendingBinary(locator, path, asked.localId);
|
|
2582
|
+
if (closed)
|
|
2583
|
+
return UNCERTAIN;
|
|
2584
|
+
const name = { locator, path: asked.path, revision: asked.revision };
|
|
2585
|
+
const kept = await keptFiles.find(name);
|
|
2586
|
+
if (kept !== null)
|
|
2587
|
+
return kept;
|
|
2588
|
+
const mediaType = await heldMediaType(locator, path);
|
|
2589
|
+
if (mediaType?.startsWith('image/') === true)
|
|
2590
|
+
return fetchBinary(asked, name);
|
|
2591
|
+
return linkOf(asked, revision === undefined ? mediaType : undefined);
|
|
2592
|
+
}
|
|
2593
|
+
async function attachBinary(locator, file) {
|
|
2644
2594
|
await ready;
|
|
2645
|
-
|
|
2646
|
-
if (
|
|
2647
|
-
return
|
|
2648
|
-
|
|
2595
|
+
await firstDiscovery;
|
|
2596
|
+
if (closed || !roomsNamed || connectionOf() === 'unreachable' || refusal !== null)
|
|
2597
|
+
return OFFLINE;
|
|
2598
|
+
const request = await attachedFile(locator, file);
|
|
2599
|
+
return transferBinary({ locator, path: file.name }, async (held) => {
|
|
2600
|
+
const result = await options.adapter.attach(held, request);
|
|
2601
|
+
if (result.kind === 'attached') {
|
|
2602
|
+
const landed = attachedFiles.get(locator) ?? new Map();
|
|
2603
|
+
landed.set(result.path, { revision: result.revision, mediaType: request.mediaType });
|
|
2604
|
+
attachedFiles.set(locator, landed);
|
|
2605
|
+
}
|
|
2606
|
+
return result;
|
|
2607
|
+
}, attachAnswer);
|
|
2649
2608
|
}
|
|
2650
2609
|
function readPart(file, part) {
|
|
2651
|
-
return
|
|
2610
|
+
return database.ask({ kind: 'file.part', key: file, part });
|
|
2652
2611
|
}
|
|
2653
2612
|
async function cleanUp() {
|
|
2654
|
-
const freed = await
|
|
2613
|
+
const freed = await database.ask({ kind: 'opened.clean' });
|
|
2655
2614
|
return freed + ((await options.shell.cleanUp?.()) ?? 0);
|
|
2656
2615
|
}
|
|
2657
|
-
function declaredAgents() {
|
|
2658
|
-
for (const capability of capabilities) {
|
|
2659
|
-
if (capability.conversationAgents !== undefined)
|
|
2660
|
-
return capability.conversationAgents;
|
|
2661
|
-
}
|
|
2662
|
-
return undefined;
|
|
2663
|
-
}
|
|
2664
2616
|
function declaredAction(name) {
|
|
2665
2617
|
for (const capability of capabilities) {
|
|
2666
2618
|
for (const action of capability.actions ?? []) {
|
|
@@ -2783,169 +2735,222 @@ export function composeRuntime(options) {
|
|
|
2783
2735
|
options.adapter.reached(locator, written, roomVersion) &&
|
|
2784
2736
|
!options.adapter.reached(locator, roomVersion, written));
|
|
2785
2737
|
}
|
|
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) {
|
|
2738
|
+
function refreshingOf(local, key, composition, liveness, replaying = null) {
|
|
2739
|
+
const write = replaying === null ? exclusive : finishingWrite;
|
|
2882
2740
|
return {
|
|
2741
|
+
turn() {
|
|
2742
|
+
if (replaying !== null)
|
|
2743
|
+
installing(replaying);
|
|
2744
|
+
},
|
|
2883
2745
|
prior: async (paths) => new Map((await local.entries(key, 'refreshed', paths)).map((entry) => [entry.path, entry])),
|
|
2884
2746
|
async stage(page) {
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
const last =
|
|
2888
|
-
const
|
|
2747
|
+
for (let written = 0;;) {
|
|
2748
|
+
const end = batchEnd(page.entries, written);
|
|
2749
|
+
const last = end === page.entries.length;
|
|
2750
|
+
const entries = page.entries.slice(written, end);
|
|
2751
|
+
const staged = await write(async () => {
|
|
2889
2752
|
if (!liveness())
|
|
2890
2753
|
return false;
|
|
2891
|
-
await local.refresh(key, last
|
|
2754
|
+
await timed(stageBudget, () => local.refresh(key, composition, last
|
|
2892
2755
|
? { ...page, entries }
|
|
2893
|
-
: { entries, removals: [], folders: [], folderRemovals: [] });
|
|
2756
|
+
: { entries, removals: [], folders: [], folderRemovals: [] }));
|
|
2894
2757
|
return true;
|
|
2895
2758
|
});
|
|
2896
2759
|
if (!staged)
|
|
2897
2760
|
return false;
|
|
2761
|
+
if (last)
|
|
2762
|
+
return true;
|
|
2763
|
+
written = end;
|
|
2898
2764
|
}
|
|
2899
|
-
return true;
|
|
2900
2765
|
},
|
|
2901
2766
|
};
|
|
2902
2767
|
}
|
|
2903
|
-
function
|
|
2904
|
-
|
|
2768
|
+
function batchEnd(entries, from) {
|
|
2769
|
+
let end = from;
|
|
2905
2770
|
let spent = 0;
|
|
2906
|
-
|
|
2907
|
-
const size = (
|
|
2908
|
-
if (spent + size >
|
|
2909
|
-
|
|
2910
|
-
spent = 0;
|
|
2911
|
-
}
|
|
2912
|
-
batches.at(-1).push(entry);
|
|
2771
|
+
while (end < entries.length) {
|
|
2772
|
+
const size = (entries[end].content?.length ?? 0) + ROW_WEIGHT;
|
|
2773
|
+
if (spent + size > stageBudget.bytes && end > from)
|
|
2774
|
+
break;
|
|
2913
2775
|
spent += size;
|
|
2776
|
+
end += 1;
|
|
2777
|
+
}
|
|
2778
|
+
return end;
|
|
2779
|
+
}
|
|
2780
|
+
async function timed(budget, work) {
|
|
2781
|
+
const started = performance.now();
|
|
2782
|
+
const done = await work();
|
|
2783
|
+
const spent = performance.now() - started;
|
|
2784
|
+
if (spent < budget.aim / 2)
|
|
2785
|
+
budget.bytes = Math.min(budget.ceiling, Math.round(budget.bytes * 1.5));
|
|
2786
|
+
else if (spent > budget.aim * 2) {
|
|
2787
|
+
budget.bytes = Math.max(budget.floor, Math.round(budget.bytes / 1.5));
|
|
2788
|
+
}
|
|
2789
|
+
return done;
|
|
2790
|
+
}
|
|
2791
|
+
function installWrite(work) {
|
|
2792
|
+
return installTurn(installTurns.starting, work);
|
|
2793
|
+
}
|
|
2794
|
+
function finishingWrite(work) {
|
|
2795
|
+
return installTurn(installTurns.finishing, work);
|
|
2796
|
+
}
|
|
2797
|
+
function installTurn(queue, work) {
|
|
2798
|
+
return new Promise((resolve, reject) => {
|
|
2799
|
+
queue.push(() => void exclusive(work)
|
|
2800
|
+
.then(resolve, reject)
|
|
2801
|
+
.finally(() => {
|
|
2802
|
+
installTurns.busy = false;
|
|
2803
|
+
nextInstallTurn();
|
|
2804
|
+
}));
|
|
2805
|
+
nextInstallTurn();
|
|
2806
|
+
});
|
|
2807
|
+
}
|
|
2808
|
+
function nextInstallTurn() {
|
|
2809
|
+
if (installTurns.busy || installTurns.ahead > 0)
|
|
2810
|
+
return;
|
|
2811
|
+
const turn = installTurns.finishing.shift() ?? installTurns.starting.shift();
|
|
2812
|
+
if (turn === undefined)
|
|
2813
|
+
return;
|
|
2814
|
+
installTurns.busy = true;
|
|
2815
|
+
turn();
|
|
2816
|
+
}
|
|
2817
|
+
async function ahead(work) {
|
|
2818
|
+
let taken = false;
|
|
2819
|
+
const hold = {
|
|
2820
|
+
take() {
|
|
2821
|
+
if (taken)
|
|
2822
|
+
return;
|
|
2823
|
+
taken = true;
|
|
2824
|
+
installTurns.ahead += 1;
|
|
2825
|
+
},
|
|
2826
|
+
release() {
|
|
2827
|
+
if (!taken)
|
|
2828
|
+
return;
|
|
2829
|
+
taken = false;
|
|
2830
|
+
installTurns.ahead -= 1;
|
|
2831
|
+
resumeInstall();
|
|
2832
|
+
},
|
|
2833
|
+
};
|
|
2834
|
+
hold.take();
|
|
2835
|
+
try {
|
|
2836
|
+
return await work(hold);
|
|
2837
|
+
}
|
|
2838
|
+
finally {
|
|
2839
|
+
hold.release();
|
|
2914
2840
|
}
|
|
2915
|
-
return batches;
|
|
2916
2841
|
}
|
|
2917
|
-
|
|
2842
|
+
function resumeInstall() {
|
|
2843
|
+
if (installTurns.ahead > 0)
|
|
2844
|
+
return;
|
|
2845
|
+
void yielded().then(nextInstallTurn);
|
|
2846
|
+
}
|
|
2847
|
+
async function heldStream() {
|
|
2848
|
+
while (streamsFree === 0)
|
|
2849
|
+
await new Promise((admit) => installWaiters.push(admit));
|
|
2850
|
+
streamsFree -= 1;
|
|
2851
|
+
return once(() => {
|
|
2852
|
+
streamsFree += 1;
|
|
2853
|
+
});
|
|
2854
|
+
}
|
|
2855
|
+
async function unqueued() {
|
|
2856
|
+
while (queuedText >= QUEUED_TEXT) {
|
|
2857
|
+
await new Promise((admit) => installWaiters.push(admit));
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
function once(release) {
|
|
2861
|
+
let released = false;
|
|
2862
|
+
return () => {
|
|
2863
|
+
if (released)
|
|
2864
|
+
return;
|
|
2865
|
+
released = true;
|
|
2866
|
+
release();
|
|
2867
|
+
for (const admit of installWaiters.splice(0))
|
|
2868
|
+
admit();
|
|
2869
|
+
};
|
|
2870
|
+
}
|
|
2871
|
+
function installing(locator) {
|
|
2872
|
+
installs.add(locator);
|
|
2873
|
+
queueReadiness();
|
|
2874
|
+
}
|
|
2875
|
+
function untransferred(locator) {
|
|
2876
|
+
transfers.delete(locator);
|
|
2877
|
+
arrivals.delete(locator);
|
|
2878
|
+
streaming.delete(locator);
|
|
2879
|
+
underived.delete(locator);
|
|
2880
|
+
unindexed.delete(locator);
|
|
2881
|
+
}
|
|
2882
|
+
function setBack(locator) {
|
|
2883
|
+
const now = bytesOf(locator, true);
|
|
2884
|
+
const earlier = startedOver.get(locator);
|
|
2885
|
+
startedOver.set(locator, {
|
|
2886
|
+
done: Math.max(now.done, earlier?.done ?? 0),
|
|
2887
|
+
total: Math.max(now.total, earlier?.total ?? 0),
|
|
2888
|
+
});
|
|
2889
|
+
}
|
|
2890
|
+
function owedIndexes(locator, bytes, text) {
|
|
2891
|
+
if (bytes <= 0 || text <= 0)
|
|
2892
|
+
return;
|
|
2893
|
+
unindexed.set(locator, [...(unindexed.get(locator) ?? []), { bytes, text }]);
|
|
2894
|
+
}
|
|
2895
|
+
function built(locator, { done, text }) {
|
|
2896
|
+
const owed = unindexed.get(locator) ?? [];
|
|
2897
|
+
let left = text;
|
|
2898
|
+
while (!done && left > 0 && owed.length > 0) {
|
|
2899
|
+
const [first] = owed;
|
|
2900
|
+
if (left >= first.text) {
|
|
2901
|
+
left -= first.text;
|
|
2902
|
+
owed.shift();
|
|
2903
|
+
continue;
|
|
2904
|
+
}
|
|
2905
|
+
owed[0] = {
|
|
2906
|
+
bytes: (first.bytes * (first.text - left)) / first.text,
|
|
2907
|
+
text: first.text - left,
|
|
2908
|
+
};
|
|
2909
|
+
left = 0;
|
|
2910
|
+
}
|
|
2911
|
+
if (done || owed.length === 0)
|
|
2912
|
+
unindexed.delete(locator);
|
|
2913
|
+
queueReadiness();
|
|
2914
|
+
}
|
|
2915
|
+
function installed(locator) {
|
|
2916
|
+
if (installs.delete(locator))
|
|
2917
|
+
queueReadiness();
|
|
2918
|
+
}
|
|
2919
|
+
async function pullRefresh(authorized, held, liveness, hold, refresh) {
|
|
2918
2920
|
if (!liveness())
|
|
2919
2921
|
return unconfirmed;
|
|
2920
|
-
const turn = generation;
|
|
2921
2922
|
const { record } = held;
|
|
2922
|
-
const
|
|
2923
|
+
const from = standingOf(record.locator);
|
|
2923
2924
|
const local = await openLocal();
|
|
2924
2925
|
try {
|
|
2925
2926
|
await exclusive(() => local.unrefresh(record.key));
|
|
2926
|
-
const result = await advancement(
|
|
2927
|
+
const result = await advancement(async (authorization, request) => {
|
|
2928
|
+
hold.release();
|
|
2929
|
+
try {
|
|
2930
|
+
return await refresh(authorization, request);
|
|
2931
|
+
}
|
|
2932
|
+
finally {
|
|
2933
|
+
hold.take();
|
|
2934
|
+
}
|
|
2935
|
+
}, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.key, composed(record), liveness));
|
|
2927
2936
|
if (result.kind !== 'advanced') {
|
|
2928
2937
|
journal.record(`transfer refresh ${record.locator} ${result.kind}`);
|
|
2929
2938
|
return result;
|
|
2930
2939
|
}
|
|
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
2940
|
journal.record(`transfer refresh ${record.locator} ${result.record.roomVersion}`);
|
|
2940
|
-
return {
|
|
2941
|
+
return {
|
|
2942
|
+
kind: 'advanced',
|
|
2943
|
+
record: result.record,
|
|
2944
|
+
from,
|
|
2945
|
+
remodelled: result.record.model !== record.model,
|
|
2946
|
+
};
|
|
2941
2947
|
}
|
|
2942
2948
|
catch (error) {
|
|
2949
|
+
if (refusedForSpace(error))
|
|
2950
|
+
return unconfirmed;
|
|
2943
2951
|
journal.record(`transfer refresh ${record.locator} transport-uncertainty`);
|
|
2944
2952
|
throw error;
|
|
2945
2953
|
}
|
|
2946
|
-
finally {
|
|
2947
|
-
reads.finished(record.locator, turn);
|
|
2948
|
-
}
|
|
2949
2954
|
}
|
|
2950
2955
|
const heldContents = new Map();
|
|
2951
2956
|
function heldContentOf(local, key) {
|
|
@@ -2963,63 +2968,115 @@ export function composeRuntime(options) {
|
|
|
2963
2968
|
heldContents.set(key, made);
|
|
2964
2969
|
return made;
|
|
2965
2970
|
}
|
|
2971
|
+
async function abandoned(local, locator) {
|
|
2972
|
+
await local.abandon(locator);
|
|
2973
|
+
setBack(locator);
|
|
2974
|
+
untransferred(locator);
|
|
2975
|
+
liveQueries.recover();
|
|
2976
|
+
sweep();
|
|
2977
|
+
}
|
|
2966
2978
|
function staging(local, liveness) {
|
|
2979
|
+
const derived = async (locator, key, composition) => {
|
|
2980
|
+
underived.delete(locator);
|
|
2981
|
+
for (;;) {
|
|
2982
|
+
if (!liveness())
|
|
2983
|
+
return false;
|
|
2984
|
+
const done = await finishingWrite(async () => liveness()
|
|
2985
|
+
? timed(deriveBudget, () => local.rederive(key, composition, deriveBudget.bytes, DERIVE_LONGEST_MS, DERIVE_FLOOR))
|
|
2986
|
+
: null);
|
|
2987
|
+
if (done === null)
|
|
2988
|
+
return false;
|
|
2989
|
+
built(locator, done);
|
|
2990
|
+
if (done.done)
|
|
2991
|
+
return true;
|
|
2992
|
+
}
|
|
2993
|
+
};
|
|
2967
2994
|
return {
|
|
2968
2995
|
held: (locator) => transfers.get(locator) ?? null,
|
|
2996
|
+
async turn(locator, step) {
|
|
2997
|
+
installing(locator);
|
|
2998
|
+
return step === 'downloading' ? heldStream() : () => { };
|
|
2999
|
+
},
|
|
3000
|
+
unqueued,
|
|
3001
|
+
queued(text) {
|
|
3002
|
+
queuedText += text;
|
|
3003
|
+
return once(() => {
|
|
3004
|
+
queuedText -= text;
|
|
3005
|
+
});
|
|
3006
|
+
},
|
|
3007
|
+
arriving(locator) {
|
|
3008
|
+
let ended = false;
|
|
3009
|
+
return {
|
|
3010
|
+
at(bytes) {
|
|
3011
|
+
if (ended)
|
|
3012
|
+
return;
|
|
3013
|
+
arrivals.set(locator, bytes);
|
|
3014
|
+
streaming.add(locator);
|
|
3015
|
+
const now = performance.now();
|
|
3016
|
+
if (now - arrivalReported < ARRIVAL_REPORT_MS)
|
|
3017
|
+
return;
|
|
3018
|
+
arrivalReported = now;
|
|
3019
|
+
queueReadiness();
|
|
3020
|
+
},
|
|
3021
|
+
end() {
|
|
3022
|
+
if (ended)
|
|
3023
|
+
return;
|
|
3024
|
+
ended = true;
|
|
3025
|
+
if (streaming.delete(locator))
|
|
3026
|
+
queueReadiness();
|
|
3027
|
+
},
|
|
3028
|
+
};
|
|
3029
|
+
},
|
|
3030
|
+
streamed: options.adapter.roomStream !== undefined,
|
|
2969
3031
|
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)
|
|
3032
|
+
const { locator } = record;
|
|
3033
|
+
installing(locator);
|
|
3034
|
+
if (!liveness())
|
|
3035
|
+
return null;
|
|
3036
|
+
const key = await installWrite(async () => {
|
|
3037
|
+
if (!liveness())
|
|
2991
3038
|
return null;
|
|
2992
|
-
|
|
2993
|
-
}
|
|
2994
|
-
|
|
3039
|
+
return local.stage(record, composition, page);
|
|
3040
|
+
});
|
|
3041
|
+
if (key === null || !liveness())
|
|
3042
|
+
return null;
|
|
3043
|
+
const arrived = !transfers.has(locator);
|
|
3044
|
+
const prior = transfers.get(locator)?.done.bytes ?? 0;
|
|
3045
|
+
const held = { ...record, key };
|
|
3046
|
+
transfers.set(locator, held);
|
|
3047
|
+
if (arrived)
|
|
3048
|
+
liveQueries.recover();
|
|
3049
|
+
installing(locator);
|
|
2995
3050
|
liveQueries.recover();
|
|
2996
|
-
|
|
3051
|
+
const text = page.entries.reduce((sum, entry) => sum + (entry.content?.length ?? 0), 0);
|
|
3052
|
+
owedIndexes(locator, record.done.bytes - prior, text);
|
|
3053
|
+
const owed = text + (underived.get(locator) ?? 0);
|
|
3054
|
+
underived.set(locator, owed);
|
|
3055
|
+
if (owed < deriveBudget.bytes)
|
|
3056
|
+
return held;
|
|
3057
|
+
return (await derived(locator, key, composition)) ? held : null;
|
|
2997
3058
|
},
|
|
2998
|
-
|
|
3059
|
+
derive: derived,
|
|
3060
|
+
async refreshing(locator, key, composition) {
|
|
2999
3061
|
await exclusive(() => local.unrefresh(key));
|
|
3000
|
-
return refreshingOf(local, key, liveness);
|
|
3062
|
+
return refreshingOf(local, key, composition, liveness, locator);
|
|
3001
3063
|
},
|
|
3002
3064
|
abandon: (locator) => exclusive(async () => {
|
|
3003
|
-
if (
|
|
3004
|
-
|
|
3005
|
-
await local.abandon(locator);
|
|
3006
|
-
transfers.delete(locator);
|
|
3007
|
-
liveQueries.recover();
|
|
3008
|
-
sweep();
|
|
3065
|
+
if (liveness())
|
|
3066
|
+
await abandoned(local, locator);
|
|
3009
3067
|
}),
|
|
3010
|
-
setAside:
|
|
3068
|
+
setAside: async (locator) => {
|
|
3011
3069
|
if (!liveness())
|
|
3012
3070
|
return;
|
|
3013
|
-
|
|
3014
|
-
|
|
3071
|
+
setBack(locator);
|
|
3072
|
+
untransferred(locator);
|
|
3015
3073
|
liveQueries.recover();
|
|
3016
|
-
}
|
|
3074
|
+
},
|
|
3017
3075
|
release: (key) => exclusive(async () => {
|
|
3018
3076
|
await local.release(key);
|
|
3019
3077
|
sweep();
|
|
3020
3078
|
}),
|
|
3021
3079
|
content: (key) => heldContentOf(local, key),
|
|
3022
|
-
validate: (record, view) => validatedRoom(local, record, view, null),
|
|
3023
3080
|
};
|
|
3024
3081
|
}
|
|
3025
3082
|
function reusable(room, local) {
|
|
@@ -3031,30 +3088,28 @@ export function composeRuntime(options) {
|
|
|
3031
3088
|
async function pullBootstrap(authorized, room, local, liveness) {
|
|
3032
3089
|
if (!liveness())
|
|
3033
3090
|
return unconfirmed;
|
|
3034
|
-
const turn = generation;
|
|
3035
3091
|
if (replicas.has(room.locator)) {
|
|
3036
3092
|
catching.add(room.locator);
|
|
3037
3093
|
liveQueries.recover();
|
|
3038
3094
|
}
|
|
3039
3095
|
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));
|
|
3096
|
+
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
3097
|
const detail = result.kind === 'staged' ? result.record.roomVersion : result.kind;
|
|
3045
3098
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
3046
|
-
if (result.kind !== 'staged')
|
|
3099
|
+
if (result.kind !== 'staged') {
|
|
3047
3100
|
caughtUpTo(room.locator);
|
|
3101
|
+
installed(room.locator);
|
|
3102
|
+
}
|
|
3048
3103
|
return result;
|
|
3049
3104
|
}
|
|
3050
3105
|
catch (error) {
|
|
3051
|
-
journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
|
|
3052
3106
|
caughtUpTo(room.locator);
|
|
3107
|
+
installed(room.locator);
|
|
3108
|
+
if (refusedForSpace(error))
|
|
3109
|
+
return unconfirmed;
|
|
3110
|
+
journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
|
|
3053
3111
|
throw error;
|
|
3054
3112
|
}
|
|
3055
|
-
finally {
|
|
3056
|
-
reads.finished(room.locator, turn);
|
|
3057
|
-
}
|
|
3058
3113
|
}
|
|
3059
3114
|
function caughtUpTo(locator) {
|
|
3060
3115
|
if (catching.delete(locator))
|
|
@@ -3062,72 +3117,82 @@ export function composeRuntime(options) {
|
|
|
3062
3117
|
}
|
|
3063
3118
|
function behind(room) {
|
|
3064
3119
|
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)));
|
|
3120
|
+
return held !== undefined && !options.adapter.reached(room.locator, held, room.roomVersion);
|
|
3067
3121
|
}
|
|
3068
|
-
|
|
3069
|
-
const
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
return
|
|
3122
|
+
function standingOf(locator) {
|
|
3123
|
+
const held = replicas.get(locator);
|
|
3124
|
+
return { key: held?.record.key ?? null, confirmed: held?.confirmed ?? 0 };
|
|
3125
|
+
}
|
|
3126
|
+
function stands(locator, from) {
|
|
3127
|
+
const now = standingOf(locator);
|
|
3128
|
+
return now.key === from.key && now.confirmed === from.confirmed;
|
|
3075
3129
|
}
|
|
3076
|
-
async function
|
|
3077
|
-
|
|
3130
|
+
async function advance(room, local, authorized, running) {
|
|
3131
|
+
const live = () => running() && !full;
|
|
3132
|
+
if (!live())
|
|
3078
3133
|
return unconfirmed;
|
|
3079
3134
|
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')
|
|
3135
|
+
if (held !== undefined && options.adapter.roomStream !== undefined) {
|
|
3136
|
+
stale.delete(room.locator);
|
|
3137
|
+
name(room.locator);
|
|
3138
|
+
return null;
|
|
3139
|
+
}
|
|
3140
|
+
const refresh = reads.refresh;
|
|
3141
|
+
if (held !== undefined && refresh !== undefined) {
|
|
3142
|
+
const refreshed = await ahead((hold) => refreshedRoom(room, local, authorized, held, live, hold, refresh));
|
|
3143
|
+
if (refreshed === 'behind')
|
|
3144
|
+
return advance(room, local, authorized, live);
|
|
3145
|
+
if (refreshed !== 'beyond-horizon')
|
|
3107
3146
|
return refreshed;
|
|
3108
|
-
if (
|
|
3109
|
-
owed.add(room.locator);
|
|
3110
|
-
liveQueries.recover();
|
|
3111
|
-
return unconfirmed;
|
|
3112
|
-
}
|
|
3113
|
-
if (reloadsDue.get(room.locator) === generation)
|
|
3147
|
+
if (reloadsDue.has(room.locator))
|
|
3114
3148
|
return unconfirmed;
|
|
3115
3149
|
}
|
|
3150
|
+
const from = standingOf(room.locator);
|
|
3116
3151
|
const bootstrapped = await pullBootstrap(authorized, room, local, live);
|
|
3152
|
+
if (bootstrapped.kind !== 'staged')
|
|
3153
|
+
return live() ? bootstrapped : unconfirmed;
|
|
3154
|
+
const landing = { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from };
|
|
3155
|
+
if (await publish(local, room, bootstrapped.record, landing, live)) {
|
|
3156
|
+
return live() ? null : unconfirmed;
|
|
3157
|
+
}
|
|
3158
|
+
return reachedBy(room.locator, bootstrapped.record.roomVersion)
|
|
3159
|
+
? null
|
|
3160
|
+
: advance(room, local, authorized, live);
|
|
3161
|
+
}
|
|
3162
|
+
async function refreshedRoom(room, local, authorized, held, live, hold, refresh) {
|
|
3163
|
+
const refreshed = await pullRefresh(authorized, held, live, hold, refresh);
|
|
3117
3164
|
if (!live())
|
|
3118
3165
|
return unconfirmed;
|
|
3119
|
-
if (
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3166
|
+
if (refreshed.kind === 'still-current') {
|
|
3167
|
+
stale.delete(room.locator);
|
|
3168
|
+
if (unreadable.delete(room.locator)) {
|
|
3169
|
+
scopedRefusals.delete(room.locator);
|
|
3170
|
+
list(room, true);
|
|
3171
|
+
emitRooms();
|
|
3172
|
+
}
|
|
3173
|
+
return null;
|
|
3174
|
+
}
|
|
3175
|
+
if (refreshed.kind === 'advanced') {
|
|
3176
|
+
const landing = {
|
|
3177
|
+
kind: 'refreshed',
|
|
3178
|
+
from: refreshed.from,
|
|
3179
|
+
remodelled: refreshed.remodelled,
|
|
3180
|
+
};
|
|
3181
|
+
if (!(await publish(local, room, refreshed.record, landing, live))) {
|
|
3182
|
+
const now = replicas.get(room.locator)?.record.roomVersion;
|
|
3183
|
+
const reached = now !== undefined &&
|
|
3184
|
+
options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
|
|
3185
|
+
return reached ? null : 'behind';
|
|
3186
|
+
}
|
|
3187
|
+
await retainModel(local, room, modelOf(room.locator));
|
|
3188
|
+
return null;
|
|
3189
|
+
}
|
|
3190
|
+
return refreshed.kind === 'beyond-horizon' ? 'beyond-horizon' : refreshed;
|
|
3123
3191
|
}
|
|
3124
3192
|
async function withhold(withheld, room, local, progress) {
|
|
3125
3193
|
if (withheld.kind === 'unconfirmed')
|
|
3126
3194
|
return;
|
|
3127
|
-
|
|
3128
|
-
stale.delete(room.locator);
|
|
3129
|
-
else
|
|
3130
|
-
stale.add(room.locator);
|
|
3195
|
+
stale.add(room.locator);
|
|
3131
3196
|
if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
|
|
3132
3197
|
const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
|
|
3133
3198
|
repairable.set(room.locator, reset);
|
|
@@ -3149,30 +3214,10 @@ export function composeRuntime(options) {
|
|
|
3149
3214
|
list(room, false);
|
|
3150
3215
|
emitRooms();
|
|
3151
3216
|
}
|
|
3152
|
-
else if (withheld.kind === 'refused') {
|
|
3153
|
-
refusedSource(room, withheld);
|
|
3154
|
-
}
|
|
3155
3217
|
else if (withheld.kind === 'revoked-authorization') {
|
|
3156
3218
|
await refuse('revoked-authorization');
|
|
3157
3219
|
}
|
|
3158
3220
|
}
|
|
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
3221
|
function leaving(batch, result) {
|
|
3177
3222
|
if (result.kind === 'expired')
|
|
3178
3223
|
return { batch, returned: returnedBy('expired') };
|
|
@@ -3194,8 +3239,8 @@ export function composeRuntime(options) {
|
|
|
3194
3239
|
function settling(acceptance) {
|
|
3195
3240
|
return acceptance !== null && 'apply' in acceptance;
|
|
3196
3241
|
}
|
|
3197
|
-
function
|
|
3198
|
-
return settling(acceptance) ?
|
|
3242
|
+
function failureOf(acceptance) {
|
|
3243
|
+
return settling(acceptance) ? null : acceptance;
|
|
3199
3244
|
}
|
|
3200
3245
|
async function acceptance(room, local, submitted, accepted) {
|
|
3201
3246
|
if (!continuing())
|
|
@@ -3218,34 +3263,6 @@ export function composeRuntime(options) {
|
|
|
3218
3263
|
? accepted.roomVersion
|
|
3219
3264
|
: held,
|
|
3220
3265
|
};
|
|
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
3266
|
changes = back;
|
|
3250
3267
|
}
|
|
3251
3268
|
const settled = await settlement(local, room.locator, current, record, current.queue.filter((batch) => batch.id !== submitted.id), { room: true, ...(changes === undefined ? {} : { changes }) });
|
|
@@ -3255,8 +3272,6 @@ export function composeRuntime(options) {
|
|
|
3255
3272
|
const set = settled.apply(answer);
|
|
3256
3273
|
pendings.delete(pendingKey(room.locator, submitted.id));
|
|
3257
3274
|
unreadable.delete(room.locator);
|
|
3258
|
-
sourceRefusals.delete(room.locator);
|
|
3259
|
-
declarationRefusals.delete(room.locator);
|
|
3260
3275
|
list(room, true);
|
|
3261
3276
|
return set;
|
|
3262
3277
|
},
|
|
@@ -3274,13 +3289,9 @@ export function composeRuntime(options) {
|
|
|
3274
3289
|
const failed = await exclusive(async () => {
|
|
3275
3290
|
const adopting = await acceptance(room, local, submitted, accepted);
|
|
3276
3291
|
await adoptedTogether(local, [adopting]);
|
|
3277
|
-
return
|
|
3292
|
+
return failureOf(adopting);
|
|
3278
3293
|
});
|
|
3279
|
-
|
|
3280
|
-
return true;
|
|
3281
|
-
if (failed.kind === 'refused' && continuing())
|
|
3282
|
-
refusedSource(room, failed);
|
|
3283
|
-
return false;
|
|
3294
|
+
return failed === null;
|
|
3284
3295
|
}
|
|
3285
3296
|
async function adoptedMove(room, partner, local, submitted, accepted) {
|
|
3286
3297
|
const other = discovered.get(partner);
|
|
@@ -3294,18 +3305,10 @@ export function composeRuntime(options) {
|
|
|
3294
3305
|
...accepted.destination,
|
|
3295
3306
|
});
|
|
3296
3307
|
await adoptedTogether(local, [left, arrived]);
|
|
3297
|
-
|
|
3298
|
-
if (other !== undefined && refusedArrival?.kind === 'refused' && continuing()) {
|
|
3299
|
-
refusedSource(other, refusedArrival);
|
|
3300
|
-
}
|
|
3301
|
-
return refusalOf(left);
|
|
3308
|
+
return failureOf(left);
|
|
3302
3309
|
});
|
|
3303
3310
|
submitLater(partner);
|
|
3304
|
-
|
|
3305
|
-
return true;
|
|
3306
|
-
if (failed.kind === 'refused' && continuing())
|
|
3307
|
-
refusedSource(room, failed);
|
|
3308
|
-
return false;
|
|
3311
|
+
return failed === null;
|
|
3309
3312
|
}
|
|
3310
3313
|
function queueIn(locator) {
|
|
3311
3314
|
return replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
@@ -3428,8 +3431,6 @@ export function composeRuntime(options) {
|
|
|
3428
3431
|
if (replica === undefined || head === undefined)
|
|
3429
3432
|
return 'done';
|
|
3430
3433
|
const state = pendings.get(pendingKey(room.locator, head.id));
|
|
3431
|
-
if (state?.accepted !== undefined)
|
|
3432
|
-
return 'done';
|
|
3433
3434
|
const partner = partnerOf(head, room.locator);
|
|
3434
3435
|
if (partner !== null && !leavingIn(head, room.locator)) {
|
|
3435
3436
|
submitLater(partner);
|
|
@@ -3543,12 +3544,12 @@ export function composeRuntime(options) {
|
|
|
3543
3544
|
}
|
|
3544
3545
|
if (result.kind === 'expired') {
|
|
3545
3546
|
await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, partner !== null && lost));
|
|
3546
|
-
const there = other === undefined ? null : await
|
|
3547
|
+
const there = other === undefined ? null : await advanced(other, local, authorized);
|
|
3547
3548
|
if (other !== undefined && there !== null) {
|
|
3548
3549
|
await withhold(there, other, local, progress);
|
|
3549
3550
|
return 'done';
|
|
3550
3551
|
}
|
|
3551
|
-
const withheld = await
|
|
3552
|
+
const withheld = await advanced(room, local, authorized);
|
|
3552
3553
|
if (withheld !== null) {
|
|
3553
3554
|
await withhold(withheld, room, local, progress);
|
|
3554
3555
|
return 'done';
|
|
@@ -3570,8 +3571,8 @@ export function composeRuntime(options) {
|
|
|
3570
3571
|
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
3571
3572
|
const left = leaving(head, result);
|
|
3572
3573
|
const refreshed = async () => ({
|
|
3573
|
-
there: other === undefined ? null : await
|
|
3574
|
-
here: await
|
|
3574
|
+
there: other === undefined ? null : await advanced(other, local, authorized),
|
|
3575
|
+
here: await advanced(room, local, authorized),
|
|
3575
3576
|
});
|
|
3576
3577
|
const judged = result.kind === 'conflicted' && other !== undefined ? await refreshed() : null;
|
|
3577
3578
|
if (judged !== null && (judged.there !== null || judged.here !== null)) {
|
|
@@ -3642,6 +3643,7 @@ export function composeRuntime(options) {
|
|
|
3642
3643
|
locator: room.locator,
|
|
3643
3644
|
key: before.record.key,
|
|
3644
3645
|
roots: before.record.typedSelection?.roots ?? [],
|
|
3646
|
+
composed: composedOf(composed(before.record)),
|
|
3645
3647
|
overlay: { set: [], cleared: [...before.overlaid] },
|
|
3646
3648
|
},
|
|
3647
3649
|
]),
|
|
@@ -3674,39 +3676,37 @@ export function composeRuntime(options) {
|
|
|
3674
3676
|
if (signal.aborted)
|
|
3675
3677
|
return;
|
|
3676
3678
|
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
|
-
});
|
|
3679
|
+
await roomTask(room.locator, (live) => reload(room, local, progress, live));
|
|
3683
3680
|
if (signal.aborted)
|
|
3684
3681
|
return;
|
|
3685
3682
|
await rest(progress);
|
|
3686
3683
|
}
|
|
3687
3684
|
}
|
|
3688
3685
|
function awaitingAnswer(locator) {
|
|
3686
|
+
answering.get(locator)?.release();
|
|
3689
3687
|
let release = () => { };
|
|
3690
|
-
const
|
|
3688
|
+
const answered = new Promise((resolve) => {
|
|
3691
3689
|
release = resolve;
|
|
3692
3690
|
});
|
|
3693
|
-
|
|
3691
|
+
const awaited = { answered, release };
|
|
3692
|
+
answering.set(locator, awaited);
|
|
3694
3693
|
return () => {
|
|
3695
|
-
if (answering.get(locator)
|
|
3694
|
+
if (answering.get(locator) === awaited)
|
|
3696
3695
|
answering.delete(locator);
|
|
3697
3696
|
release();
|
|
3698
3697
|
};
|
|
3699
3698
|
}
|
|
3700
|
-
|
|
3701
|
-
const
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3699
|
+
function declaredAsHeld(room) {
|
|
3700
|
+
const held = replicas.get(room.locator)?.record;
|
|
3701
|
+
return (held === undefined ||
|
|
3702
|
+
(JSON.stringify(held.operations) === JSON.stringify(room.operations) &&
|
|
3703
|
+
JSON.stringify(held.replica) === JSON.stringify(room.replica) &&
|
|
3704
|
+
JSON.stringify(held.typedSelection) === JSON.stringify(room.typedSelection ?? null)));
|
|
3705
|
+
}
|
|
3706
|
+
async function retainModel(local, room, next) {
|
|
3707
|
+
const key = room.locator;
|
|
3708
|
+
const previous = modelOf(room.locator);
|
|
3709
|
+
if (JSON.stringify(previous) === JSON.stringify(next) && declaredAsHeld(room))
|
|
3710
3710
|
return;
|
|
3711
3711
|
if (next.status === 'unavailable') {
|
|
3712
3712
|
invalidModels.add(key);
|
|
@@ -3718,26 +3718,13 @@ export function composeRuntime(options) {
|
|
|
3718
3718
|
const held = replicas.get(room.locator);
|
|
3719
3719
|
let changed = null;
|
|
3720
3720
|
if (held !== undefined) {
|
|
3721
|
-
const
|
|
3721
|
+
const record = {
|
|
3722
3722
|
...held.record,
|
|
3723
3723
|
operations: room.operations,
|
|
3724
3724
|
replica: room.replica,
|
|
3725
3725
|
typedSelection: room.typedSelection ?? null,
|
|
3726
|
-
|
|
3726
|
+
model: next,
|
|
3727
3727
|
};
|
|
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
3728
|
const confirmedLayer = await sliceAt(record, 'confirmed', held.queue, held.overlaid);
|
|
3742
3729
|
const replayedLayer = await replayed(record, confirmedLayer, held.queue);
|
|
3743
3730
|
const diff = await overlayDiff(record, held, replayedLayer.visible, replayedLayer.touched);
|
|
@@ -3748,41 +3735,33 @@ export function composeRuntime(options) {
|
|
|
3748
3735
|
key: record.key,
|
|
3749
3736
|
record: storedRecord(record),
|
|
3750
3737
|
roots: record.typedSelection?.roots ?? [],
|
|
3751
|
-
|
|
3738
|
+
composed: composedOf(composed(record)),
|
|
3752
3739
|
overlay: diff.overlay,
|
|
3753
3740
|
},
|
|
3754
3741
|
],
|
|
3755
3742
|
});
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
replicas.set(room.locator, { ...held, record, overlaid: diff.touched, count });
|
|
3743
|
+
const { count, indexed } = committedRoom(room.locator, answer);
|
|
3744
|
+
replicas.set(room.locator, { ...held, record, overlaid: diff.touched, count, indexed });
|
|
3759
3745
|
changed = changeSet(room.locator, {
|
|
3760
3746
|
documents: answer.changed[room.locator] ?? [],
|
|
3761
3747
|
room: true,
|
|
3762
3748
|
});
|
|
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
3749
|
}
|
|
3774
|
-
|
|
3775
|
-
if (next.status === 'current') {
|
|
3776
|
-
liveModels.set(key, JSON.stringify(next.snapshot));
|
|
3777
|
-
invalidModels.delete(key);
|
|
3778
|
-
}
|
|
3779
|
-
list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
3750
|
+
heldModel(room, next);
|
|
3780
3751
|
if (changed === null)
|
|
3781
3752
|
emitRooms();
|
|
3782
3753
|
else
|
|
3783
3754
|
publishChangeSet(changed);
|
|
3784
3755
|
});
|
|
3785
3756
|
}
|
|
3757
|
+
function heldModel(room, next) {
|
|
3758
|
+
models.set(room.locator, next);
|
|
3759
|
+
if (next.status === 'current') {
|
|
3760
|
+
liveModels.set(room.locator, JSON.stringify(next.snapshot));
|
|
3761
|
+
invalidModels.delete(room.locator);
|
|
3762
|
+
}
|
|
3763
|
+
list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
3764
|
+
}
|
|
3786
3765
|
async function scoped(result, room, local) {
|
|
3787
3766
|
if (result.scope === 'content' && !result.retryable) {
|
|
3788
3767
|
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
@@ -3796,14 +3775,14 @@ export function composeRuntime(options) {
|
|
|
3796
3775
|
});
|
|
3797
3776
|
let scopedAway = room;
|
|
3798
3777
|
if (result.scope === 'model')
|
|
3799
|
-
await retainModel(local, room,
|
|
3778
|
+
await retainModel(local, room, {
|
|
3800
3779
|
status: 'unavailable',
|
|
3801
3780
|
reason: 'not-readable',
|
|
3802
3781
|
});
|
|
3803
3782
|
else if (result.scope !== 'operation') {
|
|
3804
3783
|
unreadable.add(room.locator);
|
|
3805
3784
|
scopedAway = { ...room, replica: { ...room.replica, admission: 'unavailable' } };
|
|
3806
|
-
await retainModel(local, scopedAway,
|
|
3785
|
+
await retainModel(local, scopedAway, modelOf(room.locator));
|
|
3807
3786
|
}
|
|
3808
3787
|
if (!continuing())
|
|
3809
3788
|
return;
|
|
@@ -3811,6 +3790,17 @@ export function composeRuntime(options) {
|
|
|
3811
3790
|
emitRooms();
|
|
3812
3791
|
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
3813
3792
|
}
|
|
3793
|
+
function name(locator) {
|
|
3794
|
+
const stream = options.adapter.roomStream;
|
|
3795
|
+
const record = replicas.get(locator)?.record;
|
|
3796
|
+
const room = discovered.get(locator);
|
|
3797
|
+
if (stream === undefined || record === undefined || room === undefined)
|
|
3798
|
+
return;
|
|
3799
|
+
if (!socketOpen || !handed || record.replica.selection !== room.replica.selection)
|
|
3800
|
+
return;
|
|
3801
|
+
naming += 1;
|
|
3802
|
+
stream.hold([{ locator, roomVersion: record.roomVersion }]);
|
|
3803
|
+
}
|
|
3814
3804
|
function heldRooms(listed) {
|
|
3815
3805
|
return listed.flatMap((room) => {
|
|
3816
3806
|
const record = replicas.get(room.locator)?.record;
|
|
@@ -3828,14 +3818,20 @@ export function composeRuntime(options) {
|
|
|
3828
3818
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
3829
3819
|
});
|
|
3830
3820
|
if (attempt === 'opened') {
|
|
3821
|
+
naming += 1;
|
|
3831
3822
|
options.adapter.roomStream?.hold(heldRooms(ready));
|
|
3832
3823
|
handed = true;
|
|
3833
3824
|
}
|
|
3834
3825
|
discovering?.release();
|
|
3835
|
-
const
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3826
|
+
for (const room of ready) {
|
|
3827
|
+
if (attempt !== 'unstreamed' && replicas.has(room.locator)) {
|
|
3828
|
+
if (attempt === 'authorized')
|
|
3829
|
+
name(room.locator);
|
|
3830
|
+
continue;
|
|
3831
|
+
}
|
|
3832
|
+
void roomTask(room.locator, (live) => loadRoom(room, local, progress, live)).then(() => rest(progress));
|
|
3833
|
+
}
|
|
3834
|
+
const failed = prepared.find((room) => room.status === 'rejected');
|
|
3839
3835
|
if (failed !== undefined)
|
|
3840
3836
|
throw failed.reason;
|
|
3841
3837
|
}
|
|
@@ -3853,27 +3849,29 @@ export function composeRuntime(options) {
|
|
|
3853
3849
|
return false;
|
|
3854
3850
|
}
|
|
3855
3851
|
if (prior !== undefined)
|
|
3856
|
-
await retainModel(local, room,
|
|
3852
|
+
await retainModel(local, room, modelOf(room.locator));
|
|
3857
3853
|
if (discoveryPaused || authorization === undefined)
|
|
3858
3854
|
return false;
|
|
3859
3855
|
return room.replica.admission === 'admitted';
|
|
3860
3856
|
}
|
|
3861
3857
|
async function caughtUp(room, local, progress, live) {
|
|
3862
|
-
if (
|
|
3858
|
+
if (options.adapter.roomStream !== undefined || authorization === undefined)
|
|
3859
|
+
return;
|
|
3860
|
+
if (!live() || !behind(room))
|
|
3863
3861
|
return;
|
|
3864
3862
|
const withheld = await advance(room, local, authorization, live);
|
|
3865
3863
|
if (withheld !== null)
|
|
3866
3864
|
await withhold(withheld, room, local, progress);
|
|
3867
3865
|
}
|
|
3868
3866
|
function revoked(locator) {
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
3872
|
-
});
|
|
3867
|
+
const model = modelOf(locator);
|
|
3868
|
+
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
3873
3869
|
}
|
|
3874
3870
|
async function loadRoom(room, local, progress, live) {
|
|
3875
3871
|
if (!live() || authorization === undefined)
|
|
3876
3872
|
return;
|
|
3873
|
+
if (replicas.has(room.locator) && !(await prepareRoom(room, local, progress)))
|
|
3874
|
+
return;
|
|
3877
3875
|
if (replicas.has(room.locator) && revoked(room.locator)) {
|
|
3878
3876
|
await reload(room, local, progress, live);
|
|
3879
3877
|
return;
|
|
@@ -3891,14 +3889,14 @@ export function composeRuntime(options) {
|
|
|
3891
3889
|
reloadsDue.delete(room.locator);
|
|
3892
3890
|
if (!live() || authorization === undefined)
|
|
3893
3891
|
return;
|
|
3892
|
+
const from = standingOf(room.locator);
|
|
3894
3893
|
const bootstrapped = await pullBootstrap(authorization, room, local, live);
|
|
3895
|
-
if (!live())
|
|
3896
|
-
return;
|
|
3897
3894
|
if (bootstrapped.kind !== 'staged') {
|
|
3898
|
-
|
|
3895
|
+
if (live())
|
|
3896
|
+
await withhold(bootstrapped, room, local, progress);
|
|
3899
3897
|
return;
|
|
3900
3898
|
}
|
|
3901
|
-
await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
|
|
3899
|
+
await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from }, live);
|
|
3902
3900
|
await caughtUp(room, local, progress, live);
|
|
3903
3901
|
}
|
|
3904
3902
|
finally {
|
|
@@ -3920,6 +3918,8 @@ export function composeRuntime(options) {
|
|
|
3920
3918
|
catch {
|
|
3921
3919
|
result = { kind: 'transport-uncertainty' };
|
|
3922
3920
|
}
|
|
3921
|
+
if (result.kind === 'discovery')
|
|
3922
|
+
roomsNamed = true;
|
|
3923
3923
|
if (!live()) {
|
|
3924
3924
|
progress.complete = false;
|
|
3925
3925
|
notifyReady();
|
|
@@ -3927,12 +3927,7 @@ export function composeRuntime(options) {
|
|
|
3927
3927
|
}
|
|
3928
3928
|
journal.record(`connection discover ${result.kind}`);
|
|
3929
3929
|
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
|
-
}
|
|
3930
|
+
await settleModels([...rooms.values()].map((room) => discovered.get(room.locator) ?? room), 'cached', local);
|
|
3936
3931
|
progress.reachable = false;
|
|
3937
3932
|
progress.complete = false;
|
|
3938
3933
|
notifyReady();
|
|
@@ -4009,23 +4004,45 @@ export function composeRuntime(options) {
|
|
|
4009
4004
|
}
|
|
4010
4005
|
}
|
|
4011
4006
|
async function admittedRead(work) {
|
|
4012
|
-
|
|
4013
|
-
|
|
4007
|
+
while (roomReads.free === 0) {
|
|
4008
|
+
await new Promise((admit) => roomReads.waiting.push(admit));
|
|
4014
4009
|
}
|
|
4015
|
-
|
|
4016
|
-
while (reads.free === 0)
|
|
4017
|
-
await new Promise((admit) => reads.waiting.push(admit));
|
|
4018
|
-
reads.free -= 1;
|
|
4010
|
+
roomReads.free -= 1;
|
|
4019
4011
|
try {
|
|
4020
4012
|
await work();
|
|
4021
4013
|
}
|
|
4022
4014
|
finally {
|
|
4023
|
-
|
|
4024
|
-
|
|
4015
|
+
roomReads.free += 1;
|
|
4016
|
+
roomReads.waiting.shift()?.();
|
|
4025
4017
|
}
|
|
4026
4018
|
}
|
|
4019
|
+
function inRoom(locator, work) {
|
|
4020
|
+
roomWork += 1;
|
|
4021
|
+
const ran = (roomLanes.get(locator) ?? Promise.resolve()).then(async () => {
|
|
4022
|
+
try {
|
|
4023
|
+
return await work(continuing);
|
|
4024
|
+
}
|
|
4025
|
+
finally {
|
|
4026
|
+
roomWork -= 1;
|
|
4027
|
+
}
|
|
4028
|
+
});
|
|
4029
|
+
const chain = ran.then(() => undefined, () => undefined);
|
|
4030
|
+
roomLanes.set(locator, chain);
|
|
4031
|
+
working.add(chain);
|
|
4032
|
+
void chain.finally(() => {
|
|
4033
|
+
working.delete(chain);
|
|
4034
|
+
if (roomLanes.get(locator) === chain)
|
|
4035
|
+
roomLanes.delete(locator);
|
|
4036
|
+
});
|
|
4037
|
+
return ran;
|
|
4038
|
+
}
|
|
4027
4039
|
function roomTask(locator, work) {
|
|
4028
|
-
|
|
4040
|
+
if (stopping)
|
|
4041
|
+
return Promise.resolve();
|
|
4042
|
+
return inRoom(locator, (live) => admittedRead(() => work(live))).catch(() => undefined);
|
|
4043
|
+
}
|
|
4044
|
+
function advanced(room, local, authorized) {
|
|
4045
|
+
return inRoom(room.locator, (live) => advance(room, local, authorized, live));
|
|
4029
4046
|
}
|
|
4030
4047
|
function task(lane, work) {
|
|
4031
4048
|
if (stopping)
|
|
@@ -4044,20 +4061,17 @@ export function composeRuntime(options) {
|
|
|
4044
4061
|
function sync(attempt) {
|
|
4045
4062
|
return task(OPENING_LANE, async (live) => {
|
|
4046
4063
|
if (!live()) {
|
|
4047
|
-
|
|
4064
|
+
readyFromStore();
|
|
4048
4065
|
return;
|
|
4049
4066
|
}
|
|
4050
4067
|
const progress = { complete: true, reachable: true };
|
|
4051
4068
|
const gate = discover();
|
|
4052
|
-
|
|
4053
|
-
opened = generation;
|
|
4054
|
-
underway = attempt === 'opened' ? { generation, progress } : null;
|
|
4069
|
+
underway = attempt === 'opened' ? progress : null;
|
|
4055
4070
|
if (attempt === 'opened')
|
|
4056
4071
|
streamed = progress;
|
|
4057
4072
|
complete = false;
|
|
4058
4073
|
recorded = false;
|
|
4059
4074
|
moveTo(transferring(status));
|
|
4060
|
-
handed = false;
|
|
4061
4075
|
try {
|
|
4062
4076
|
const local = await openLocal();
|
|
4063
4077
|
if (!live())
|
|
@@ -4085,7 +4099,6 @@ export function composeRuntime(options) {
|
|
|
4085
4099
|
if (handed || closed || progress.reachable)
|
|
4086
4100
|
return;
|
|
4087
4101
|
underway = null;
|
|
4088
|
-
opened = null;
|
|
4089
4102
|
journal.record('connection reopen');
|
|
4090
4103
|
await rest(progress, live);
|
|
4091
4104
|
options.adapter.roomStream?.reopen();
|
|
@@ -4122,13 +4135,14 @@ export function composeRuntime(options) {
|
|
|
4122
4135
|
});
|
|
4123
4136
|
}
|
|
4124
4137
|
function closedSocket(revoked, lost) {
|
|
4138
|
+
notifyDiscovered();
|
|
4125
4139
|
const opened = socketOpen;
|
|
4126
4140
|
socketOpen = false;
|
|
4141
|
+
handed = false;
|
|
4142
|
+
naming = 0;
|
|
4127
4143
|
connectionLost();
|
|
4128
|
-
if (lost)
|
|
4129
|
-
reads.lost();
|
|
4144
|
+
if (lost)
|
|
4130
4145
|
conversations.lost();
|
|
4131
|
-
}
|
|
4132
4146
|
if (revoked) {
|
|
4133
4147
|
void refuse('revoked-authorization');
|
|
4134
4148
|
return;
|
|
@@ -4143,14 +4157,13 @@ export function composeRuntime(options) {
|
|
|
4143
4157
|
});
|
|
4144
4158
|
}
|
|
4145
4159
|
function applyOpening(expired) {
|
|
4146
|
-
const progress = underway
|
|
4160
|
+
const progress = underway;
|
|
4161
|
+
underway = null;
|
|
4162
|
+
naming = Math.max(0, naming - 1);
|
|
4147
4163
|
const cycle = progress ?? { complete: true, reachable: true };
|
|
4148
|
-
if (opened === generation)
|
|
4149
|
-
opened = null;
|
|
4150
4164
|
const reloading = progress === null ? [] : owedReloads(expired);
|
|
4151
4165
|
const replacing = reloadNow((room) => expired.includes(room.locator) || reloading.includes(room.locator), cycle);
|
|
4152
4166
|
return task(OPENING_LANE, async (live) => {
|
|
4153
|
-
underway = null;
|
|
4154
4167
|
if (!live())
|
|
4155
4168
|
return;
|
|
4156
4169
|
if (progress === null) {
|
|
@@ -4158,24 +4171,14 @@ export function composeRuntime(options) {
|
|
|
4158
4171
|
await rest(cycle, live);
|
|
4159
4172
|
return;
|
|
4160
4173
|
}
|
|
4161
|
-
await roomsCarried([...expired, ...reloading]);
|
|
4162
|
-
if (!live())
|
|
4163
|
-
return;
|
|
4164
4174
|
try {
|
|
4165
4175
|
const local = await openLocal();
|
|
4166
|
-
|
|
4176
|
+
await replacing;
|
|
4167
4177
|
if (progress.reachable) {
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
!reloading.includes(room.locator)) {
|
|
4172
|
-
await confirmModels(room, local);
|
|
4173
|
-
}
|
|
4174
|
-
}
|
|
4178
|
+
await settleModels([...discovered.values()].filter((room) => replicas.has(room.locator) &&
|
|
4179
|
+
!expired.includes(room.locator) &&
|
|
4180
|
+
!reloading.includes(room.locator)), 'current', local);
|
|
4175
4181
|
}
|
|
4176
|
-
const failed = reloaded.find((outcome) => outcome.status === 'rejected');
|
|
4177
|
-
if (failed !== undefined)
|
|
4178
|
-
throw failed.reason;
|
|
4179
4182
|
}
|
|
4180
4183
|
catch {
|
|
4181
4184
|
progress.complete = false;
|
|
@@ -4187,11 +4190,8 @@ export function composeRuntime(options) {
|
|
|
4187
4190
|
}
|
|
4188
4191
|
function owedReloads(expired) {
|
|
4189
4192
|
const reloading = [...discovered.values()]
|
|
4190
|
-
.filter((room) => replicas.has(room.locator) &&
|
|
4191
|
-
!expired.includes(room.locator) &&
|
|
4192
|
-
(owed.has(room.locator) || revoked(room.locator)))
|
|
4193
|
+
.filter((room) => replicas.has(room.locator) && !expired.includes(room.locator) && revoked(room.locator))
|
|
4193
4194
|
.map((room) => room.locator);
|
|
4194
|
-
owed.clear();
|
|
4195
4195
|
for (const locator of reloading)
|
|
4196
4196
|
catching.add(locator);
|
|
4197
4197
|
liveQueries.recover();
|
|
@@ -4200,24 +4200,43 @@ export function composeRuntime(options) {
|
|
|
4200
4200
|
async function reloadNow(replaced, progress) {
|
|
4201
4201
|
const reloaded = [...discovered.values()].filter((room) => replicas.has(room.locator) && replaced(room));
|
|
4202
4202
|
for (const room of reloaded)
|
|
4203
|
-
reloadsDue.
|
|
4203
|
+
reloadsDue.add(room.locator);
|
|
4204
4204
|
try {
|
|
4205
4205
|
const local = await openLocal();
|
|
4206
|
-
|
|
4206
|
+
for (const room of reloaded) {
|
|
4207
|
+
void roomTask(room.locator, (live) => reload(room, local, progress, live)).then(() => rest(progress));
|
|
4208
|
+
}
|
|
4207
4209
|
}
|
|
4208
4210
|
catch {
|
|
4209
4211
|
for (const room of reloaded)
|
|
4210
4212
|
reloadsDue.delete(room.locator);
|
|
4211
4213
|
progress.reachable = false;
|
|
4212
|
-
return [];
|
|
4213
4214
|
}
|
|
4214
4215
|
}
|
|
4215
|
-
async function
|
|
4216
|
-
|
|
4217
|
-
const model = modelOf(room.locator
|
|
4218
|
-
|
|
4219
|
-
|
|
4216
|
+
async function settleModels(held, status, local) {
|
|
4217
|
+
const owed = (room) => {
|
|
4218
|
+
const model = modelOf(room.locator);
|
|
4219
|
+
return model.status !== 'unavailable' && model.status !== status;
|
|
4220
|
+
};
|
|
4221
|
+
const settled = await exclusive(async () => {
|
|
4222
|
+
if (!continuing())
|
|
4223
|
+
return [];
|
|
4224
|
+
const unchanged = held.filter((room) => owed(room) && declaredAsHeld(room));
|
|
4225
|
+
for (const room of unchanged) {
|
|
4226
|
+
const next = { ...modelOf(room.locator), status };
|
|
4227
|
+
const replica = replicas.get(room.locator);
|
|
4228
|
+
if (replica !== undefined)
|
|
4229
|
+
replicas.set(room.locator, { ...replica, record: { ...replica.record, model: next } });
|
|
4230
|
+
heldModel(room, next);
|
|
4220
4231
|
}
|
|
4232
|
+
if (unchanged.length > 0)
|
|
4233
|
+
emitRooms();
|
|
4234
|
+
return unchanged;
|
|
4235
|
+
});
|
|
4236
|
+
for (const room of held) {
|
|
4237
|
+
if (settled.includes(room) || !owed(room))
|
|
4238
|
+
continue;
|
|
4239
|
+
await retainModel(local, room, { ...modelOf(room.locator), status });
|
|
4221
4240
|
}
|
|
4222
4241
|
}
|
|
4223
4242
|
function submitLater(locator) {
|
|
@@ -4262,14 +4281,10 @@ export function composeRuntime(options) {
|
|
|
4262
4281
|
try {
|
|
4263
4282
|
const local = await openLocal();
|
|
4264
4283
|
await roomTask(locator, async (live) => {
|
|
4265
|
-
|
|
4266
|
-
if (!current() || authorization === undefined)
|
|
4284
|
+
if (!live() || authorization === undefined)
|
|
4267
4285
|
return;
|
|
4268
|
-
const withheld = await
|
|
4269
|
-
|
|
4270
|
-
abandonedOn(signal),
|
|
4271
|
-
]);
|
|
4272
|
-
if (withheld !== null && current())
|
|
4286
|
+
const withheld = await advance(room, local, authorization, live);
|
|
4287
|
+
if (withheld !== null && live())
|
|
4273
4288
|
await withhold(withheld, room, local, progress);
|
|
4274
4289
|
});
|
|
4275
4290
|
}
|
|
@@ -4324,14 +4339,11 @@ export function composeRuntime(options) {
|
|
|
4324
4339
|
progress.complete = false;
|
|
4325
4340
|
return;
|
|
4326
4341
|
}
|
|
4327
|
-
reads.applied();
|
|
4328
4342
|
submitAll();
|
|
4329
4343
|
for (const locator of stale)
|
|
4330
4344
|
if (repairable.has(locator))
|
|
4331
4345
|
repairLater(locator, {});
|
|
4332
4346
|
await conversations.reconcile();
|
|
4333
|
-
if (status.providerUnreachable)
|
|
4334
|
-
conversations.reconnected();
|
|
4335
4347
|
const reading = { complete: true, reachable: true };
|
|
4336
4348
|
await conversations.catchUp(reading);
|
|
4337
4349
|
if (progress.reachable && reading.reachable)
|
|
@@ -4357,7 +4369,7 @@ export function composeRuntime(options) {
|
|
|
4357
4369
|
if (!live())
|
|
4358
4370
|
return;
|
|
4359
4371
|
let lastCompleted = status.lastCompleted;
|
|
4360
|
-
let synced = complete && stale.size === 0 && !pending();
|
|
4372
|
+
let synced = complete && roomWork === 0 && naming === 0 && stale.size === 0 && !pending();
|
|
4361
4373
|
if (synced && !recorded) {
|
|
4362
4374
|
const completedAt = new Date().toISOString();
|
|
4363
4375
|
try {
|
|
@@ -4372,74 +4384,79 @@ export function composeRuntime(options) {
|
|
|
4372
4384
|
}
|
|
4373
4385
|
}
|
|
4374
4386
|
moveTo({
|
|
4375
|
-
state: pending()
|
|
4387
|
+
state: pending()
|
|
4388
|
+
? 'pending'
|
|
4389
|
+
: synced && complete && roomWork === 0 && naming === 0
|
|
4390
|
+
? 'synced'
|
|
4391
|
+
: 'syncing',
|
|
4376
4392
|
lastCompleted,
|
|
4377
|
-
providerUnreachable: !progress.reachable,
|
|
4393
|
+
providerUnreachable: progress === null ? status.providerUnreachable : !progress.reachable,
|
|
4378
4394
|
...attention(),
|
|
4379
4395
|
});
|
|
4380
4396
|
}
|
|
4381
4397
|
function discover() {
|
|
4398
|
+
discovering?.release();
|
|
4382
4399
|
let release = () => { };
|
|
4383
4400
|
const done = new Promise((resolve) => {
|
|
4384
4401
|
release = resolve;
|
|
4385
4402
|
});
|
|
4386
|
-
discovering = {
|
|
4403
|
+
discovering = { done, release };
|
|
4387
4404
|
return discovering;
|
|
4388
4405
|
}
|
|
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));
|
|
4406
|
+
function applyRoom(change) {
|
|
4407
|
+
const { locator } = change;
|
|
4408
|
+
if (stopping)
|
|
4409
|
+
return Promise.resolve();
|
|
4410
|
+
return inRoom(locator, async (live) => {
|
|
4411
|
+
await discovering?.done;
|
|
4412
|
+
await answering.get(locator)?.answered;
|
|
4413
|
+
const progress = underway ?? { complete: true, reachable: true };
|
|
4412
4414
|
try {
|
|
4413
4415
|
const local = await openLocal();
|
|
4414
|
-
|
|
4415
|
-
if (!live()
|
|
4416
|
-
return;
|
|
4417
|
-
const
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4416
|
+
for (;;) {
|
|
4417
|
+
if (!live())
|
|
4418
|
+
return null;
|
|
4419
|
+
const room = discovered.get(locator);
|
|
4420
|
+
const held = replicas.get(locator);
|
|
4421
|
+
if (room === undefined ||
|
|
4422
|
+
held === undefined ||
|
|
4423
|
+
room.replica.admission !== 'admitted' ||
|
|
4424
|
+
held.record.replica.selection !== room.replica.selection) {
|
|
4425
|
+
return null;
|
|
4426
|
+
}
|
|
4427
|
+
const at = held.record.roomVersion;
|
|
4428
|
+
if (options.adapter.reached(locator, at, change.roomVersion))
|
|
4429
|
+
return progress;
|
|
4430
|
+
if (!options.adapter.reached(locator, at, change.from)) {
|
|
4431
|
+
name(locator);
|
|
4432
|
+
return progress;
|
|
4433
|
+
}
|
|
4434
|
+
if (underway === null)
|
|
4435
|
+
moveTo(transferring(status));
|
|
4436
|
+
const writing = () => live() && !full;
|
|
4437
|
+
const refreshed = await ahead((hold) => pullRefresh(authorization ?? {}, held, writing, hold, () => change.read()));
|
|
4438
|
+
if (refreshed.kind !== 'advanced')
|
|
4439
|
+
return progress;
|
|
4440
|
+
const landing = {
|
|
4441
|
+
kind: 'refreshed',
|
|
4442
|
+
from: refreshed.from,
|
|
4443
|
+
remodelled: refreshed.remodelled,
|
|
4444
|
+
};
|
|
4445
|
+
if (await publish(local, room, refreshed.record, landing, writing)) {
|
|
4446
|
+
stale.delete(locator);
|
|
4447
|
+
await retainModel(local, room, modelOf(locator));
|
|
4448
|
+
return progress;
|
|
4449
|
+
}
|
|
4450
|
+
}
|
|
4421
4451
|
}
|
|
4422
4452
|
catch {
|
|
4453
|
+
if (!stale.has(locator))
|
|
4454
|
+
name(locator);
|
|
4423
4455
|
stale.add(locator);
|
|
4456
|
+
return progress;
|
|
4424
4457
|
}
|
|
4425
|
-
|
|
4426
|
-
if (during === null)
|
|
4427
|
-
await rest(progress, live);
|
|
4428
|
-
}
|
|
4429
|
-
});
|
|
4458
|
+
}).then((progress) => rest(progress), () => undefined);
|
|
4430
4459
|
}
|
|
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
4460
|
function observeStream() {
|
|
4444
4461
|
const stream = options.adapter.roomStream;
|
|
4445
4462
|
if (stream === undefined || withdrawStream !== undefined)
|
|
@@ -4449,9 +4466,10 @@ export function composeRuntime(options) {
|
|
|
4449
4466
|
return;
|
|
4450
4467
|
if (event.kind === 'opened') {
|
|
4451
4468
|
socketOpen = true;
|
|
4469
|
+
handed = false;
|
|
4470
|
+
naming = 0;
|
|
4452
4471
|
connectionLive();
|
|
4453
4472
|
generation += 1;
|
|
4454
|
-
reads.opened();
|
|
4455
4473
|
conversations.reopened();
|
|
4456
4474
|
recovery.resume();
|
|
4457
4475
|
void sync('opened');
|
|
@@ -4463,15 +4481,8 @@ export function composeRuntime(options) {
|
|
|
4463
4481
|
else if (event.kind === 'authority-changed')
|
|
4464
4482
|
void rediscover(event.scope);
|
|
4465
4483
|
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]]));
|
|
4484
|
+
void applyRoom(event);
|
|
4485
|
+
});
|
|
4475
4486
|
}
|
|
4476
4487
|
function writesHeld() {
|
|
4477
4488
|
return serviceMode === 'write-halted' || haltedRefusal;
|
|
@@ -4541,8 +4552,9 @@ export function composeRuntime(options) {
|
|
|
4541
4552
|
if (closed)
|
|
4542
4553
|
return;
|
|
4543
4554
|
closed = true;
|
|
4544
|
-
|
|
4545
|
-
|
|
4555
|
+
notifyDiscovered();
|
|
4556
|
+
if (probing !== null)
|
|
4557
|
+
clearInterval(probing);
|
|
4546
4558
|
connectionLive();
|
|
4547
4559
|
lockListeners.clear();
|
|
4548
4560
|
readinessWatches.clear();
|
|
@@ -4553,11 +4565,8 @@ export function composeRuntime(options) {
|
|
|
4553
4565
|
withdrawServiceMode?.();
|
|
4554
4566
|
conversations.close();
|
|
4555
4567
|
void recovery.retire();
|
|
4556
|
-
store?.close();
|
|
4557
|
-
void opening?.then((opened) => {
|
|
4558
|
-
opened.close();
|
|
4559
|
-
}, () => { });
|
|
4560
4568
|
roomListeners.clear();
|
|
4569
|
+
unlistedRooms.clear();
|
|
4561
4570
|
refusalListeners.clear();
|
|
4562
4571
|
storageLossListeners.clear();
|
|
4563
4572
|
statusListeners.clear();
|
|
@@ -4570,6 +4579,7 @@ export function composeRuntime(options) {
|
|
|
4570
4579
|
}
|
|
4571
4580
|
async function stop() {
|
|
4572
4581
|
stopping = true;
|
|
4582
|
+
inFlight.abort();
|
|
4573
4583
|
await recovery.retire();
|
|
4574
4584
|
await Promise.all(working);
|
|
4575
4585
|
await mutations;
|
|
@@ -4583,9 +4593,9 @@ export function composeRuntime(options) {
|
|
|
4583
4593
|
observeStream();
|
|
4584
4594
|
void openLocal().then(() => {
|
|
4585
4595
|
if (capabilities.length > 0)
|
|
4586
|
-
|
|
4596
|
+
readyFromStore();
|
|
4587
4597
|
}, () => {
|
|
4588
|
-
|
|
4598
|
+
readyFromStore();
|
|
4589
4599
|
});
|
|
4590
4600
|
if (options.adapter.roomStream === undefined)
|
|
4591
4601
|
void sync('unstreamed');
|
|
@@ -4632,9 +4642,8 @@ export function composeRuntime(options) {
|
|
|
4632
4642
|
}
|
|
4633
4643
|
async function searched(input, reach, locked) {
|
|
4634
4644
|
const asked = searchPlanOf(input);
|
|
4635
|
-
const plan = { ...asked, limit: capped(asked.limit) };
|
|
4636
4645
|
const filter = {
|
|
4637
|
-
...filterOf(
|
|
4646
|
+
...filterOf(asked.documents ?? {}, roomFacts(reach), planReach(reach)),
|
|
4638
4647
|
locked,
|
|
4639
4648
|
};
|
|
4640
4649
|
const unlocking = (filter.rooms ?? [])
|
|
@@ -4643,65 +4652,41 @@ export function composeRuntime(options) {
|
|
|
4643
4652
|
for (;;) {
|
|
4644
4653
|
const cover = covered(filter);
|
|
4645
4654
|
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);
|
|
4655
|
+
const pending = [...unlocking, ...('pending' in cover ? cover.pending : [])];
|
|
4670
4656
|
const complete = pending.length === 0 && !('pending' in cover);
|
|
4657
|
+
const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
|
|
4671
4658
|
const answering = complete
|
|
4672
4659
|
? filter
|
|
4673
|
-
: {
|
|
4674
|
-
...filter,
|
|
4675
|
-
rooms: covering
|
|
4676
|
-
.filter(({ room }) => searchIndex.rooms().has(room))
|
|
4677
|
-
.map(({ room }) => room),
|
|
4678
|
-
};
|
|
4660
|
+
: { ...filter, rooms: reachedRooms(filter).filter((room) => !awaited.has(room)) };
|
|
4679
4661
|
const answered = covered(answering);
|
|
4680
4662
|
if ('pending' in answered)
|
|
4681
4663
|
continue;
|
|
4682
|
-
const
|
|
4683
|
-
const
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4664
|
+
const listed = conversations.searchable();
|
|
4665
|
+
const named = reach !== null && !reach.conversations
|
|
4666
|
+
? []
|
|
4667
|
+
: (asked.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
|
|
4668
|
+
const hits = await (await openLocal()).search({
|
|
4669
|
+
text: asked.text,
|
|
4670
|
+
limit: capped(asked.limit),
|
|
4671
|
+
after: asked.after,
|
|
4672
|
+
total: asked.total,
|
|
4673
|
+
documents: {
|
|
4674
|
+
plan: answering,
|
|
4675
|
+
rooms: answered.rooms,
|
|
4676
|
+
beneath: asked.collection
|
|
4677
|
+
? answered.rooms.map(({ locator, key }) => ({
|
|
4678
|
+
key,
|
|
4679
|
+
roots: replicas.get(locator)?.record.typedSelection?.roots ?? [],
|
|
4680
|
+
}))
|
|
4681
|
+
: [],
|
|
4682
|
+
},
|
|
4683
|
+
conversations: named,
|
|
4684
|
+
});
|
|
4685
|
+
if (JSON.stringify(covered(answering)) !== JSON.stringify(answered))
|
|
4690
4686
|
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
4687
|
const coverage = {
|
|
4702
4688
|
rooms: answered.coverage.rooms,
|
|
4703
|
-
conversations:
|
|
4704
|
-
index: searchIndex.generation(),
|
|
4689
|
+
conversations: named,
|
|
4705
4690
|
overlay: answered.coverage.overlay,
|
|
4706
4691
|
};
|
|
4707
4692
|
return complete
|
|
@@ -4758,12 +4743,13 @@ export function composeRuntime(options) {
|
|
|
4758
4743
|
};
|
|
4759
4744
|
}
|
|
4760
4745
|
if (topic === 'readiness') {
|
|
4761
|
-
return watchReadiness({ target: '', code: 'stored',
|
|
4746
|
+
return watchReadiness({ target: '', code: 'stored', space: null, search: 'unused' }, null, deliver);
|
|
4762
4747
|
}
|
|
4763
4748
|
if (topic === 'rooms') {
|
|
4764
|
-
|
|
4765
|
-
|
|
4749
|
+
unlistedRooms.add(deliver);
|
|
4750
|
+
emitKnownRooms();
|
|
4766
4751
|
return () => {
|
|
4752
|
+
unlistedRooms.delete(deliver);
|
|
4767
4753
|
roomListeners.delete(deliver);
|
|
4768
4754
|
};
|
|
4769
4755
|
}
|
|
@@ -4817,7 +4803,17 @@ export function composeRuntime(options) {
|
|
|
4817
4803
|
return openLocal().then(() => undefined);
|
|
4818
4804
|
},
|
|
4819
4805
|
rooms() {
|
|
4820
|
-
|
|
4806
|
+
if (closed)
|
|
4807
|
+
return Promise.resolve([]);
|
|
4808
|
+
return new Promise((settle) => {
|
|
4809
|
+
const answer = (value) => {
|
|
4810
|
+
unlistedRooms.delete(answer);
|
|
4811
|
+
roomListeners.delete(answer);
|
|
4812
|
+
settle(value);
|
|
4813
|
+
};
|
|
4814
|
+
unlistedRooms.add(answer);
|
|
4815
|
+
emitKnownRooms();
|
|
4816
|
+
});
|
|
4821
4817
|
},
|
|
4822
4818
|
query: ((input) => rowsQuery(planned(input, null))),
|
|
4823
4819
|
search: (input) => searched(input, null, []),
|
|
@@ -4827,7 +4823,7 @@ export function composeRuntime(options) {
|
|
|
4827
4823
|
typedSelection: rooms.get(locator)?.typedSelection === undefined ||
|
|
4828
4824
|
rooms.get(locator)?.typedSelection === null
|
|
4829
4825
|
? null
|
|
4830
|
-
: modelTypes(
|
|
4826
|
+
: modelTypes(builtIn),
|
|
4831
4827
|
};
|
|
4832
4828
|
},
|
|
4833
4829
|
roomDraft: {
|
|
@@ -4844,33 +4840,17 @@ export function composeRuntime(options) {
|
|
|
4844
4840
|
const record = roomDrafts.get(room)?.get(path);
|
|
4845
4841
|
return record === undefined ? null : ((await roomDraftsOf([record]))[0] ?? null);
|
|
4846
4842
|
},
|
|
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
|
-
});
|
|
4843
|
+
write(room, path, revision, input, next = roomDraftRevision()) {
|
|
4844
|
+
if (typeof next !== 'string' || next === '')
|
|
4845
|
+
return Promise.reject(runtimeError('room-draft-malformed'));
|
|
4846
|
+
return gathered(`room-draft\u0000${room}\u0000${path}`, { revision, input, next }, (earlier, later) => {
|
|
4847
|
+
if (later.revision !== earlier.next)
|
|
4848
|
+
return null;
|
|
4849
|
+
const parts = gatheredParts(earlier.input, later.input);
|
|
4850
|
+
return parts === null
|
|
4851
|
+
? null
|
|
4852
|
+
: { revision: earlier.revision, input: parts, next: later.next };
|
|
4853
|
+
}, (written) => writtenRoomDraft(room, path, written.revision, written.input, written.next));
|
|
4874
4854
|
},
|
|
4875
4855
|
discard(room, path, revision) {
|
|
4876
4856
|
return exclusive(async () => {
|
|
@@ -4897,13 +4877,13 @@ export function composeRuntime(options) {
|
|
|
4897
4877
|
});
|
|
4898
4878
|
},
|
|
4899
4879
|
write(app, value) {
|
|
4900
|
-
return
|
|
4880
|
+
return exclusive(() => keptPresentation(app, () => structuredClone(value)));
|
|
4901
4881
|
},
|
|
4902
4882
|
edit(app, members) {
|
|
4903
|
-
return
|
|
4904
|
-
...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(
|
|
4905
|
-
...Object.entries(
|
|
4906
|
-
]));
|
|
4883
|
+
return gathered(`presentation\u0000${app}`, structuredClone(members), (earlier, later) => ({ ...earlier, ...later }), (edited) => keptPresentation(app, (held) => Object.fromEntries([
|
|
4884
|
+
...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(edited, member)),
|
|
4885
|
+
...Object.entries(edited).filter(([, value]) => value !== null),
|
|
4886
|
+
])));
|
|
4907
4887
|
},
|
|
4908
4888
|
},
|
|
4909
4889
|
save(room, named, writtenBy) {
|
|
@@ -4922,7 +4902,8 @@ export function composeRuntime(options) {
|
|
|
4922
4902
|
part: readPart,
|
|
4923
4903
|
read: readBinaryPath,
|
|
4924
4904
|
link: linkBinaryPath,
|
|
4925
|
-
|
|
4905
|
+
show: showBinaryPath,
|
|
4906
|
+
attach: attachBinary,
|
|
4926
4907
|
},
|
|
4927
4908
|
async call(name, input) {
|
|
4928
4909
|
await ready;
|
|
@@ -4943,9 +4924,10 @@ export function composeRuntime(options) {
|
|
|
4943
4924
|
},
|
|
4944
4925
|
async retry() {
|
|
4945
4926
|
journal.record('retry');
|
|
4946
|
-
|
|
4927
|
+
unpreparable.clear();
|
|
4947
4928
|
queueReadiness();
|
|
4948
|
-
|
|
4929
|
+
if (full)
|
|
4930
|
+
await probed();
|
|
4949
4931
|
},
|
|
4950
4932
|
observe: observing(watch),
|
|
4951
4933
|
async reauthorize() {
|
|
@@ -4967,7 +4949,6 @@ export function composeRuntime(options) {
|
|
|
4967
4949
|
const reachable = {
|
|
4968
4950
|
md,
|
|
4969
4951
|
watch,
|
|
4970
|
-
referrerScope: (referrer) => options.adapter.referrerScope(referrer),
|
|
4971
4952
|
plan: planned,
|
|
4972
4953
|
query: (plan) => rowsQuery(plan),
|
|
4973
4954
|
watchPlan,
|
|
@@ -4977,6 +4958,7 @@ export function composeRuntime(options) {
|
|
|
4977
4958
|
lockedRooms,
|
|
4978
4959
|
watchLocks,
|
|
4979
4960
|
located,
|
|
4961
|
+
room: (locator) => listed().find((one) => one.locator === locator),
|
|
4980
4962
|
};
|
|
4981
4963
|
return {
|
|
4982
4964
|
md,
|
|
@@ -4991,6 +4973,10 @@ export function composeRuntime(options) {
|
|
|
4991
4973
|
journal.record(`persistence ${answer}`);
|
|
4992
4974
|
queueReadiness();
|
|
4993
4975
|
},
|
|
4994
|
-
within: (reach, port) =>
|
|
4976
|
+
within: (reach, port) => {
|
|
4977
|
+
if (full)
|
|
4978
|
+
void probed();
|
|
4979
|
+
return narrowed(reachable, reach, port);
|
|
4980
|
+
},
|
|
4995
4981
|
};
|
|
4996
4982
|
}
|