@agentshouse/mdruntime 0.1.0-cee9c8aa4cd86 → 0.1.0-cef5833f6c689
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 +11 -9
- package/dist/adapter.d.ts +96 -67
- 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 +17 -9
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +143 -94
- package/dist/chain.d.ts +2 -1
- package/dist/chain.d.ts.map +1 -1
- package/dist/client.d.ts +12 -12
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +54 -8
- package/dist/collection.d.ts +7 -4
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +24 -19
- package/dist/compile.d.ts +1 -6
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +118 -247
- package/dist/connection.d.ts +11 -11
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +1598 -1248
- package/dist/context.d.ts +2 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/{chat/context.js → context.js} +1 -4
- package/dist/conversation.d.ts +18 -15
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +268 -171
- 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 +3 -2
- package/dist/entry.d.ts.map +1 -1
- package/dist/entry.js +15 -6
- package/dist/file.d.ts +2 -11
- package/dist/file.d.ts.map +1 -1
- package/dist/file.js +6 -4
- package/dist/host.d.ts +3 -6
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +49 -35
- 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 +15 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -3
- package/dist/job.d.ts +14 -0
- package/dist/job.d.ts.map +1 -0
- package/dist/job.js +28 -0
- package/dist/live.d.ts +2 -1
- package/dist/live.d.ts.map +1 -1
- package/dist/live.js +9 -5
- 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 +11 -6
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +178 -101
- package/dist/messages.d.ts +2 -2
- package/dist/messages.d.ts.map +1 -1
- package/dist/messages.js +15 -16
- package/dist/model.d.ts +7 -3
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +7 -13
- package/dist/observation.d.ts +3 -2
- package/dist/observation.d.ts.map +1 -1
- package/dist/observation.js +8 -3
- 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 -205
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +3 -494
- package/dist/reach.d.ts +14 -9
- package/dist/reach.d.ts.map +1 -1
- package/dist/reach.js +81 -71
- package/dist/react.d.ts +3 -1
- package/dist/react.d.ts.map +1 -1
- package/dist/readiness.d.ts +13 -16
- 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 +10 -4
- package/dist/refresh.d.ts.map +1 -1
- package/dist/refresh.js +104 -30
- package/dist/replica.d.ts +6 -5
- package/dist/replica.d.ts.map +1 -1
- package/dist/replica.js +8 -7
- package/dist/room-draft.d.ts +22 -5
- package/dist/room-draft.d.ts.map +1 -1
- package/dist/room-draft.js +131 -51
- package/dist/rows.d.ts +8 -15
- package/dist/rows.d.ts.map +1 -1
- package/dist/rows.js +51 -151
- 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 +12 -63
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +9 -52
- 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 +32 -31
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +67 -63
- package/dist/stored.d.ts +42 -18
- package/dist/stored.d.ts.map +1 -1
- package/dist/stored.js +193 -194
- package/dist/tables.d.ts +60 -40
- package/dist/tables.d.ts.map +1 -1
- package/dist/tables.js +78 -62
- 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 +39 -97
- package/dist/windows.d.ts +1 -12
- package/dist/windows.d.ts.map +1 -1
- package/dist/windows.js +2 -75
- package/dist/wire.d.ts +6 -9
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +3 -3
- package/package.json +11 -18
- package/AGENTS.md +0 -320
- 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/earlier.d.ts +0 -10
- package/dist/chat/earlier.d.ts.map +0 -1
- package/dist/chat/earlier.js +0 -54
- package/dist/chat/house.d.ts +0 -25
- package/dist/chat/house.d.ts.map +0 -1
- package/dist/chat/house.js +0 -38
- package/dist/chat/input.d.ts +0 -11
- package/dist/chat/input.d.ts.map +0 -1
- package/dist/chat/input.js +0 -92
- package/dist/chat/kit.d.ts +0 -61
- package/dist/chat/kit.d.ts.map +0 -1
- package/dist/chat/kit.js +0 -123
- package/dist/chat/lasting.d.ts +0 -6
- package/dist/chat/lasting.d.ts.map +0 -1
- package/dist/chat/lasting.js +0 -19
- package/dist/chat/list.d.ts +0 -28
- package/dist/chat/list.d.ts.map +0 -1
- package/dist/chat/list.js +0 -103
- package/dist/chat/markdown.d.ts +0 -21
- package/dist/chat/markdown.d.ts.map +0 -1
- package/dist/chat/markdown.js +0 -246
- package/dist/chat/message.d.ts +0 -7
- package/dist/chat/message.d.ts.map +0 -1
- package/dist/chat/message.js +0 -254
- package/dist/chat/messages.d.ts +0 -35
- package/dist/chat/messages.d.ts.map +0 -1
- package/dist/chat/messages.js +0 -210
- package/dist/chat/name.d.ts +0 -7
- package/dist/chat/name.d.ts.map +0 -1
- package/dist/chat/name.js +0 -37
- package/dist/chat/opened.d.ts +0 -12
- package/dist/chat/opened.d.ts.map +0 -1
- package/dist/chat/opened.js +0 -19
- package/dist/chat/overlay.d.ts +0 -32
- package/dist/chat/overlay.d.ts.map +0 -1
- package/dist/chat/overlay.js +0 -103
- package/dist/chat/parts.d.ts +0 -58
- package/dist/chat/parts.d.ts.map +0 -1
- package/dist/chat/parts.js +0 -120
- package/dist/chat/pieces.d.ts +0 -62
- package/dist/chat/pieces.d.ts.map +0 -1
- package/dist/chat/pieces.js +0 -1110
- package/dist/chat/process.d.ts +0 -20
- package/dist/chat/process.d.ts.map +0 -1
- package/dist/chat/process.js +0 -32
- package/dist/chat/provider.d.ts +0 -70
- package/dist/chat/provider.d.ts.map +0 -1
- package/dist/chat/provider.js +0 -635
- package/dist/chat/question.d.ts +0 -9
- package/dist/chat/question.d.ts.map +0 -1
- package/dist/chat/question.js +0 -69
- package/dist/chat/questions.d.ts +0 -31
- package/dist/chat/questions.d.ts.map +0 -1
- package/dist/chat/questions.js +0 -111
- package/dist/chat/read.d.ts +0 -5
- package/dist/chat/read.d.ts.map +0 -1
- package/dist/chat/read.js +0 -13
- package/dist/chat/ready.d.ts +0 -37
- package/dist/chat/ready.d.ts.map +0 -1
- package/dist/chat/ready.js +0 -93
- package/dist/chat/room-file.d.ts +0 -8
- package/dist/chat/room-file.d.ts.map +0 -1
- package/dist/chat/room-file.js +0 -72
- package/dist/chat/steady.d.ts +0 -3
- package/dist/chat/steady.d.ts.map +0 -1
- package/dist/chat/steady.js +0 -26
- package/dist/chat/style.d.ts +0 -4
- package/dist/chat/style.d.ts.map +0 -1
- package/dist/chat/style.js +0 -307
- package/dist/chat.d.ts +0 -15
- package/dist/chat.d.ts.map +0 -1
- package/dist/chat.js +0 -8
- package/dist/fulltext.d.ts +0 -27
- package/dist/fulltext.d.ts.map +0 -1
- package/dist/fulltext.js +0 -368
- package/dist/reads.d.ts +0 -11
- package/dist/reads.d.ts.map +0 -1
- package/dist/reads.js +0 -98
- package/dist/words.d.ts +0 -19
- package/dist/words.d.ts.map +0 -1
- package/dist/words.js +0 -150
package/dist/connection.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { attachedModel,
|
|
1
|
+
import { bodied, identityOf, parseFrontmatter, plain, queryKey, 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
|
-
import {
|
|
5
|
+
import { planWithin, } 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';
|
|
@@ -12,29 +12,28 @@ 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
|
-
import {
|
|
35
|
-
import { beneath, normalize } from './paths.js';
|
|
33
|
+
import { roomDraftRevision } from './identity.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
39
|
function within(root, path) {
|
|
@@ -55,34 +54,44 @@ const CONVERSATION_LANE = 'conversations';
|
|
|
55
54
|
const ROOM_READS = 256;
|
|
56
55
|
const ROOM_ATTEMPTS = 16;
|
|
57
56
|
const ERASE_BATCH = 512;
|
|
58
|
-
const
|
|
59
|
-
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;
|
|
60
63
|
const QUERY_BYTES = 1 << 20;
|
|
61
64
|
const STAGE_BYTES = 1 << 20;
|
|
62
|
-
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;
|
|
63
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.
|
|
64
74
|
const LOCK_GRACE_MS = 120_000;
|
|
65
75
|
function operationOf(input) {
|
|
66
76
|
const operation = input?.operation_id;
|
|
67
77
|
return typeof operation === 'string' ? operation : null;
|
|
68
78
|
}
|
|
69
|
-
function
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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));
|
|
75
86
|
});
|
|
76
87
|
}
|
|
77
|
-
|
|
78
|
-
return `room ${locator}`;
|
|
79
|
-
}
|
|
88
|
+
const TEXT_READS = 4;
|
|
80
89
|
function pendingKey(locator, id) {
|
|
81
90
|
return `${locator}\u0000${id}`;
|
|
82
91
|
}
|
|
83
92
|
export function composeRuntime(options) {
|
|
84
93
|
const localSaves = options.adapter.localSaves === true;
|
|
85
|
-
const roomTypes = roomTypesOf(options.adapter.roomTypes);
|
|
94
|
+
const roomTypes = roomTypesOf(options.adapter.roomTypes, options.adapter.nameableRoomTypes);
|
|
86
95
|
let authorization = options.authorization;
|
|
87
96
|
let opening;
|
|
88
97
|
const fences = new Map();
|
|
@@ -92,44 +101,53 @@ export function composeRuntime(options) {
|
|
|
92
101
|
const fencedSince = (locator, since) => fences.has(locator) || (lastFenced.get(locator) ?? 0) > since;
|
|
93
102
|
let discoveries = 0;
|
|
94
103
|
const replicas = new FencedMap(isFenced);
|
|
104
|
+
const reopened = new Set();
|
|
105
|
+
const usableWhenHeld = options.usableWhenHeld ?? [];
|
|
95
106
|
const queuesWithoutReplica = new FencedMap(isFenced);
|
|
96
107
|
const pendings = new Map();
|
|
108
|
+
const landedAhead = new Map();
|
|
97
109
|
const seeds = new Map();
|
|
98
110
|
const seedWatchers = new Map();
|
|
99
111
|
const roomDrafts = new FencedMap(isFenced);
|
|
112
|
+
const attachedFiles = new FencedMap(isFenced);
|
|
100
113
|
const presentations = new Map();
|
|
101
114
|
let limits = [];
|
|
102
115
|
let capabilities = [];
|
|
103
116
|
const rooms = new FencedMap(isFenced);
|
|
104
117
|
const counted = new Map();
|
|
105
118
|
const unreadable = new Set();
|
|
106
|
-
const
|
|
107
|
-
const
|
|
108
|
-
const
|
|
119
|
+
const builtIn = builtInModel(roomTypes);
|
|
120
|
+
const typedPaths = options.adapter.typedPaths ?? [];
|
|
121
|
+
const models = new FencedMap(isFenced);
|
|
122
|
+
const liveModels = new FencedMap(isFenced);
|
|
109
123
|
const invalidModels = new Set();
|
|
110
124
|
const discovered = new Map();
|
|
111
|
-
|
|
125
|
+
let inFlight = new AbortController();
|
|
126
|
+
const refreshed = options.adapter.refresh?.bind(options.adapter);
|
|
127
|
+
const reads = {
|
|
128
|
+
bootstrap: (authorized, request) => readUntil(options.adapter.bootstrap(authorized, request), inFlight.signal),
|
|
129
|
+
...(refreshed === undefined
|
|
130
|
+
? {}
|
|
131
|
+
: {
|
|
132
|
+
refresh: (authorized, request) => readUntil(refreshed(authorized, request), inFlight.signal),
|
|
133
|
+
}),
|
|
134
|
+
reached: (locator, roomVersion, target) => options.adapter.reached(locator, roomVersion, target),
|
|
135
|
+
};
|
|
112
136
|
let roomsListed = false;
|
|
113
137
|
const scopedRefusals = new Map();
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
function modelKey(locator, attached) {
|
|
117
|
-
return `${attached} ${locator}`;
|
|
118
|
-
}
|
|
119
|
-
function modelOf(locator, attached = 'collection') {
|
|
120
|
-
return models.get(modelKey(locator, attached)) ?? NO_MODEL;
|
|
138
|
+
function modelOf(locator) {
|
|
139
|
+
return models.get(locator) ?? NO_MODEL;
|
|
121
140
|
}
|
|
122
|
-
function savedModel(locator
|
|
123
|
-
return invalidModels.has(
|
|
141
|
+
function savedModel(locator) {
|
|
142
|
+
return invalidModels.has(locator)
|
|
124
143
|
? { status: 'unavailable', reason: 'revoked' }
|
|
125
|
-
: modelOf(locator
|
|
144
|
+
: modelOf(locator);
|
|
145
|
+
}
|
|
146
|
+
function composed(record) {
|
|
147
|
+
return compositionOf(record, builtIn, typedPaths);
|
|
126
148
|
}
|
|
127
149
|
function compositionFor(record) {
|
|
128
|
-
return
|
|
129
|
-
...record,
|
|
130
|
-
model: savedModel(record.locator, 'collection'),
|
|
131
|
-
selectionModel: savedModel(record.locator, 'typed-selection'),
|
|
132
|
-
});
|
|
150
|
+
return composed({ ...record, model: savedModel(record.locator) });
|
|
133
151
|
}
|
|
134
152
|
let refusal = null;
|
|
135
153
|
let storageLoss = null;
|
|
@@ -139,29 +157,31 @@ export function composeRuntime(options) {
|
|
|
139
157
|
let closed = false;
|
|
140
158
|
let stopping = false;
|
|
141
159
|
let withdrawStream;
|
|
142
|
-
const announced = new Map();
|
|
143
160
|
let underway = null;
|
|
144
161
|
let streamed = null;
|
|
145
162
|
let handed = false;
|
|
163
|
+
let naming = 0;
|
|
146
164
|
const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
|
|
147
165
|
const repairing = new Set();
|
|
148
166
|
const calling = new Set();
|
|
149
167
|
const repairable = new Map();
|
|
150
|
-
let outcomeRetention = null;
|
|
151
168
|
const answering = new Map();
|
|
152
169
|
let complete = false;
|
|
153
170
|
let recorded = false;
|
|
154
171
|
const stale = new Set();
|
|
155
172
|
let generation = 0;
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const
|
|
159
|
-
const reloadsDue = new Map();
|
|
173
|
+
const reloadsDue = new Set();
|
|
174
|
+
const highest = new Map();
|
|
175
|
+
const beyondHorizon = new Set();
|
|
160
176
|
const lanes = new Map();
|
|
161
|
-
|
|
177
|
+
const roomLanes = new Map();
|
|
178
|
+
let roomWork = 0;
|
|
179
|
+
const roomReads = { free: ROOM_READS, waiting: [] };
|
|
162
180
|
const working = new Set();
|
|
163
181
|
let mutations = Promise.resolve();
|
|
182
|
+
let gathering = null;
|
|
164
183
|
const roomListeners = new Set();
|
|
184
|
+
const unlistedRooms = new Set();
|
|
165
185
|
const refusalListeners = new Set();
|
|
166
186
|
const storageLossListeners = new Set();
|
|
167
187
|
const statusListeners = new Set();
|
|
@@ -172,6 +192,8 @@ export function composeRuntime(options) {
|
|
|
172
192
|
let withdrawConversations;
|
|
173
193
|
let currencyQueued = null;
|
|
174
194
|
let socketOpen = false;
|
|
195
|
+
let announced = false;
|
|
196
|
+
let missedChange = false;
|
|
175
197
|
const confirmed = new Set();
|
|
176
198
|
let lockGrace = null;
|
|
177
199
|
const lockListeners = new Set();
|
|
@@ -181,49 +203,83 @@ export function composeRuntime(options) {
|
|
|
181
203
|
const serviceModeListeners = new Set();
|
|
182
204
|
let member = null;
|
|
183
205
|
const memberListeners = new Set();
|
|
184
|
-
|
|
206
|
+
let authorityChanges = 0;
|
|
207
|
+
const authorityChangeListeners = new Set();
|
|
185
208
|
const unpreparedDemand = new Set();
|
|
186
209
|
const unpreparable = new Set();
|
|
187
210
|
let preparing = false;
|
|
188
211
|
let targets = {};
|
|
189
212
|
let persistence = options.shell.persistence;
|
|
190
|
-
let
|
|
191
|
-
|
|
192
|
-
let
|
|
213
|
+
let full = false;
|
|
214
|
+
let probing = null;
|
|
215
|
+
let testing = null;
|
|
216
|
+
const database = {
|
|
217
|
+
async ask(request) {
|
|
218
|
+
try {
|
|
219
|
+
return await options.database.ask(request);
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
if (refusedForSpace(error))
|
|
223
|
+
filled();
|
|
224
|
+
throw error;
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
};
|
|
193
228
|
const readinessWatches = new Set();
|
|
194
229
|
let readinessQueued = false;
|
|
195
230
|
const catching = new Set();
|
|
196
231
|
const transfers = new FencedMap(isFenced);
|
|
232
|
+
const installs = new Set();
|
|
233
|
+
const arrivals = new Map();
|
|
234
|
+
const streaming = new Set();
|
|
235
|
+
const underived = new Map();
|
|
236
|
+
const unindexed = new Map();
|
|
237
|
+
const startedOver = new Map();
|
|
238
|
+
let arrivalReported = 0;
|
|
239
|
+
let streamsFree = STREAMS_HELD;
|
|
240
|
+
let queuedText = 0;
|
|
241
|
+
const installWaiters = [];
|
|
242
|
+
const stageBudget = {
|
|
243
|
+
bytes: STAGE_FLOOR * 4,
|
|
244
|
+
floor: STAGE_FLOOR,
|
|
245
|
+
ceiling: STAGE_BYTES,
|
|
246
|
+
aim: STAGE_AIM_MS,
|
|
247
|
+
};
|
|
248
|
+
const deriveBudget = {
|
|
249
|
+
bytes: DERIVE_FLOOR * 4,
|
|
250
|
+
floor: DERIVE_FLOOR,
|
|
251
|
+
ceiling: DERIVE_BYTES,
|
|
252
|
+
aim: DERIVE_AIM_MS,
|
|
253
|
+
};
|
|
254
|
+
const installTurns = { busy: false, ahead: 0, finishing: [], starting: [] };
|
|
197
255
|
const conversations = createConversationStore({
|
|
198
|
-
agents: declaredAgents,
|
|
199
|
-
documents: (declaration, after) => rowsQuery(planOf({
|
|
200
|
-
rooms: [declaration.locator],
|
|
201
|
-
types: [declaration.type],
|
|
202
|
-
select: {
|
|
203
|
-
fields: [
|
|
204
|
-
declaration.identity,
|
|
205
|
-
declaration.name,
|
|
206
|
-
...(declaration.retired === undefined ? [] : [declaration.retired]),
|
|
207
|
-
].map((field) => `/${escapePointer(field)}`),
|
|
208
|
-
},
|
|
209
|
-
limit: MAXIMUM_WINDOW,
|
|
210
|
-
...(after === undefined ? {} : { after }),
|
|
211
|
-
}, roomFacts())),
|
|
212
256
|
call: (name, input) => invokeAction(name, input),
|
|
257
|
+
history: (request) => readConversationHistory(request),
|
|
213
258
|
durably,
|
|
214
259
|
stream: () => options.adapter.conversations,
|
|
215
260
|
publish: publishChangeSet,
|
|
216
261
|
online: () => connectionOf() !== 'unreachable' && refusal === null,
|
|
262
|
+
full: () => full,
|
|
217
263
|
closed: () => closed,
|
|
218
264
|
unanswered: resendLater,
|
|
219
|
-
expired: (operation) => pastHorizon(operation),
|
|
220
265
|
});
|
|
221
266
|
const journal = createLog();
|
|
222
|
-
const keptFiles = openedFiles(
|
|
223
|
-
|
|
267
|
+
const keptFiles = openedFiles(database, isFenced);
|
|
268
|
+
const holding = new Map();
|
|
269
|
+
const unholdable = new Map();
|
|
270
|
+
let readyFromStore = () => { };
|
|
224
271
|
const ready = new Promise((resolve) => {
|
|
225
|
-
|
|
272
|
+
readyFromStore = resolve;
|
|
273
|
+
});
|
|
274
|
+
let notifyDiscovered = () => { };
|
|
275
|
+
const firstDiscovery = new Promise((resolve) => {
|
|
276
|
+
notifyDiscovered = resolve;
|
|
226
277
|
});
|
|
278
|
+
let roomsNamed = false;
|
|
279
|
+
function notifyReady() {
|
|
280
|
+
notifyDiscovered();
|
|
281
|
+
readyFromStore();
|
|
282
|
+
}
|
|
227
283
|
function rememberRoomDraft(record) {
|
|
228
284
|
const room = roomDrafts.get(record.locator) ?? new Map();
|
|
229
285
|
room.set(record.path, record);
|
|
@@ -243,7 +299,7 @@ export function composeRuntime(options) {
|
|
|
243
299
|
return undefined;
|
|
244
300
|
const local = await openLocal();
|
|
245
301
|
const slice = await stepped(slicing(await local.entries(held.record.key, 'visible', paths), paths));
|
|
246
|
-
return { ...slice, collection: collectionOf(slice.documents,
|
|
302
|
+
return { ...slice, collection: collectionOf(slice.documents, composed(held.record)) };
|
|
247
303
|
}
|
|
248
304
|
function roomDraftOf(record, visible) {
|
|
249
305
|
return structuredClone({
|
|
@@ -299,14 +355,40 @@ export function composeRuntime(options) {
|
|
|
299
355
|
const value = presentations.get(app);
|
|
300
356
|
return value === undefined ? null : structuredClone(value);
|
|
301
357
|
}
|
|
302
|
-
function
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
358
|
+
async function writtenRoomDraft(room, path, revision, input, next) {
|
|
359
|
+
const parts = readParts(input, path);
|
|
360
|
+
if (path === '' || normalize(path) !== path)
|
|
361
|
+
throw runtimeError('room-draft-malformed', `room-draft-malformed at ${path}: a path inside the Room, such as notes/a.md`);
|
|
362
|
+
const local = await openLocal();
|
|
363
|
+
const current = roomDrafts.get(room)?.get(path);
|
|
364
|
+
if ((current?.revision ?? null) !== revision) {
|
|
365
|
+
return staleWrite(current === undefined ? null : ((await roomDraftsOf([current]))[0] ?? null));
|
|
366
|
+
}
|
|
367
|
+
const file = parts.file === undefined
|
|
368
|
+
? undefined
|
|
369
|
+
: await heldFile(parts.file.blob, parts.file.base === undefined
|
|
370
|
+
? binaryBase(await visibleOf(room, [path]), path)
|
|
371
|
+
: parts.file.base);
|
|
372
|
+
const record = {
|
|
373
|
+
locator: room,
|
|
374
|
+
path,
|
|
375
|
+
revision: next,
|
|
376
|
+
parts: held(current?.parts, parts, file),
|
|
377
|
+
returned: current?.returned ?? null,
|
|
378
|
+
};
|
|
379
|
+
await local.keepRoomDraft(record, parts.file === undefined || file === undefined
|
|
380
|
+
? []
|
|
381
|
+
: [{ key: file.localId, blob: parts.file.blob }]);
|
|
382
|
+
rememberRoomDraft(record);
|
|
383
|
+
publishChangeSet(changeSet(room, { drafts: [path] }));
|
|
384
|
+
return { outcome: 'written', roomDraft: (await roomDraftsOf([record]))[0] ?? null };
|
|
385
|
+
}
|
|
386
|
+
async function keptPresentation(app, next) {
|
|
387
|
+
const local = await openLocal();
|
|
388
|
+
const stored = next(presentations.get(app));
|
|
389
|
+
await local.present({ app, value: stored });
|
|
390
|
+
presentations.set(app, stored);
|
|
391
|
+
publishChangeSet(changeSet('', { presentation: app }));
|
|
310
392
|
}
|
|
311
393
|
function emitPresentation(app) {
|
|
312
394
|
if (closed)
|
|
@@ -321,6 +403,20 @@ export function composeRuntime(options) {
|
|
|
321
403
|
(queuesWithoutReplica.size > 0 ||
|
|
322
404
|
[...replicas.values()].some((replica) => replica.queue.length > 0)));
|
|
323
405
|
}
|
|
406
|
+
function unsent() {
|
|
407
|
+
const changed = new Map();
|
|
408
|
+
for (const room of [...replicas.keys(), ...queuesWithoutReplica.keys()]) {
|
|
409
|
+
for (const { batch, state } of entriesOf(room)) {
|
|
410
|
+
if (state === 'local')
|
|
411
|
+
continue;
|
|
412
|
+
for (const { path } of batch.changes)
|
|
413
|
+
changed.set(`${room}\u0000${path}`, { room, path });
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return [...changed]
|
|
417
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
418
|
+
.map(([, change]) => change);
|
|
419
|
+
}
|
|
324
420
|
function attention() {
|
|
325
421
|
const attentionRequired = [];
|
|
326
422
|
for (const record of [...roomDrafts.values()].flatMap((room) => [...room.values()])) {
|
|
@@ -332,16 +428,9 @@ export function composeRuntime(options) {
|
|
|
332
428
|
returned: structuredClone(record.returned),
|
|
333
429
|
});
|
|
334
430
|
}
|
|
335
|
-
for (const [room, refusal] of sourceRefusals)
|
|
336
|
-
attentionRequired.push({ room, refusal: structuredClone(refusal) });
|
|
337
431
|
attentionRequired.sort((left, right) => {
|
|
338
432
|
if (left.room !== right.room)
|
|
339
433
|
return left.room < right.room ? -1 : 1;
|
|
340
|
-
if ('refusal' in left || 'refusal' in right) {
|
|
341
|
-
if ('refusal' in left && 'refusal' in right)
|
|
342
|
-
return 0;
|
|
343
|
-
return 'refusal' in left ? -1 : 1;
|
|
344
|
-
}
|
|
345
434
|
if (left.path !== right.path)
|
|
346
435
|
return left.path < right.path ? -1 : 1;
|
|
347
436
|
return 0;
|
|
@@ -353,7 +442,7 @@ export function composeRuntime(options) {
|
|
|
353
442
|
if (reset === null || submission.reset < reset)
|
|
354
443
|
reset = submission.reset;
|
|
355
444
|
}
|
|
356
|
-
return { attentionRequired, reset };
|
|
445
|
+
return { attentionRequired, unsent: unsent(), reset };
|
|
357
446
|
}
|
|
358
447
|
function documentsIn(locator) {
|
|
359
448
|
return replicas.get(locator)?.count ?? 0;
|
|
@@ -426,7 +515,7 @@ export function composeRuntime(options) {
|
|
|
426
515
|
const governed = governs(reach, room);
|
|
427
516
|
const attached = [
|
|
428
517
|
...(governed.collection ? [modelOf(room.locator)] : []),
|
|
429
|
-
...(governed.selection ? [
|
|
518
|
+
...(governed.selection ? [builtIn] : []),
|
|
430
519
|
];
|
|
431
520
|
return {
|
|
432
521
|
locator: room.locator,
|
|
@@ -442,11 +531,26 @@ export function composeRuntime(options) {
|
|
|
442
531
|
const value = listed();
|
|
443
532
|
for (const listener of roomListeners)
|
|
444
533
|
listener(value);
|
|
534
|
+
emitKnownRooms();
|
|
535
|
+
}
|
|
536
|
+
function emitKnownRooms() {
|
|
537
|
+
conversations.learned();
|
|
538
|
+
if (unlistedRooms.size === 0)
|
|
539
|
+
return;
|
|
540
|
+
if (!roomsListed && rooms.size === 0 && refusal === null && !status.providerUnreachable)
|
|
541
|
+
return;
|
|
542
|
+
const value = listed();
|
|
543
|
+
for (const listener of [...unlistedRooms]) {
|
|
544
|
+
unlistedRooms.delete(listener);
|
|
545
|
+
roomListeners.add(listener);
|
|
546
|
+
listener(value);
|
|
547
|
+
}
|
|
445
548
|
}
|
|
446
549
|
function emitRefusal() {
|
|
447
550
|
queueReadiness();
|
|
448
551
|
for (const listener of refusalListeners)
|
|
449
552
|
listener(refusal);
|
|
553
|
+
emitKnownRooms();
|
|
450
554
|
}
|
|
451
555
|
function emitStorageLoss() {
|
|
452
556
|
queueReadiness();
|
|
@@ -467,6 +571,7 @@ export function composeRuntime(options) {
|
|
|
467
571
|
if (closed)
|
|
468
572
|
return;
|
|
469
573
|
queueReadiness();
|
|
574
|
+
emitKnownRooms();
|
|
470
575
|
const next = status.state === 'synced' && !conversations.current()
|
|
471
576
|
? { ...status, state: 'syncing' }
|
|
472
577
|
: status;
|
|
@@ -480,9 +585,6 @@ export function composeRuntime(options) {
|
|
|
480
585
|
if (localSaves)
|
|
481
586
|
return Object.freeze({ batch, state: 'local' });
|
|
482
587
|
const state = pendings.get(pendingKey(locator, batch.id));
|
|
483
|
-
if (state?.accepted !== undefined) {
|
|
484
|
-
return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
|
|
485
|
-
}
|
|
486
588
|
return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
|
|
487
589
|
}
|
|
488
590
|
function entriesOf(locator) {
|
|
@@ -501,18 +603,25 @@ export function composeRuntime(options) {
|
|
|
501
603
|
function unpublished(error) {
|
|
502
604
|
journal.record(`publication failed ${error instanceof Error ? error.name : 'unknown'}`);
|
|
503
605
|
}
|
|
606
|
+
function indexed(locator) {
|
|
607
|
+
return replicas.get(locator)?.indexed === true;
|
|
608
|
+
}
|
|
504
609
|
async function prepareReplica(locator) {
|
|
505
610
|
for (;;) {
|
|
506
611
|
const held = replicas.get(locator);
|
|
507
|
-
if (held === undefined || closed ||
|
|
612
|
+
if (held === undefined || closed || held.indexed)
|
|
508
613
|
return;
|
|
509
614
|
const key = held.record.key;
|
|
510
|
-
const done = await exclusive(async () =>
|
|
511
|
-
|
|
615
|
+
const done = await exclusive(async () => {
|
|
616
|
+
const local = await openLocal();
|
|
617
|
+
return timed(deriveBudget, () => local.rederive(key, composed(held.record), deriveBudget.bytes, DERIVE_LONGEST_MS, DERIVE_FLOOR));
|
|
618
|
+
});
|
|
619
|
+
const now = replicas.get(locator);
|
|
620
|
+
if (now?.record.key !== key)
|
|
512
621
|
continue;
|
|
513
|
-
if (!done)
|
|
622
|
+
if (!done.done)
|
|
514
623
|
continue;
|
|
515
|
-
|
|
624
|
+
replicas.set(locator, { ...now, indexed: true });
|
|
516
625
|
publishChangeSet(changeSet(locator, { documents: null }));
|
|
517
626
|
liveQueries.recover();
|
|
518
627
|
queueReadiness();
|
|
@@ -520,7 +629,7 @@ export function composeRuntime(options) {
|
|
|
520
629
|
}
|
|
521
630
|
}
|
|
522
631
|
function unprepared() {
|
|
523
|
-
return [...unpreparedDemand, ...replicas.keys()].find((locator) => !
|
|
632
|
+
return [...unpreparedDemand, ...replicas.keys()].find((locator) => !indexed(locator) && !unpreparable.has(locator));
|
|
524
633
|
}
|
|
525
634
|
function unpreparedBy(locator, error) {
|
|
526
635
|
unpreparable.add(locator);
|
|
@@ -581,14 +690,14 @@ export function composeRuntime(options) {
|
|
|
581
690
|
pending.push({ room: locator, awaits: 'acquisition' });
|
|
582
691
|
continue;
|
|
583
692
|
}
|
|
584
|
-
if (!
|
|
693
|
+
if (!replica.indexed) {
|
|
585
694
|
pending.push({ room: locator, awaits: 'preparation' });
|
|
586
695
|
continue;
|
|
587
696
|
}
|
|
588
697
|
coverage.push({
|
|
589
698
|
room: locator,
|
|
590
699
|
generation: replica.record.transfer,
|
|
591
|
-
current: !(catching.has(locator) ||
|
|
700
|
+
current: !(catching.has(locator) || transfers.has(locator)),
|
|
592
701
|
});
|
|
593
702
|
read.push({ locator, key: replica.record.key });
|
|
594
703
|
for (const batch of replica.queue)
|
|
@@ -615,8 +724,6 @@ export function composeRuntime(options) {
|
|
|
615
724
|
return { listed: named.every((room) => replicas.has(room)), rooms: named };
|
|
616
725
|
}
|
|
617
726
|
function roomState(locator) {
|
|
618
|
-
if (sourceRefusals.has(locator))
|
|
619
|
-
return 'refused';
|
|
620
727
|
const listedRoom = discovered.get(locator);
|
|
621
728
|
if (roomsListed &&
|
|
622
729
|
(listedRoom === undefined ||
|
|
@@ -625,13 +732,13 @@ export function composeRuntime(options) {
|
|
|
625
732
|
return 'unadmitted';
|
|
626
733
|
if (!replicas.has(locator))
|
|
627
734
|
return 'acquiring';
|
|
628
|
-
if (!
|
|
735
|
+
if (!indexed(locator))
|
|
629
736
|
return 'preparing';
|
|
630
737
|
return 'complete';
|
|
631
738
|
}
|
|
632
739
|
function conversationCoverage(reach) {
|
|
633
740
|
const declared = reach === null || reach.conversations;
|
|
634
|
-
if (!declared || options.adapter.conversations === undefined
|
|
741
|
+
if (!declared || options.adapter.conversations === undefined)
|
|
635
742
|
return 'undeclared';
|
|
636
743
|
if (!conversations.known() || !conversations.current())
|
|
637
744
|
return 'acquiring';
|
|
@@ -646,25 +753,50 @@ export function composeRuntime(options) {
|
|
|
646
753
|
? 'connected'
|
|
647
754
|
: 'connecting';
|
|
648
755
|
}
|
|
649
|
-
function
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
}
|
|
756
|
+
function bytesOf(room, underway) {
|
|
757
|
+
const transfer = transfers.get(room);
|
|
758
|
+
if (transfer !== undefined) {
|
|
759
|
+
const owed = (unindexed.get(room) ?? []).reduce((sum, part) => sum + part.bytes, 0);
|
|
760
|
+
return {
|
|
761
|
+
done: transfer.done.bytes - Math.ceil(owed),
|
|
762
|
+
total: Math.max(arrivals.get(room) ?? 0, transfer.done.bytes),
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
const held = underway ? NOTHING_HELD : (replicas.get(room)?.record.inventory ?? NOTHING_HELD);
|
|
766
|
+
return { done: held.bytes, total: arrivals.get(room) ?? held.bytes };
|
|
767
|
+
}
|
|
768
|
+
function installationOf(coverage) {
|
|
769
|
+
const rooms = { done: 0, total: 0 };
|
|
770
|
+
const bytes = { done: 0, total: 0 };
|
|
771
|
+
let installing = 0;
|
|
772
|
+
let downloading = false;
|
|
773
|
+
for (const { room, state } of coverage) {
|
|
774
|
+
if (state === 'unadmitted')
|
|
775
|
+
continue;
|
|
776
|
+
rooms.total += 1;
|
|
777
|
+
if (state === 'complete')
|
|
778
|
+
rooms.done += 1;
|
|
779
|
+
const underway = state === 'acquiring' || (installs.has(room) && state !== 'complete');
|
|
780
|
+
if (underway)
|
|
781
|
+
installing += 1;
|
|
782
|
+
if (underway && (streaming.has(room) || !arrivals.has(room)))
|
|
783
|
+
downloading = true;
|
|
784
|
+
const now = bytesOf(room, underway);
|
|
785
|
+
const earlier = underway ? startedOver.get(room) : undefined;
|
|
786
|
+
bytes.done += Math.max(now.done, earlier?.done ?? 0);
|
|
787
|
+
bytes.total += Math.max(now.total, earlier?.total ?? 0);
|
|
788
|
+
}
|
|
789
|
+
const step = installing === 0
|
|
790
|
+
? null
|
|
791
|
+
: downloading
|
|
792
|
+
? 'downloading'
|
|
793
|
+
: bytes.done < bytes.total
|
|
794
|
+
? 'writing'
|
|
795
|
+
: 'replaying';
|
|
796
|
+
return { step, rooms, bytes };
|
|
664
797
|
}
|
|
665
798
|
function readinessFor(port, reach) {
|
|
666
799
|
const required = requiredRooms(reach);
|
|
667
|
-
const search = port.search === 'required';
|
|
668
800
|
const coverage = required.rooms.map((room) => ({ room, state: roomState(room) }));
|
|
669
801
|
const preparing = coverage
|
|
670
802
|
.filter((room) => room.state === 'preparing' && !unpreparable.has(room.room))
|
|
@@ -677,23 +809,21 @@ export function composeRuntime(options) {
|
|
|
677
809
|
listed: required.listed,
|
|
678
810
|
rooms: coverage,
|
|
679
811
|
conversations: conversationCoverage(reach),
|
|
680
|
-
search: search ? 'required' : 'unused',
|
|
681
812
|
},
|
|
682
813
|
persistence,
|
|
683
814
|
connection: connectionOf(),
|
|
684
815
|
house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
? capacity
|
|
688
|
-
: port.capacity,
|
|
689
|
-
refused: refusedIn(required.rooms),
|
|
816
|
+
space: full ? 'full' : null,
|
|
817
|
+
installation: installationOf(coverage),
|
|
690
818
|
lost: storageLoss !== null,
|
|
691
|
-
established: targets[port.target]?.established === true
|
|
819
|
+
established: targets[port.target]?.established === true ||
|
|
820
|
+
(usableWhenHeld.includes(port.target) && required.rooms.some((room) => reopened.has(room))),
|
|
692
821
|
});
|
|
693
822
|
}
|
|
694
823
|
function establish(name) {
|
|
695
824
|
targets = { ...targets, [name]: { established: true } };
|
|
696
825
|
queueReadiness();
|
|
826
|
+
probeEachMinute();
|
|
697
827
|
void durably((local) => local.fact('targets', targets)).catch(() => {
|
|
698
828
|
journal.record(`target record failed ${name}`);
|
|
699
829
|
});
|
|
@@ -731,87 +861,56 @@ export function composeRuntime(options) {
|
|
|
731
861
|
readinessWatches.delete(watch);
|
|
732
862
|
};
|
|
733
863
|
}
|
|
734
|
-
function
|
|
735
|
-
|
|
736
|
-
for (const { path, content } of page.entries)
|
|
737
|
-
bytes += (path.length + (content?.length ?? 0)) * 2;
|
|
738
|
-
return bytes;
|
|
739
|
-
}
|
|
740
|
-
function exceeded(error) {
|
|
741
|
-
return (typeof error === 'object' &&
|
|
742
|
-
error !== null &&
|
|
743
|
-
error.name === 'QuotaExceededError');
|
|
744
|
-
}
|
|
745
|
-
async function estimated() {
|
|
746
|
-
const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
|
|
747
|
-
return estimate === null ? null : Math.max(0, estimate.total - estimate.usage);
|
|
748
|
-
}
|
|
749
|
-
function pause(locator, cause) {
|
|
750
|
-
paused.add(locator);
|
|
751
|
-
capacity = cause;
|
|
752
|
-
journal.record(`capacity ${cause.cause} ${locator} ${cause.needed} ${cause.available ?? '-'}`);
|
|
753
|
-
queueReadiness();
|
|
754
|
-
if (rechecking !== null)
|
|
864
|
+
function filled() {
|
|
865
|
+
if (full || closed)
|
|
755
866
|
return;
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
867
|
+
full = true;
|
|
868
|
+
journal.record('space full');
|
|
869
|
+
queueReadiness();
|
|
870
|
+
probeEachMinute();
|
|
760
871
|
}
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
return;
|
|
765
|
-
const available = await estimated().catch(() => null);
|
|
766
|
-
if (available === null || available < (held.needed ?? 0))
|
|
767
|
-
return;
|
|
768
|
-
if (held.cause === 'storage-exceeded' &&
|
|
769
|
-
(held.available === null || available <= held.available))
|
|
770
|
-
return;
|
|
771
|
-
resume();
|
|
872
|
+
function installedTarget() {
|
|
873
|
+
return ((usableWhenHeld.length > 0 && reopened.size > 0) ||
|
|
874
|
+
Object.values(targets).some((target) => target.established));
|
|
772
875
|
}
|
|
773
|
-
function
|
|
774
|
-
if (
|
|
775
|
-
clearInterval(rechecking);
|
|
776
|
-
rechecking = null;
|
|
777
|
-
if (paused.size === 0 && capacity === null)
|
|
876
|
+
function probeEachMinute() {
|
|
877
|
+
if (!full || probing !== null || closed || !installedTarget())
|
|
778
878
|
return;
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
if (!closed && authorization !== undefined)
|
|
784
|
-
void sync('unstreamed');
|
|
785
|
-
}
|
|
786
|
-
async function reclaimed(local) {
|
|
787
|
-
const unreferenced = [...transfers.keys()].filter((locator) => !discovered.has(locator));
|
|
788
|
-
for (const locator of unreferenced) {
|
|
789
|
-
await local.abandon(locator);
|
|
790
|
-
transfers.delete(locator);
|
|
791
|
-
}
|
|
792
|
-
if (unreferenced.length > 0)
|
|
793
|
-
journal.record(`capacity reclaimed ${unreferenced.length}`);
|
|
794
|
-
return unreferenced.length > 0;
|
|
879
|
+
// 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.
|
|
880
|
+
probing = setInterval(() => {
|
|
881
|
+
void probed();
|
|
882
|
+
}, 60_000);
|
|
795
883
|
}
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
if (available === null || available >= needed)
|
|
801
|
-
return true;
|
|
802
|
-
if (await reclaimed(local))
|
|
803
|
-
available = await estimated().catch(() => null);
|
|
804
|
-
if (available === null || available >= needed)
|
|
884
|
+
function probed() {
|
|
885
|
+
testing ??= database.ask({ kind: 'space.probe' }).then(() => {
|
|
886
|
+
testing = null;
|
|
887
|
+
spaceFreed();
|
|
805
888
|
return true;
|
|
806
|
-
|
|
807
|
-
|
|
889
|
+
}, () => {
|
|
890
|
+
testing = null;
|
|
891
|
+
return false;
|
|
892
|
+
});
|
|
893
|
+
return testing;
|
|
808
894
|
}
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
895
|
+
function spaceFreed() {
|
|
896
|
+
if (!full)
|
|
897
|
+
return;
|
|
898
|
+
full = false;
|
|
899
|
+
if (probing !== null)
|
|
900
|
+
clearInterval(probing);
|
|
901
|
+
probing = null;
|
|
902
|
+
unpreparable.clear();
|
|
903
|
+
journal.record('space freed');
|
|
904
|
+
queueReadiness();
|
|
905
|
+
if (closed || authorization === undefined)
|
|
906
|
+
return;
|
|
907
|
+
if (options.adapter.roomStream === undefined)
|
|
908
|
+
void sync('unstreamed', null);
|
|
909
|
+
else
|
|
910
|
+
void resynchronize();
|
|
812
911
|
}
|
|
813
912
|
function seedRoomHeld(room, version) {
|
|
814
|
-
return
|
|
913
|
+
return indexed(room) && reachedBy(room, version);
|
|
815
914
|
}
|
|
816
915
|
function seedHeld(at) {
|
|
817
916
|
return Object.entries(at).every(([room, version]) => (roomsListed && !rooms.has(room)) || seedRoomHeld(room, version));
|
|
@@ -848,7 +947,24 @@ export function composeRuntime(options) {
|
|
|
848
947
|
function sameCover(left, right) {
|
|
849
948
|
return JSON.stringify(left) === JSON.stringify(right);
|
|
850
949
|
}
|
|
950
|
+
async function unheldRows(local, plan, rows, rooms) {
|
|
951
|
+
if (plan.shape.kind !== 'documents' || !bodied(plan.shape))
|
|
952
|
+
return [];
|
|
953
|
+
if (!('documents' in rows) || !textOnline())
|
|
954
|
+
return [];
|
|
955
|
+
const named = new Map();
|
|
956
|
+
for (const { room, path, textHeld } of rows.documents) {
|
|
957
|
+
if (textHeld === false)
|
|
958
|
+
named.set(room, [...(named.get(room) ?? []), path]);
|
|
959
|
+
}
|
|
960
|
+
const unheld = [];
|
|
961
|
+
for (const { locator, key } of rooms) {
|
|
962
|
+
unheld.push(...(await unheldIn(local, locator, key, named.get(locator) ?? [])));
|
|
963
|
+
}
|
|
964
|
+
return unheld.filter((one) => unholdable.get(textKey(one)) !== one.revision);
|
|
965
|
+
}
|
|
851
966
|
async function coveredRows(plan, cover) {
|
|
967
|
+
const asked = new Set();
|
|
852
968
|
for (;;) {
|
|
853
969
|
const before = cover();
|
|
854
970
|
if ('pending' in before)
|
|
@@ -859,8 +975,14 @@ export function composeRuntime(options) {
|
|
|
859
975
|
const current = () => local.changes(keys) === changes;
|
|
860
976
|
const rows = await windowRows(local, plan, before.rooms, current);
|
|
861
977
|
const after = cover();
|
|
862
|
-
if (rows
|
|
978
|
+
if (rows === null || !sameCover(before, after))
|
|
979
|
+
continue;
|
|
980
|
+
const unheld = (await unheldRows(local, plan, rows, before.rooms)).filter((one) => !asked.has(textKey(one)));
|
|
981
|
+
if (unheld.length === 0)
|
|
863
982
|
return { rows, cover: before, current };
|
|
983
|
+
for (const one of unheld)
|
|
984
|
+
asked.add(textKey(one));
|
|
985
|
+
await holdTexts(unheld);
|
|
864
986
|
}
|
|
865
987
|
}
|
|
866
988
|
async function windowRows(local, plan, rooms, unchanged) {
|
|
@@ -934,6 +1056,10 @@ export function composeRuntime(options) {
|
|
|
934
1056
|
}
|
|
935
1057
|
function publishChangeSet(set) {
|
|
936
1058
|
applyChangeSet(set);
|
|
1059
|
+
for (const { locator, path } of set.documents) {
|
|
1060
|
+
if (path !== null)
|
|
1061
|
+
attachedFiles.get(locator)?.delete(path);
|
|
1062
|
+
}
|
|
937
1063
|
if (set.rooms.length > 0 ||
|
|
938
1064
|
[...new Set(set.documents.map(({ locator }) => locator))].some((locator) => rooms.has(locator) && counted.get(locator) !== documentsIn(locator)))
|
|
939
1065
|
emitRooms();
|
|
@@ -952,6 +1078,8 @@ export function composeRuntime(options) {
|
|
|
952
1078
|
}
|
|
953
1079
|
async function refuse(reason, provider = null) {
|
|
954
1080
|
discoveryPaused = true;
|
|
1081
|
+
inFlight.abort();
|
|
1082
|
+
inFlight = new AbortController();
|
|
955
1083
|
try {
|
|
956
1084
|
if (reason === 'revoked-authorization') {
|
|
957
1085
|
await exclusive(async () => {
|
|
@@ -959,27 +1087,24 @@ export function composeRuntime(options) {
|
|
|
959
1087
|
const records = [...replicas].map(([locator, held]) => ({
|
|
960
1088
|
locator,
|
|
961
1089
|
held,
|
|
962
|
-
record: { ...held.record, model: revoked
|
|
1090
|
+
record: { ...held.record, model: revoked },
|
|
963
1091
|
}));
|
|
1092
|
+
let answer = null;
|
|
964
1093
|
try {
|
|
965
1094
|
const local = await openLocal();
|
|
966
|
-
await local.commit({
|
|
1095
|
+
answer = await local.commit({
|
|
967
1096
|
rooms: records.map(({ locator, record }) => ({
|
|
968
1097
|
locator,
|
|
969
1098
|
key: record.key,
|
|
970
1099
|
record: storedRecord(record),
|
|
971
|
-
|
|
1100
|
+
composed: composedOf(composed(record)),
|
|
972
1101
|
})),
|
|
973
1102
|
});
|
|
974
1103
|
}
|
|
975
1104
|
catch {
|
|
976
1105
|
journal.record('model revocation persistence failed');
|
|
977
1106
|
}
|
|
978
|
-
const keys = new Set(models.keys());
|
|
979
|
-
for (const locator of rooms.keys()) {
|
|
980
|
-
keys.add(modelKey(locator, 'collection'));
|
|
981
|
-
keys.add(modelKey(locator, 'typed-selection'));
|
|
982
|
-
}
|
|
1107
|
+
const keys = new Set([...models.keys(), ...rooms.keys()]);
|
|
983
1108
|
for (const key of keys) {
|
|
984
1109
|
invalidModels.add(key);
|
|
985
1110
|
models.set(key, revoked);
|
|
@@ -989,8 +1114,8 @@ export function composeRuntime(options) {
|
|
|
989
1114
|
for (const { locator, held, record } of records) {
|
|
990
1115
|
if (replicas.get(locator) !== held)
|
|
991
1116
|
continue;
|
|
992
|
-
|
|
993
|
-
replicas.set(locator, { ...held, record });
|
|
1117
|
+
const indexed = answer === null ? held.indexed : committedRoom(locator, answer).indexed;
|
|
1118
|
+
replicas.set(locator, { ...held, record, indexed });
|
|
994
1119
|
}
|
|
995
1120
|
for (const locator of [...rooms.keys()]) {
|
|
996
1121
|
const room = rooms.get(locator);
|
|
@@ -1034,15 +1159,15 @@ export function composeRuntime(options) {
|
|
|
1034
1159
|
? null
|
|
1035
1160
|
: {
|
|
1036
1161
|
roots: selection.roots,
|
|
1037
|
-
model: modelTypes(
|
|
1162
|
+
model: modelTypes(builtIn),
|
|
1038
1163
|
},
|
|
1039
1164
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
1040
1165
|
roomVersion: replicas.get(room.locator)?.record.roomVersion ?? null,
|
|
1041
1166
|
synchronizationAvailable,
|
|
1042
|
-
refusal: scopedRefusals.get(room.locator) ??
|
|
1167
|
+
refusal: scopedRefusals.get(room.locator) ?? null,
|
|
1043
1168
|
});
|
|
1044
1169
|
}
|
|
1045
|
-
async function applyDiscovery(found, started,
|
|
1170
|
+
async function applyDiscovery(found, started, left) {
|
|
1046
1171
|
const lockedBefore = lockedRooms();
|
|
1047
1172
|
for (const room of found) {
|
|
1048
1173
|
const fence = fences.get(room.locator);
|
|
@@ -1056,18 +1181,11 @@ export function composeRuntime(options) {
|
|
|
1056
1181
|
const listedLocators = new Set(listed.map((room) => room.locator));
|
|
1057
1182
|
for (const locator of listedLocators)
|
|
1058
1183
|
scopedRefusals.delete(locator);
|
|
1059
|
-
if (
|
|
1184
|
+
if (left === undefined)
|
|
1060
1185
|
discovered.clear();
|
|
1061
|
-
else
|
|
1062
|
-
for (const locator of covered)
|
|
1063
|
-
discovered.delete(locator);
|
|
1064
1186
|
for (const room of listed)
|
|
1065
1187
|
discovered.set(room.locator, room);
|
|
1066
|
-
|
|
1067
|
-
if (options.adapter.exhaustiveDiscovery === true)
|
|
1068
|
-
await loseRooms(unlisted);
|
|
1069
|
-
else
|
|
1070
|
-
unlistedKept(unlisted);
|
|
1188
|
+
await loseRooms(left ?? unlistedHeld(new Set(found.map((room) => room.locator))));
|
|
1071
1189
|
for (const room of listed) {
|
|
1072
1190
|
const held = replicas.get(room.locator)?.record;
|
|
1073
1191
|
if (held !== undefined && held.replica.selection !== room.replica.selection) {
|
|
@@ -1098,15 +1216,6 @@ export function composeRuntime(options) {
|
|
|
1098
1216
|
]),
|
|
1099
1217
|
].filter((locator) => !listed.has(locator));
|
|
1100
1218
|
}
|
|
1101
|
-
function unlistedKept(unlisted) {
|
|
1102
|
-
for (const locator of unlisted) {
|
|
1103
|
-
const held = replicas.get(locator)?.record;
|
|
1104
|
-
if (held !== undefined)
|
|
1105
|
-
list(held, false);
|
|
1106
|
-
else
|
|
1107
|
-
rooms.delete(locator);
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
1219
|
function fence(locators) {
|
|
1111
1220
|
const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
|
|
1112
1221
|
const removed = fresh.map((locator) => ({
|
|
@@ -1117,31 +1226,29 @@ export function composeRuntime(options) {
|
|
|
1117
1226
|
fences.set(locator, { at: discoveries, erased: false });
|
|
1118
1227
|
lastFenced.set(locator, ++fencings);
|
|
1119
1228
|
confirmed.delete(locator);
|
|
1120
|
-
for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
|
|
1229
|
+
for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []) {
|
|
1121
1230
|
pendings.delete(pendingKey(locator, batch.id));
|
|
1231
|
+
landedAhead.delete(pendingKey(locator, batch.id));
|
|
1232
|
+
}
|
|
1122
1233
|
replicas.delete(locator);
|
|
1234
|
+
highest.delete(locator);
|
|
1235
|
+
beyondHorizon.delete(locator);
|
|
1123
1236
|
queuesWithoutReplica.delete(locator);
|
|
1124
1237
|
rooms.delete(locator);
|
|
1125
1238
|
roomDrafts.delete(locator);
|
|
1126
|
-
|
|
1239
|
+
attachedFiles.delete(locator);
|
|
1240
|
+
untransferred(locator);
|
|
1241
|
+
startedOver.delete(locator);
|
|
1127
1242
|
catching.delete(locator);
|
|
1128
1243
|
discovered.delete(locator);
|
|
1129
|
-
announced.delete(locator);
|
|
1130
1244
|
unreadable.delete(locator);
|
|
1131
1245
|
stale.delete(locator);
|
|
1132
1246
|
repairable.delete(locator);
|
|
1133
1247
|
unpreparable.delete(locator);
|
|
1134
|
-
paused.delete(locator);
|
|
1135
|
-
prepared.delete(locator);
|
|
1136
1248
|
scopedRefusals.delete(locator);
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
const key = modelKey(locator, attached);
|
|
1141
|
-
models.delete(key);
|
|
1142
|
-
liveModels.delete(key);
|
|
1143
|
-
invalidModels.delete(key);
|
|
1144
|
-
}
|
|
1249
|
+
models.delete(locator);
|
|
1250
|
+
liveModels.delete(locator);
|
|
1251
|
+
invalidModels.delete(locator);
|
|
1145
1252
|
journal.record(`room lost ${locator}`);
|
|
1146
1253
|
}
|
|
1147
1254
|
if (fresh.length === 0)
|
|
@@ -1189,7 +1296,14 @@ export function composeRuntime(options) {
|
|
|
1189
1296
|
});
|
|
1190
1297
|
await eraseFenced(fresh);
|
|
1191
1298
|
}
|
|
1192
|
-
function
|
|
1299
|
+
function retyped(persisted) {
|
|
1300
|
+
if (persisted.status === 'unavailable')
|
|
1301
|
+
return persisted;
|
|
1302
|
+
const { state } = attachedModel({ kind: 'model', snapshot: persisted.snapshot }, NO_MODEL, roomTypes);
|
|
1303
|
+
return state.status === 'unavailable' ? state : { ...persisted, types: state.types };
|
|
1304
|
+
}
|
|
1305
|
+
function restored(key, held) {
|
|
1306
|
+
const persisted = retyped(held);
|
|
1193
1307
|
const prior = models.get(key);
|
|
1194
1308
|
if (prior !== undefined &&
|
|
1195
1309
|
prior.status !== 'unavailable' &&
|
|
@@ -1220,8 +1334,9 @@ export function composeRuntime(options) {
|
|
|
1220
1334
|
const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
|
|
1221
1335
|
targets = held.targets;
|
|
1222
1336
|
transfers.clear();
|
|
1223
|
-
|
|
1224
|
-
|
|
1337
|
+
arrivals.clear();
|
|
1338
|
+
streaming.clear();
|
|
1339
|
+
startedOver.clear();
|
|
1225
1340
|
limits = held.providerLimits;
|
|
1226
1341
|
capabilities = held.capabilities;
|
|
1227
1342
|
member = held.member;
|
|
@@ -1231,15 +1346,16 @@ export function composeRuntime(options) {
|
|
|
1231
1346
|
presentations.clear();
|
|
1232
1347
|
for (const record of held.presentation)
|
|
1233
1348
|
presentations.set(record.app, record.value);
|
|
1234
|
-
conversations.hydrate(heldConversations, chatDrafts);
|
|
1349
|
+
conversations.hydrate(heldConversations, chatDrafts, held.conversationList, held.conversationCursor);
|
|
1235
1350
|
pendings.clear();
|
|
1351
|
+
landedAhead.clear();
|
|
1236
1352
|
for (const queue of queues) {
|
|
1237
1353
|
for (const { batch, state } of queue.pending) {
|
|
1238
1354
|
pendings.set(pendingKey(queue.locator, batch.id), state);
|
|
1239
1355
|
}
|
|
1240
1356
|
}
|
|
1241
1357
|
replicas.clear();
|
|
1242
|
-
|
|
1358
|
+
reopened.clear();
|
|
1243
1359
|
queuesWithoutReplica.clear();
|
|
1244
1360
|
for (const [locator, queue] of queuedBatches) {
|
|
1245
1361
|
if (!held.rooms.has(locator))
|
|
@@ -1248,39 +1364,40 @@ export function composeRuntime(options) {
|
|
|
1248
1364
|
records.forEach((stored, index) => {
|
|
1249
1365
|
if (fences.has(stored.locator))
|
|
1250
1366
|
return;
|
|
1251
|
-
const
|
|
1252
|
-
|
|
1253
|
-
const record = { ...stored, model, selectionModel };
|
|
1367
|
+
const record = { ...stored, model: restored(stored.locator, stored.model) };
|
|
1368
|
+
reopened.add(record.locator);
|
|
1254
1369
|
replicas.set(record.locator, {
|
|
1255
1370
|
record,
|
|
1256
1371
|
queue: queuedBatches.get(record.locator) ?? [],
|
|
1257
1372
|
overlaid: held.overlaid.get(record.key) ?? new Set(),
|
|
1258
1373
|
count: counts[index] ?? 0,
|
|
1259
1374
|
confirmed: 0,
|
|
1375
|
+
indexed: !held.unindexed.has(record.key),
|
|
1260
1376
|
});
|
|
1261
|
-
if (!held.unprepared.has(record.key))
|
|
1262
|
-
prepared.add(record.locator);
|
|
1263
1377
|
const existing = rooms.get(record.locator);
|
|
1264
1378
|
if (existing === undefined)
|
|
1265
|
-
list(record,
|
|
1379
|
+
list(record, record.roomVersion !== null);
|
|
1266
1380
|
else {
|
|
1267
1381
|
rooms.set(record.locator, {
|
|
1268
1382
|
...existing,
|
|
1269
1383
|
roomVersion: record.roomVersion,
|
|
1270
1384
|
operations: record.operations,
|
|
1271
1385
|
replica: record.replica,
|
|
1272
|
-
model: modelTypes(model),
|
|
1386
|
+
model: modelTypes(record.model),
|
|
1273
1387
|
typedSelection: record.typedSelection === null
|
|
1274
1388
|
? null
|
|
1275
|
-
: {
|
|
1276
|
-
roots: record.typedSelection.roots,
|
|
1277
|
-
model: modelTypes(selectionModel),
|
|
1278
|
-
},
|
|
1389
|
+
: { roots: record.typedSelection.roots, model: modelTypes(builtIn) },
|
|
1279
1390
|
});
|
|
1280
1391
|
}
|
|
1281
1392
|
});
|
|
1282
|
-
|
|
1283
|
-
|
|
1393
|
+
try {
|
|
1394
|
+
for (const key of held.orphans)
|
|
1395
|
+
await opened.release(key);
|
|
1396
|
+
}
|
|
1397
|
+
catch (error) {
|
|
1398
|
+
if (!refusedForSpace(error))
|
|
1399
|
+
throw error;
|
|
1400
|
+
}
|
|
1284
1401
|
if (held.erasing || held.orphans.length > 0)
|
|
1285
1402
|
queueMicrotask(sweep);
|
|
1286
1403
|
return {
|
|
@@ -1305,7 +1422,7 @@ export function composeRuntime(options) {
|
|
|
1305
1422
|
async function open() {
|
|
1306
1423
|
if (closed)
|
|
1307
1424
|
throw new Error('runtime closed');
|
|
1308
|
-
const opened = openRuntimeStore(isFenced,
|
|
1425
|
+
const opened = openRuntimeStore(isFenced, database);
|
|
1309
1426
|
const interrupted = await opened.fences();
|
|
1310
1427
|
for (const locator of interrupted) {
|
|
1311
1428
|
fences.set(locator, { at: discoveries, erased: false });
|
|
@@ -1351,10 +1468,29 @@ export function composeRuntime(options) {
|
|
|
1351
1468
|
return opening;
|
|
1352
1469
|
}
|
|
1353
1470
|
function exclusive(work) {
|
|
1471
|
+
gathering = null;
|
|
1354
1472
|
const next = mutations.then(work, work);
|
|
1355
1473
|
mutations = next.then(() => undefined, () => undefined);
|
|
1356
1474
|
return next;
|
|
1357
1475
|
}
|
|
1476
|
+
function gathered(key, input, merge, work) {
|
|
1477
|
+
const tail = gathering;
|
|
1478
|
+
if (tail !== null && tail.key === key) {
|
|
1479
|
+
const merged = merge(tail.input, input);
|
|
1480
|
+
if (merged !== null) {
|
|
1481
|
+
tail.input = merged;
|
|
1482
|
+
return tail.done;
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
const entry = { key, input, done: Promise.resolve() };
|
|
1486
|
+
entry.done = exclusive(() => {
|
|
1487
|
+
if (gathering === entry)
|
|
1488
|
+
gathering = null;
|
|
1489
|
+
return work(entry.input);
|
|
1490
|
+
});
|
|
1491
|
+
gathering = entry;
|
|
1492
|
+
return entry.done;
|
|
1493
|
+
}
|
|
1358
1494
|
function withdrawable(locator) {
|
|
1359
1495
|
return (batch) => {
|
|
1360
1496
|
const state = pendings.get(pendingKey(locator, batch.id));
|
|
@@ -1363,6 +1499,16 @@ export function composeRuntime(options) {
|
|
|
1363
1499
|
}
|
|
1364
1500
|
const REWRITE_LINKS = ['field', 'body'];
|
|
1365
1501
|
const CHECKED_LINKS = ['field', 'section'];
|
|
1502
|
+
const BODY_LINKS = ['body'];
|
|
1503
|
+
async function textsNeeded(local, record, named) {
|
|
1504
|
+
const paths = [...new Set(named.map((one) => (typeof one === 'string' ? one : one.path)))];
|
|
1505
|
+
const unheld = await unheldIn(local, record.locator, record.key, paths);
|
|
1506
|
+
if (unheld.length === 0)
|
|
1507
|
+
return null;
|
|
1508
|
+
await holdTexts(unheld);
|
|
1509
|
+
const [first] = await unheldIn(local, record.locator, record.key, unheld.map(({ path }) => path));
|
|
1510
|
+
return first?.path ?? null;
|
|
1511
|
+
}
|
|
1366
1512
|
async function sliceAt(record, view, queue, extra = [], moved = []) {
|
|
1367
1513
|
const local = await openLocal();
|
|
1368
1514
|
const needed = new Set([...touchedBy(queue), ...extra]);
|
|
@@ -1436,26 +1582,14 @@ export function composeRuntime(options) {
|
|
|
1436
1582
|
},
|
|
1437
1583
|
};
|
|
1438
1584
|
}
|
|
1439
|
-
function
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
fingerprint(compositionOf(before)) === fingerprint(compositionOf(after))) {
|
|
1443
|
-
return;
|
|
1444
|
-
}
|
|
1445
|
-
if (before !== undefined && before.key === after.key)
|
|
1446
|
-
prepared.delete(locator);
|
|
1447
|
-
}
|
|
1448
|
-
function committedRoom(locator, record, answer) {
|
|
1449
|
-
if (answer.prepared[locator] === false)
|
|
1450
|
-
prepared.delete(locator);
|
|
1451
|
-
else if (answer.prepared[locator] === true)
|
|
1452
|
-
prepared.add(locator);
|
|
1453
|
-
if (!prepared.has(locator))
|
|
1585
|
+
function committedRoom(locator, answer) {
|
|
1586
|
+
const indexed = answer.indexed[locator] ?? replicas.get(locator)?.indexed ?? false;
|
|
1587
|
+
if (!indexed)
|
|
1454
1588
|
prepare([locator]);
|
|
1455
|
-
return { count: answer.counts[locator] ?? replicas.get(locator)?.count ?? 0 };
|
|
1589
|
+
return { count: answer.counts[locator] ?? replicas.get(locator)?.count ?? 0, indexed };
|
|
1456
1590
|
}
|
|
1457
1591
|
function replayed(record, confirmedLayer, queue) {
|
|
1458
|
-
return stepped(replaying(confirmedLayer, queue, record.locator,
|
|
1592
|
+
return stepped(replaying(confirmedLayer, queue, record.locator, composed(record)));
|
|
1459
1593
|
}
|
|
1460
1594
|
async function overlayDiff(record, before, visible, touched) {
|
|
1461
1595
|
const kept = before !== undefined && before.record.key === record.key
|
|
@@ -1475,8 +1609,6 @@ export function composeRuntime(options) {
|
|
|
1475
1609
|
function unpairedIn(locator, batch) {
|
|
1476
1610
|
if (!queueIn(locator).some((held) => held.id === batch.id))
|
|
1477
1611
|
return false;
|
|
1478
|
-
if (pendings.get(pendingKey(locator, batch.id))?.accepted !== undefined)
|
|
1479
|
-
return false;
|
|
1480
1612
|
return !leavingIn(batch, locator) || withdrawable(locator)(batch);
|
|
1481
1613
|
}
|
|
1482
1614
|
async function together(local, settlements) {
|
|
@@ -1518,11 +1650,17 @@ export function composeRuntime(options) {
|
|
|
1518
1650
|
const confirmedLayer = overrides === undefined
|
|
1519
1651
|
? read
|
|
1520
1652
|
: await stepped(slicingWith(read, overrides.set, overrides.removed));
|
|
1521
|
-
const composition =
|
|
1522
|
-
const
|
|
1523
|
-
|
|
1653
|
+
const composition = composed(record);
|
|
1654
|
+
const shown = queue.filter((batch) => {
|
|
1655
|
+
const held = landedAhead.get(pendingKey(locator, batch.id));
|
|
1656
|
+
return (held !== undefined &&
|
|
1657
|
+
Object.entries(held).some(([path, revision]) => confirmedLayer.revisions[path] !== revision));
|
|
1658
|
+
});
|
|
1659
|
+
const unshown = queue.filter((batch) => !shown.includes(batch));
|
|
1660
|
+
const { withdrawn, kept } = await stepped(standing(confirmedLayer, unshown, withdrawable(locator), localSaves, locator, composition));
|
|
1661
|
+
const founded = kept.filter((batch) => !unshown.includes(batch));
|
|
1524
1662
|
const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
|
|
1525
|
-
const unqueued =
|
|
1663
|
+
const unqueued = shown.map((batch) => ({ batch, landed: true }));
|
|
1526
1664
|
for (const batch of withdrawn) {
|
|
1527
1665
|
unqueued.push({ batch, landed: await landedWhole(locator, batch, confirmedLayer) });
|
|
1528
1666
|
}
|
|
@@ -1561,6 +1699,7 @@ export function composeRuntime(options) {
|
|
|
1561
1699
|
key: record.key,
|
|
1562
1700
|
record: storedRecord(record),
|
|
1563
1701
|
roots: record.typedSelection?.roots ?? [],
|
|
1702
|
+
composed: composedOf(composition),
|
|
1564
1703
|
...settling.commit,
|
|
1565
1704
|
...(overrides === undefined
|
|
1566
1705
|
? {}
|
|
@@ -1586,8 +1725,10 @@ export function composeRuntime(options) {
|
|
|
1586
1725
|
commit,
|
|
1587
1726
|
apply: (answer) => {
|
|
1588
1727
|
for (const batch of previousQueue) {
|
|
1589
|
-
if (
|
|
1590
|
-
|
|
1728
|
+
if (staying.has(batch.id))
|
|
1729
|
+
continue;
|
|
1730
|
+
pendings.delete(pendingKey(locator, batch.id));
|
|
1731
|
+
landedAhead.delete(pendingKey(locator, batch.id));
|
|
1591
1732
|
}
|
|
1592
1733
|
for (const draft of written.values())
|
|
1593
1734
|
rememberRoomDraft(draft);
|
|
@@ -1602,14 +1743,14 @@ export function composeRuntime(options) {
|
|
|
1602
1743
|
const confirmedChanged = settling.commit?.published !== undefined ||
|
|
1603
1744
|
settling.commit?.refreshed === true ||
|
|
1604
1745
|
overrides !== undefined;
|
|
1605
|
-
|
|
1606
|
-
const { count } = committedRoom(locator, record, answer);
|
|
1746
|
+
const { count, indexed } = committedRoom(locator, answer);
|
|
1607
1747
|
replicas.set(locator, {
|
|
1608
1748
|
record,
|
|
1609
1749
|
queue: kept,
|
|
1610
1750
|
overlaid: touched,
|
|
1611
1751
|
count,
|
|
1612
1752
|
confirmed: (before?.confirmed ?? 0) + (confirmedChanged ? 1 : 0),
|
|
1753
|
+
indexed,
|
|
1613
1754
|
});
|
|
1614
1755
|
return mergeChangeSets([
|
|
1615
1756
|
changeSet(locator, {
|
|
@@ -1624,110 +1765,107 @@ export function composeRuntime(options) {
|
|
|
1624
1765
|
};
|
|
1625
1766
|
}
|
|
1626
1767
|
function adoptModels(locator, record) {
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
if (model.status === 'current')
|
|
1635
|
-
liveModels.set(key, JSON.stringify(model.snapshot));
|
|
1636
|
-
else if (model.status === 'unavailable')
|
|
1637
|
-
liveModels.delete(key);
|
|
1638
|
-
}
|
|
1768
|
+
const model = record.model;
|
|
1769
|
+
models.set(locator, model);
|
|
1770
|
+
invalidModels.delete(locator);
|
|
1771
|
+
if (model.status === 'current')
|
|
1772
|
+
liveModels.set(locator, JSON.stringify(model.snapshot));
|
|
1773
|
+
else if (model.status === 'unavailable')
|
|
1774
|
+
liveModels.delete(locator);
|
|
1639
1775
|
}
|
|
1640
1776
|
function remodel(locator, answers, held) {
|
|
1641
|
-
const
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
};
|
|
1652
|
-
return {
|
|
1653
|
-
collection: taken(answers?.collection, held.collection, 'collection'),
|
|
1654
|
-
selection: taken(answers?.selection, held.selection, 'typed-selection'),
|
|
1655
|
-
};
|
|
1777
|
+
const answer = answers?.collection;
|
|
1778
|
+
if (answer === undefined)
|
|
1779
|
+
return held;
|
|
1780
|
+
const { state, refused } = attachedModel(answer, held.collection, roomTypes);
|
|
1781
|
+
if (refused !== null) {
|
|
1782
|
+
const where = refused.location === undefined ? '' : ` ${refused.location}`;
|
|
1783
|
+
journal.record(`model invalid ${locator} collection ${refused.identity} ${refused.rule}${where}`);
|
|
1784
|
+
}
|
|
1785
|
+
models.set(locator, state);
|
|
1786
|
+
return { collection: state };
|
|
1656
1787
|
}
|
|
1657
|
-
function publish(local, room,
|
|
1658
|
-
|
|
1788
|
+
function publish(local, room, reached, landing, live) {
|
|
1789
|
+
const landed = exclusive(async () => {
|
|
1659
1790
|
if (!live()) {
|
|
1660
1791
|
caughtUpTo(room.locator);
|
|
1661
1792
|
return true;
|
|
1662
1793
|
}
|
|
1663
1794
|
const prior = replicas.get(room.locator);
|
|
1664
|
-
if (landing.
|
|
1665
|
-
(
|
|
1666
|
-
|
|
1795
|
+
if (!stands(room.locator, landing.from)) {
|
|
1796
|
+
if (landing.kind === 'refreshed') {
|
|
1797
|
+
await local.unrefresh(reached.key);
|
|
1798
|
+
return false;
|
|
1799
|
+
}
|
|
1800
|
+
caughtUpTo(room.locator);
|
|
1801
|
+
await abandoned(local, room.locator);
|
|
1667
1802
|
return false;
|
|
1668
1803
|
}
|
|
1804
|
+
const record = landing.kind === 'refreshed' && prior !== undefined
|
|
1805
|
+
? {
|
|
1806
|
+
...prior.record,
|
|
1807
|
+
roomVersion: reached.roomVersion,
|
|
1808
|
+
...(landing.remodelled ? { model: reached.model } : {}),
|
|
1809
|
+
}
|
|
1810
|
+
: reached;
|
|
1669
1811
|
adoptModels(room.locator, record);
|
|
1670
1812
|
const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
|
|
1671
|
-
const roomVersion = record.roomVersion;
|
|
1672
|
-
const accepted = queue.filter((batch) => {
|
|
1673
|
-
const at = pendings.get(pendingKey(room.locator, batch.id))?.accepted;
|
|
1674
|
-
return at !== undefined && options.adapter.reached(room.locator, roomVersion, at);
|
|
1675
|
-
});
|
|
1676
1813
|
let set;
|
|
1677
1814
|
try {
|
|
1678
|
-
set = await settle(local, room.locator, prior, record, queue
|
|
1815
|
+
set = await settle(local, room.locator, prior, record, queue, landing.kind === 'refreshed'
|
|
1679
1816
|
? {
|
|
1680
1817
|
room: true,
|
|
1681
1818
|
view: 'refreshed',
|
|
1682
|
-
commit: { refreshed: true
|
|
1819
|
+
commit: { refreshed: true },
|
|
1683
1820
|
}
|
|
1684
1821
|
: {
|
|
1685
1822
|
room: true,
|
|
1686
1823
|
view: landing.refreshed ? 'refreshed' : 'confirmed',
|
|
1687
1824
|
commit: {
|
|
1688
1825
|
published: { from: prior?.record.key ?? null },
|
|
1689
|
-
composition: compositionOf(record),
|
|
1690
1826
|
...(landing.refreshed ? { refreshed: true } : {}),
|
|
1691
1827
|
},
|
|
1692
1828
|
});
|
|
1693
1829
|
}
|
|
1694
1830
|
catch (error) {
|
|
1695
|
-
if (!
|
|
1831
|
+
if (!refusedForSpace(error))
|
|
1696
1832
|
throw error;
|
|
1697
|
-
await exceededOn(room.locator, null);
|
|
1698
1833
|
caughtUpTo(room.locator);
|
|
1699
1834
|
return true;
|
|
1700
1835
|
}
|
|
1701
1836
|
if (landing.kind === 'bootstrapped') {
|
|
1702
|
-
|
|
1837
|
+
highest.delete(room.locator);
|
|
1838
|
+
beyondHorizon.delete(room.locator);
|
|
1839
|
+
untransferred(room.locator);
|
|
1840
|
+
startedOver.delete(room.locator);
|
|
1703
1841
|
catching.delete(room.locator);
|
|
1704
|
-
|
|
1842
|
+
if (prior !== undefined && prior.record.key !== record.key)
|
|
1843
|
+
sweep();
|
|
1844
|
+
name(room.locator);
|
|
1705
1845
|
}
|
|
1706
|
-
for (const batch of accepted)
|
|
1707
|
-
pendings.delete(pendingKey(room.locator, batch.id));
|
|
1708
1846
|
stale.delete(room.locator);
|
|
1709
1847
|
unpreparable.delete(room.locator);
|
|
1710
1848
|
if (unreadable.delete(room.locator))
|
|
1711
1849
|
scopedRefusals.delete(room.locator);
|
|
1712
|
-
sourceRefusals.delete(room.locator);
|
|
1713
|
-
declarationRefusals.delete(room.locator);
|
|
1714
1850
|
list(room, true);
|
|
1715
1851
|
publishChangeSet(set);
|
|
1852
|
+
if (landing.kind === 'bootstrapped')
|
|
1853
|
+
void heldAgain(room.locator);
|
|
1716
1854
|
await unpair(local);
|
|
1717
1855
|
moveTo({ ...status, ...attention() });
|
|
1718
|
-
|
|
1719
|
-
if (accepted.length > 0 && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
|
|
1856
|
+
if (landing.kind === 'bootstrapped' && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
|
|
1720
1857
|
submitLater(room.locator);
|
|
1721
1858
|
}
|
|
1722
1859
|
return true;
|
|
1723
1860
|
});
|
|
1861
|
+
return landing.kind === 'refreshed' ? landed : landed.finally(() => installed(room.locator));
|
|
1724
1862
|
}
|
|
1725
1863
|
async function savedRoom(locator) {
|
|
1726
1864
|
const local = await openLocal();
|
|
1727
1865
|
const replica = replicas.get(locator);
|
|
1728
1866
|
const room = rooms.get(locator);
|
|
1729
1867
|
if (replica === undefined || room === undefined) {
|
|
1730
|
-
throw runtimeError('room
|
|
1868
|
+
throw runtimeError('unknown-room', `Unknown Room ${locator}`);
|
|
1731
1869
|
}
|
|
1732
1870
|
return { local, replica, room };
|
|
1733
1871
|
}
|
|
@@ -1745,12 +1883,7 @@ export function composeRuntime(options) {
|
|
|
1745
1883
|
return committed(locator, target, saved, new Set(saved.map(({ path }) => path)), writtenBy);
|
|
1746
1884
|
}
|
|
1747
1885
|
async function saveParts(locator, named, writtenBy) {
|
|
1748
|
-
const read = named
|
|
1749
|
-
if (read.some(({ path }, at) => path === '' ||
|
|
1750
|
-
normalize(path) !== path ||
|
|
1751
|
-
read.findIndex((one) => one.path === path) !== at)) {
|
|
1752
|
-
throw runtimeError('room-draft-malformed');
|
|
1753
|
-
}
|
|
1886
|
+
const read = readSavedParts(named);
|
|
1754
1887
|
const target = await savedRoom(locator);
|
|
1755
1888
|
const saved = [];
|
|
1756
1889
|
const files = [];
|
|
@@ -1759,7 +1892,7 @@ export function composeRuntime(options) {
|
|
|
1759
1892
|
for (const { path, parts } of read) {
|
|
1760
1893
|
const file = parts.file === undefined
|
|
1761
1894
|
? undefined
|
|
1762
|
-
: await heldFile(parts.file.blob, binaryBase(visible, path));
|
|
1895
|
+
: await heldFile(parts.file.blob, parts.file.base === undefined ? binaryBase(visible, path) : parts.file.base);
|
|
1763
1896
|
if (parts.file !== undefined && file !== undefined) {
|
|
1764
1897
|
files.push({ key: file.localId, blob: parts.file.blob });
|
|
1765
1898
|
}
|
|
@@ -1794,13 +1927,27 @@ export function composeRuntime(options) {
|
|
|
1794
1927
|
const referring = removed.length === 0
|
|
1795
1928
|
? []
|
|
1796
1929
|
: (await local.referrers(record.key, 'confirmed', removed, CHECKED_LINKS)).map(({ path }) => path);
|
|
1797
|
-
const
|
|
1930
|
+
const unheld = await textsNeeded(local, record, [
|
|
1931
|
+
...paths,
|
|
1932
|
+
...referring,
|
|
1933
|
+
...(moved.length === 0
|
|
1934
|
+
? []
|
|
1935
|
+
: await local.referrers(record.key, 'confirmed', moved, REWRITE_LINKS)),
|
|
1936
|
+
...(removed.length === 0
|
|
1937
|
+
? []
|
|
1938
|
+
: await local.referrers(record.key, 'confirmed', removed, BODY_LINKS)),
|
|
1939
|
+
]);
|
|
1940
|
+
if (unheld !== null) {
|
|
1941
|
+
return { refusal: { code: 'transport-uncertainty', retryable: true, file: unheld } };
|
|
1942
|
+
}
|
|
1943
|
+
const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...paths.flatMap(ancestors), ...referring], moved);
|
|
1798
1944
|
const { visible } = await replayed(record, confirmedLayer, replica.queue);
|
|
1799
|
-
const composition =
|
|
1945
|
+
const composition = composed(record);
|
|
1800
1946
|
return {
|
|
1801
1947
|
record,
|
|
1802
1948
|
confirmed: confirmedLayer,
|
|
1803
1949
|
visible,
|
|
1950
|
+
folders: new Set(await local.folders(record.key, 'visible', paths)),
|
|
1804
1951
|
*visibleOver(queue) {
|
|
1805
1952
|
return (yield* replaying(confirmedLayer, queue, record.locator, composition)).visible;
|
|
1806
1953
|
},
|
|
@@ -1812,13 +1959,7 @@ export function composeRuntime(options) {
|
|
|
1812
1959
|
return false;
|
|
1813
1960
|
const local = await openLocal();
|
|
1814
1961
|
const roots = record.typedSelection?.roots ?? [];
|
|
1815
|
-
|
|
1816
|
-
(await local.typedWithin(record.key, 'visible', roots, false))) {
|
|
1817
|
-
return true;
|
|
1818
|
-
}
|
|
1819
|
-
return (composition.selection !== null &&
|
|
1820
|
-
composition.selection.model.status === 'unavailable' &&
|
|
1821
|
-
(await local.typedWithin(record.key, 'visible', roots, true)));
|
|
1962
|
+
return local.typedWithin(record.key, 'visible', roots, false);
|
|
1822
1963
|
}
|
|
1823
1964
|
function textDiffers(left, right) {
|
|
1824
1965
|
return ((left?.content ?? undefined) !== (right?.content ?? undefined) ||
|
|
@@ -1829,18 +1970,6 @@ export function composeRuntime(options) {
|
|
|
1829
1970
|
return undefined;
|
|
1830
1971
|
return entry.binary !== null ? null : entry.content;
|
|
1831
1972
|
}
|
|
1832
|
-
function walkOf(local, key, view) {
|
|
1833
|
-
return async (after) => {
|
|
1834
|
-
const page = await local.walk(key, view, after, WALK_BYTES);
|
|
1835
|
-
return {
|
|
1836
|
-
entries: page.entries.map(({ path, content, binary }) => ({
|
|
1837
|
-
path,
|
|
1838
|
-
content: binary !== null ? null : content,
|
|
1839
|
-
})),
|
|
1840
|
-
next: page.next,
|
|
1841
|
-
};
|
|
1842
|
-
};
|
|
1843
|
-
}
|
|
1844
1973
|
function lookupOf(local, key, view) {
|
|
1845
1974
|
return async (paths) => {
|
|
1846
1975
|
if (paths.length === 0)
|
|
@@ -1857,37 +1986,6 @@ export function composeRuntime(options) {
|
|
|
1857
1986
|
const read = parseFrontmatter(entry.content ?? '');
|
|
1858
1987
|
return { type: read.ok ? (read.frontmatter.type ?? 'any') : undefined, binary: false };
|
|
1859
1988
|
}
|
|
1860
|
-
function overriddenWalk(walk, overrides) {
|
|
1861
|
-
const sorted = [...overrides.keys()].sort();
|
|
1862
|
-
const beyond = (path) => {
|
|
1863
|
-
if (path === null)
|
|
1864
|
-
return 0;
|
|
1865
|
-
let low = 0;
|
|
1866
|
-
let high = sorted.length;
|
|
1867
|
-
while (low < high) {
|
|
1868
|
-
const middle = (low + high) >>> 1;
|
|
1869
|
-
if (sorted[middle] <= path)
|
|
1870
|
-
low = middle + 1;
|
|
1871
|
-
else
|
|
1872
|
-
high = middle;
|
|
1873
|
-
}
|
|
1874
|
-
return low;
|
|
1875
|
-
};
|
|
1876
|
-
return async (after) => {
|
|
1877
|
-
const page = await walk(after);
|
|
1878
|
-
const end = page.next;
|
|
1879
|
-
const added = sorted
|
|
1880
|
-
.slice(beyond(after), end === null ? sorted.length : beyond(end))
|
|
1881
|
-
.flatMap((path) => {
|
|
1882
|
-
const entry = overrides.get(path);
|
|
1883
|
-
return entry === null || entry === undefined
|
|
1884
|
-
? []
|
|
1885
|
-
: [{ path, content: entry.binary !== null ? null : entry.content }];
|
|
1886
|
-
});
|
|
1887
|
-
const entries = [...page.entries.filter(({ path }) => !overrides.has(path)), ...added].sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0));
|
|
1888
|
-
return { entries, next: end };
|
|
1889
|
-
};
|
|
1890
|
-
}
|
|
1891
1989
|
function overriddenLookup(lookup, overrides) {
|
|
1892
1990
|
return async (paths) => {
|
|
1893
1991
|
const known = new Map(await lookup(paths.filter((path) => !overrides.has(path))));
|
|
@@ -1905,9 +2003,8 @@ export function composeRuntime(options) {
|
|
|
1905
2003
|
}
|
|
1906
2004
|
function validatorOf(record, confirmedLayer) {
|
|
1907
2005
|
return async (resulting, touched) => {
|
|
1908
|
-
const composition = compositionFor(record);
|
|
1909
2006
|
const local = await openLocal();
|
|
1910
|
-
const
|
|
2007
|
+
const changes = [];
|
|
1911
2008
|
const overrides = new Map();
|
|
1912
2009
|
const pace = paced();
|
|
1913
2010
|
for (const path of new Set([...confirmedLayer.held, ...touched])) {
|
|
@@ -1918,19 +2015,13 @@ export function composeRuntime(options) {
|
|
|
1918
2015
|
continue;
|
|
1919
2016
|
overrides.set(path, now);
|
|
1920
2017
|
if (textOf(was) !== textOf(now))
|
|
1921
|
-
|
|
2018
|
+
changes.push({ path, was: textOf(was), now: textOf(now) });
|
|
1922
2019
|
}
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
texts: textsOf(local, record.key, 'confirmed'),
|
|
1929
|
-
referrers: referrersOf(local, record.key, 'confirmed'),
|
|
1930
|
-
}));
|
|
1931
|
-
if (clean)
|
|
1932
|
-
return { verdict: 'accept' };
|
|
1933
|
-
return validatedCollection(overriddenWalk(walkOf(local, record.key, 'confirmed'), overrides), lookup, composition);
|
|
2020
|
+
return validatedNeighbourhood(changes, compositionFor(record), {
|
|
2021
|
+
lookup: overriddenLookup(lookupOf(local, record.key, 'confirmed'), overrides),
|
|
2022
|
+
texts: textsOf(local, record.key, 'confirmed'),
|
|
2023
|
+
referrers: referrersOf(local, record.key, 'confirmed'),
|
|
2024
|
+
});
|
|
1934
2025
|
};
|
|
1935
2026
|
}
|
|
1936
2027
|
function textsOf(local, key, view) {
|
|
@@ -1954,6 +2045,7 @@ export function composeRuntime(options) {
|
|
|
1954
2045
|
operations: room.operations,
|
|
1955
2046
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
1956
2047
|
visible: saving.visible,
|
|
2048
|
+
folders: saving.folders,
|
|
1957
2049
|
queued: queuedDocuments(replica.queue),
|
|
1958
2050
|
unmodeled: await unmodeledIn(record),
|
|
1959
2051
|
validate: validatorOf(record, saving.confirmed),
|
|
@@ -1974,6 +2066,7 @@ export function composeRuntime(options) {
|
|
|
1974
2066
|
locator,
|
|
1975
2067
|
key: saving.record.key,
|
|
1976
2068
|
roots: saving.record.typedSelection?.roots ?? [],
|
|
2069
|
+
composed: composedOf(composed(saving.record)),
|
|
1977
2070
|
overlay: diff.overlay,
|
|
1978
2071
|
});
|
|
1979
2072
|
}
|
|
@@ -2005,15 +2098,19 @@ export function composeRuntime(options) {
|
|
|
2005
2098
|
}
|
|
2006
2099
|
const moves = saved.map(({ path, parts }) => ({ path, to: moveTarget(parts.move) }));
|
|
2007
2100
|
const sourceSaving = await savingIn(replica, moves.map(({ path }) => ({ kind: 'remove', path })), saved.map(({ path }) => path), moves.map(({ path }) => path));
|
|
2008
|
-
|
|
2101
|
+
if ('refusal' in sourceSaving)
|
|
2102
|
+
return { outcome: 'refused', ...sourceSaving.refusal };
|
|
2103
|
+
const changed = saved.flatMap(({ path, parts }) => changedParts(path, parts, sourceSaving.visible));
|
|
2009
2104
|
if (changed.length > 0)
|
|
2010
2105
|
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
2011
2106
|
const target = replicas.get(destination);
|
|
2012
2107
|
const listedRoom = rooms.get(destination);
|
|
2013
2108
|
if (target === undefined || listedRoom === undefined) {
|
|
2014
|
-
throw runtimeError('room
|
|
2109
|
+
throw runtimeError('unknown-room', `Unknown Room ${destination}`);
|
|
2015
2110
|
}
|
|
2016
2111
|
const arriving = await savingIn(target, [], moves.map(({ to }) => to));
|
|
2112
|
+
if ('refusal' in arriving)
|
|
2113
|
+
return { outcome: 'refused', ...arriving.refusal };
|
|
2017
2114
|
const built = await buildMove({
|
|
2018
2115
|
source: await sideOf(locator, replica, room, sourceSaving),
|
|
2019
2116
|
destination: await sideOf(destination, target, listedRoom, arriving),
|
|
@@ -2074,10 +2171,10 @@ export function composeRuntime(options) {
|
|
|
2074
2171
|
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
2075
2172
|
const { answer, touched } = committedBoth;
|
|
2076
2173
|
if (answer.fenced !== null) {
|
|
2077
|
-
throw runtimeError('room
|
|
2174
|
+
throw runtimeError('unknown-room', `Unknown Room ${answer.fenced}`);
|
|
2078
2175
|
}
|
|
2079
2176
|
if (fences.has(locator) || fences.has(destination)) {
|
|
2080
|
-
throw runtimeError('room
|
|
2177
|
+
throw runtimeError('unknown-room', `Unknown Room ${locator}`);
|
|
2081
2178
|
}
|
|
2082
2179
|
pendings.set(pendingKey(locator, id), {
|
|
2083
2180
|
drafts: saved,
|
|
@@ -2117,6 +2214,8 @@ export function composeRuntime(options) {
|
|
|
2117
2214
|
submitLater(locator);
|
|
2118
2215
|
}
|
|
2119
2216
|
async function committed(locator, target, saved, consumed, writtenBy, files = []) {
|
|
2217
|
+
if (full)
|
|
2218
|
+
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
2120
2219
|
const into = movedInto(locator, saved);
|
|
2121
2220
|
if (into !== null) {
|
|
2122
2221
|
return committedMove(locator, into, target, saved, consumed, writtenBy, files);
|
|
@@ -2128,7 +2227,9 @@ export function composeRuntime(options) {
|
|
|
2128
2227
|
...saved.map(({ path }) => path),
|
|
2129
2228
|
...parts.flatMap(({ bases }) => bases.map(({ path }) => path)),
|
|
2130
2229
|
]);
|
|
2131
|
-
|
|
2230
|
+
if ('refusal' in saving)
|
|
2231
|
+
return { outcome: 'refused', ...saving.refusal };
|
|
2232
|
+
const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, held, saving.visible));
|
|
2132
2233
|
if (changed.length > 0)
|
|
2133
2234
|
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
2134
2235
|
const requeue = await stepped(requeuing(replica.queue, changes, saving.visible, withdrawable(locator), saving.visibleOver));
|
|
@@ -2156,6 +2257,7 @@ export function composeRuntime(options) {
|
|
|
2156
2257
|
providerLimits: limits,
|
|
2157
2258
|
handle: room.handle,
|
|
2158
2259
|
visible: saving.visible,
|
|
2260
|
+
folders: saving.folders,
|
|
2159
2261
|
changes,
|
|
2160
2262
|
bases: parts.flatMap(({ bases }) => bases),
|
|
2161
2263
|
queued: queuedDocuments(replica.queue),
|
|
@@ -2245,10 +2347,10 @@ export function composeRuntime(options) {
|
|
|
2245
2347
|
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
2246
2348
|
const { answer, touched } = done;
|
|
2247
2349
|
if (answer.fenced !== null) {
|
|
2248
|
-
throw runtimeError('room
|
|
2350
|
+
throw runtimeError('unknown-room', `Unknown Room ${answer.fenced}`);
|
|
2249
2351
|
}
|
|
2250
2352
|
if (fences.has(locator))
|
|
2251
|
-
throw runtimeError('room
|
|
2353
|
+
throw runtimeError('unknown-room', `Unknown Room ${locator}`);
|
|
2252
2354
|
for (const origin of withdrawn.keys())
|
|
2253
2355
|
pendings.delete(pendingKey(locator, origin));
|
|
2254
2356
|
for (const [id, drafts] of drafted) {
|
|
@@ -2297,7 +2399,7 @@ export function composeRuntime(options) {
|
|
|
2297
2399
|
throw new Error('connection refused');
|
|
2298
2400
|
await local.keepUpload(locator, identifier, upload);
|
|
2299
2401
|
if (fences.has(locator))
|
|
2300
|
-
throw runtimeError('room
|
|
2402
|
+
throw runtimeError('unknown-room', `Unknown Room ${locator}`);
|
|
2301
2403
|
const key = pendingKey(locator, identifier);
|
|
2302
2404
|
pendings.set(key, { ...pendings.get(key), upload });
|
|
2303
2405
|
if (!continuing())
|
|
@@ -2306,6 +2408,9 @@ export function composeRuntime(options) {
|
|
|
2306
2408
|
async function readHistory(locator, path, query = {}) {
|
|
2307
2409
|
if (closed || refusal !== null)
|
|
2308
2410
|
return UNCERTAIN;
|
|
2411
|
+
await firstDiscovery;
|
|
2412
|
+
if (!roomsNamed)
|
|
2413
|
+
return UNCERTAIN;
|
|
2309
2414
|
try {
|
|
2310
2415
|
await ensureAuthorization();
|
|
2311
2416
|
}
|
|
@@ -2395,14 +2500,12 @@ export function composeRuntime(options) {
|
|
|
2395
2500
|
return (await openLocal()).file(localId, change?.kind === 'binary' ? change.mediaType : '');
|
|
2396
2501
|
};
|
|
2397
2502
|
}
|
|
2398
|
-
function binaryRoom(request) {
|
|
2399
|
-
if (request.kind === 'path')
|
|
2400
|
-
return request.locator;
|
|
2401
|
-
return options.adapter.referrerScope(request.referrer)?.locator ?? null;
|
|
2402
|
-
}
|
|
2403
2503
|
async function transferBinary(request, transfer, answer) {
|
|
2404
2504
|
if (closed || refusal !== null)
|
|
2405
2505
|
return UNCERTAIN;
|
|
2506
|
+
await firstDiscovery;
|
|
2507
|
+
if (!roomsNamed)
|
|
2508
|
+
return UNCERTAIN;
|
|
2406
2509
|
try {
|
|
2407
2510
|
await ensureAuthorization();
|
|
2408
2511
|
}
|
|
@@ -2412,16 +2515,13 @@ export function composeRuntime(options) {
|
|
|
2412
2515
|
if (closed || authorization === undefined || refusal !== null)
|
|
2413
2516
|
return UNCERTAIN;
|
|
2414
2517
|
const since = fencings;
|
|
2415
|
-
journal.record(request.
|
|
2416
|
-
? `transfer binary ${request.locator} ${request.path}`
|
|
2417
|
-
: `transfer binary ${request.fileRef}`);
|
|
2518
|
+
journal.record(`transfer binary ${request.locator} ${request.path}`);
|
|
2418
2519
|
try {
|
|
2419
2520
|
const result = await transfer(authorization);
|
|
2420
2521
|
const settled = result;
|
|
2421
2522
|
journal.record(`outcome binary ${settled.kind}`);
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
return failure({ kind: 'room-not-readable', locator });
|
|
2523
|
+
if (fencedSince(request.locator, since))
|
|
2524
|
+
return failure({ kind: 'room-not-readable', locator: request.locator });
|
|
2425
2525
|
if (settled.kind === 'scoped-refusal') {
|
|
2426
2526
|
const room = replicas.get(settled.locator)?.record;
|
|
2427
2527
|
if (room !== undefined)
|
|
@@ -2436,23 +2536,104 @@ export function composeRuntime(options) {
|
|
|
2436
2536
|
return UNCERTAIN;
|
|
2437
2537
|
}
|
|
2438
2538
|
}
|
|
2539
|
+
async function heldAgain(locator) {
|
|
2540
|
+
const replica = replicas.get(locator);
|
|
2541
|
+
if (replica === undefined || !textOnline())
|
|
2542
|
+
return;
|
|
2543
|
+
try {
|
|
2544
|
+
const wanted = await (await openLocal()).wanted(locator, replica.record.key);
|
|
2545
|
+
await holdTexts(wanted.map(({ path, revision }) => ({ locator, path, revision })));
|
|
2546
|
+
}
|
|
2547
|
+
catch {
|
|
2548
|
+
journal.record(`hold text ${locator} unwritten`);
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
function textOnline() {
|
|
2552
|
+
return !closed && refusal === null && connectionOf() === 'connected';
|
|
2553
|
+
}
|
|
2554
|
+
function textKey({ locator, path }) {
|
|
2555
|
+
return documentKey(locator, path);
|
|
2556
|
+
}
|
|
2557
|
+
async function readText({ locator, path, revision }) {
|
|
2558
|
+
const replica = replicas.get(locator);
|
|
2559
|
+
if (replica === undefined || !textOnline())
|
|
2560
|
+
return;
|
|
2561
|
+
try {
|
|
2562
|
+
await ensureAuthorization();
|
|
2563
|
+
}
|
|
2564
|
+
catch {
|
|
2565
|
+
return;
|
|
2566
|
+
}
|
|
2567
|
+
if (closed || authorization === undefined || refusal !== null)
|
|
2568
|
+
return;
|
|
2569
|
+
const since = fencings;
|
|
2570
|
+
journal.record(`hold text ${locator} ${path}`);
|
|
2571
|
+
const read = await reads
|
|
2572
|
+
.bootstrap(authorization, { locator, selection: replica.record.replica.selection, path })
|
|
2573
|
+
.catch(() => null);
|
|
2574
|
+
journal.record(`outcome text ${read?.kind ?? 'transport-uncertainty'}`);
|
|
2575
|
+
if (read === null || read.kind !== 'bootstrap' || fencedSince(locator, since))
|
|
2576
|
+
return;
|
|
2577
|
+
const content = read.documents[path];
|
|
2578
|
+
const now = replicas.get(locator);
|
|
2579
|
+
if (content === undefined || read.revisions[path] !== revision || now === undefined) {
|
|
2580
|
+
unholdable.set(textKey({ locator, path, revision }), revision);
|
|
2581
|
+
return;
|
|
2582
|
+
}
|
|
2583
|
+
const held = await (await openLocal()).hold(locator, now.record.key, compositionFor(now.record), [{ path, revision, content }]);
|
|
2584
|
+
if (held.length === 0)
|
|
2585
|
+
unholdable.set(textKey({ locator, path, revision }), revision);
|
|
2586
|
+
else
|
|
2587
|
+
publishChangeSet(changeSet(locator, { documents: held }));
|
|
2588
|
+
}
|
|
2589
|
+
function holdText(unheld) {
|
|
2590
|
+
const key = textKey(unheld);
|
|
2591
|
+
const running = holding.get(key);
|
|
2592
|
+
if (running !== undefined)
|
|
2593
|
+
return running;
|
|
2594
|
+
const started = readText(unheld).finally(() => holding.delete(key));
|
|
2595
|
+
holding.set(key, started);
|
|
2596
|
+
return started;
|
|
2597
|
+
}
|
|
2598
|
+
async function holdTexts(wanted) {
|
|
2599
|
+
const queue = wanted.filter((one) => unholdable.get(textKey(one)) !== one.revision);
|
|
2600
|
+
await Promise.all(Array.from({ length: Math.min(TEXT_READS, queue.length) }, async () => {
|
|
2601
|
+
for (let next = queue.shift(); next !== undefined; next = queue.shift()) {
|
|
2602
|
+
await holdText(next);
|
|
2603
|
+
}
|
|
2604
|
+
}));
|
|
2605
|
+
}
|
|
2606
|
+
async function unheldIn(local, locator, key, paths) {
|
|
2607
|
+
if (paths.length === 0)
|
|
2608
|
+
return [];
|
|
2609
|
+
return (await local.entries(key, 'confirmed', paths)).flatMap((entry) => entry.content === null &&
|
|
2610
|
+
entry.binary === null &&
|
|
2611
|
+
entry.outline !== null &&
|
|
2612
|
+
entry.revision !== null
|
|
2613
|
+
? [{ locator, path: entry.path, revision: entry.revision }]
|
|
2614
|
+
: []);
|
|
2615
|
+
}
|
|
2616
|
+
function fetchBinary(request, name) {
|
|
2617
|
+
return transferBinary(request, (authorization) => keptFiles.fetch(name, () => options.adapter.binary(authorization, request)), binaryAnswer);
|
|
2618
|
+
}
|
|
2439
2619
|
async function readBinary(request, name) {
|
|
2440
2620
|
if (closed)
|
|
2441
2621
|
return UNCERTAIN;
|
|
2442
2622
|
const held = await keptFiles.find(name);
|
|
2443
2623
|
if (held !== null)
|
|
2444
2624
|
return held;
|
|
2445
|
-
return
|
|
2625
|
+
return fetchBinary(request, name);
|
|
2446
2626
|
}
|
|
2447
2627
|
async function binaryPath(locator, path, revision) {
|
|
2448
2628
|
if (revision !== undefined)
|
|
2449
2629
|
return { kind: 'path', locator, path, revision };
|
|
2450
2630
|
const replica = replicas.get(locator);
|
|
2451
|
-
if (replica === undefined)
|
|
2452
|
-
return null;
|
|
2453
2631
|
const local = await openLocal();
|
|
2454
|
-
const [held] = await local.entries(replica.record.key, 'visible', [path]);
|
|
2455
|
-
|
|
2632
|
+
const [held] = replica === undefined ? [] : await local.entries(replica.record.key, 'visible', [path]);
|
|
2633
|
+
const attached = held === undefined ? attachedFiles.get(locator)?.get(path) : undefined;
|
|
2634
|
+
if (attached !== undefined)
|
|
2635
|
+
return { kind: 'path', locator, path, revision: attached.revision };
|
|
2636
|
+
if (replica === undefined || held?.binary === undefined || held.binary === null)
|
|
2456
2637
|
return null;
|
|
2457
2638
|
if (held.binary.state === 'pending')
|
|
2458
2639
|
return { kind: 'pending', localId: held.binary.localId };
|
|
@@ -2468,7 +2649,7 @@ export function composeRuntime(options) {
|
|
|
2468
2649
|
const holds = (at) => {
|
|
2469
2650
|
const kept = confirmedLayer.binaries[at];
|
|
2470
2651
|
return (kept?.state === 'accepted' &&
|
|
2471
|
-
kept.
|
|
2652
|
+
kept.sha256 === record.sha256 &&
|
|
2472
2653
|
confirmedLayer.revisions[at] === record.revision);
|
|
2473
2654
|
};
|
|
2474
2655
|
let confirmedAt = path;
|
|
@@ -2482,57 +2663,101 @@ export function composeRuntime(options) {
|
|
|
2482
2663
|
locator,
|
|
2483
2664
|
path: confirmedAt,
|
|
2484
2665
|
revision: record.revision,
|
|
2485
|
-
fileRef: record.fileRef,
|
|
2486
2666
|
};
|
|
2487
2667
|
}
|
|
2488
|
-
async function
|
|
2668
|
+
async function askedBinary(locator, path, revision) {
|
|
2489
2669
|
await ready;
|
|
2490
2670
|
const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
|
|
2491
|
-
|
|
2671
|
+
return drafted !== undefined && drafted.localId === revision
|
|
2492
2672
|
? { kind: 'pending', localId: revision }
|
|
2493
|
-
:
|
|
2673
|
+
: binaryPath(locator, path, revision);
|
|
2674
|
+
}
|
|
2675
|
+
function pendingBinary(locator, path, localId) {
|
|
2676
|
+
const facts = queuedFacts(locator, path, localId);
|
|
2677
|
+
return facts === null
|
|
2678
|
+
? absentBinary(path)
|
|
2679
|
+
: {
|
|
2680
|
+
kind: 'binary',
|
|
2681
|
+
file: localId,
|
|
2682
|
+
byteLength: facts.byteLength,
|
|
2683
|
+
mediaType: facts.mediaType,
|
|
2684
|
+
};
|
|
2685
|
+
}
|
|
2686
|
+
async function readBinaryPath(locator, path, revision) {
|
|
2687
|
+
const asked = await askedBinary(locator, path, revision);
|
|
2494
2688
|
if (asked === null)
|
|
2495
2689
|
return absentBinary(path);
|
|
2496
|
-
if (asked.kind === 'pending')
|
|
2497
|
-
|
|
2498
|
-
return facts === null
|
|
2499
|
-
? absentBinary(path)
|
|
2500
|
-
: {
|
|
2501
|
-
kind: 'binary',
|
|
2502
|
-
file: asked.localId,
|
|
2503
|
-
byteLength: facts.byteLength,
|
|
2504
|
-
mediaType: facts.mediaType,
|
|
2505
|
-
};
|
|
2506
|
-
}
|
|
2690
|
+
if (asked.kind === 'pending')
|
|
2691
|
+
return pendingBinary(locator, path, asked.localId);
|
|
2507
2692
|
return readBinary(asked, { locator, path: asked.path, revision: asked.revision });
|
|
2508
2693
|
}
|
|
2694
|
+
async function heldMediaType(locator, path) {
|
|
2695
|
+
const replica = replicas.get(locator);
|
|
2696
|
+
const [held] = replica === undefined
|
|
2697
|
+
? []
|
|
2698
|
+
: await (await openLocal()).entries(replica.record.key, 'visible', [path]);
|
|
2699
|
+
return held === undefined
|
|
2700
|
+
? attachedFiles.get(locator)?.get(path)?.mediaType
|
|
2701
|
+
: held.binary?.mediaType;
|
|
2702
|
+
}
|
|
2703
|
+
function linkOf(asked, held) {
|
|
2704
|
+
return transferBinary(asked, (authorization) => options.adapter.link(authorization, asked), (result) => binaryLinkAnswer(result, held));
|
|
2705
|
+
}
|
|
2509
2706
|
async function linkBinaryPath(locator, path, revision) {
|
|
2510
2707
|
await ready;
|
|
2511
2708
|
const asked = await binaryPath(locator, path, revision);
|
|
2512
2709
|
if (asked === null || asked.kind === 'pending')
|
|
2513
2710
|
return absentBinary(path);
|
|
2514
|
-
return
|
|
2711
|
+
return linkOf(asked, revision === undefined ? await heldMediaType(locator, path) : undefined);
|
|
2515
2712
|
}
|
|
2516
|
-
async function
|
|
2713
|
+
async function showBinaryPath(locator, path, revision) {
|
|
2714
|
+
const asked = await askedBinary(locator, path, revision);
|
|
2715
|
+
if (asked === null)
|
|
2716
|
+
return absentBinary(path);
|
|
2717
|
+
if (asked.kind === 'pending')
|
|
2718
|
+
return pendingBinary(locator, path, asked.localId);
|
|
2719
|
+
if (closed)
|
|
2720
|
+
return UNCERTAIN;
|
|
2721
|
+
const name = { locator, path: asked.path, revision: asked.revision };
|
|
2722
|
+
const kept = await keptFiles.find(name);
|
|
2723
|
+
if (kept !== null)
|
|
2724
|
+
return kept;
|
|
2725
|
+
const mediaType = await heldMediaType(locator, path);
|
|
2726
|
+
if (mediaType?.startsWith('image/') === true)
|
|
2727
|
+
return fetchBinary(asked, name);
|
|
2728
|
+
return linkOf(asked, revision === undefined ? mediaType : undefined);
|
|
2729
|
+
}
|
|
2730
|
+
async function attachBinary(locator, file) {
|
|
2517
2731
|
await ready;
|
|
2518
|
-
|
|
2519
|
-
if (
|
|
2520
|
-
return
|
|
2521
|
-
|
|
2732
|
+
await firstDiscovery;
|
|
2733
|
+
if (closed || !roomsNamed || connectionOf() === 'unreachable' || refusal !== null)
|
|
2734
|
+
return OFFLINE;
|
|
2735
|
+
const request = await attachedFile(locator, file);
|
|
2736
|
+
return transferBinary({ locator, path: file.name }, async (held) => {
|
|
2737
|
+
const result = await options.adapter.attach(held, request);
|
|
2738
|
+
if (result.kind === 'attached') {
|
|
2739
|
+
const landed = attachedFiles.get(locator) ?? new Map();
|
|
2740
|
+
landed.set(result.path, { revision: result.revision, mediaType: request.mediaType });
|
|
2741
|
+
attachedFiles.set(locator, landed);
|
|
2742
|
+
}
|
|
2743
|
+
return result;
|
|
2744
|
+
}, attachAnswer);
|
|
2522
2745
|
}
|
|
2523
2746
|
function readPart(file, part) {
|
|
2524
|
-
return
|
|
2747
|
+
return database.ask({ kind: 'file.part', key: file, part });
|
|
2525
2748
|
}
|
|
2526
2749
|
async function cleanUp() {
|
|
2527
|
-
const freed = await
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2750
|
+
const freed = await database.ask({ kind: 'opened.clean' });
|
|
2751
|
+
const local = await openLocal();
|
|
2752
|
+
let texts = 0;
|
|
2753
|
+
for (const [locator, replica] of replicas) {
|
|
2754
|
+
const released = await local.unhold(locator, replica.record.key, compositionFor(replica.record));
|
|
2755
|
+
texts += released.bytes;
|
|
2756
|
+
if (released.paths.length > 0) {
|
|
2757
|
+
publishChangeSet(changeSet(locator, { documents: released.paths }));
|
|
2758
|
+
}
|
|
2534
2759
|
}
|
|
2535
|
-
return
|
|
2760
|
+
return freed + texts + ((await options.shell.cleanUp?.()) ?? 0);
|
|
2536
2761
|
}
|
|
2537
2762
|
function declaredAction(name) {
|
|
2538
2763
|
for (const capability of capabilities) {
|
|
@@ -2585,6 +2810,24 @@ export function composeRuntime(options) {
|
|
|
2585
2810
|
return UNCERTAIN;
|
|
2586
2811
|
}
|
|
2587
2812
|
}
|
|
2813
|
+
async function readConversationHistory(request) {
|
|
2814
|
+
await ready;
|
|
2815
|
+
const stream = options.adapter.conversations;
|
|
2816
|
+
if (closed || refusal !== null || stream?.history === undefined)
|
|
2817
|
+
return UNCERTAIN;
|
|
2818
|
+
journal.record('transfer conversation history');
|
|
2819
|
+
try {
|
|
2820
|
+
const result = await stream.history(request);
|
|
2821
|
+
if (result.kind === 'revoked-authorization')
|
|
2822
|
+
await refuse('revoked-authorization');
|
|
2823
|
+
journal.record(`outcome conversation history ${result.kind}`);
|
|
2824
|
+
return calledResult(result);
|
|
2825
|
+
}
|
|
2826
|
+
catch {
|
|
2827
|
+
journal.record('outcome conversation history transport-uncertainty');
|
|
2828
|
+
return UNCERTAIN;
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2588
2831
|
function unsettled(result) {
|
|
2589
2832
|
return result.kind !== 'action' && result.retryable;
|
|
2590
2833
|
}
|
|
@@ -2623,10 +2866,6 @@ export function composeRuntime(options) {
|
|
|
2623
2866
|
continue;
|
|
2624
2867
|
if (closed || signal.aborted)
|
|
2625
2868
|
return 'done';
|
|
2626
|
-
if (pastHorizon(held.operation)) {
|
|
2627
|
-
await local.dropCall(held.operation);
|
|
2628
|
-
continue;
|
|
2629
|
-
}
|
|
2630
2869
|
calling.add(held.operation);
|
|
2631
2870
|
try {
|
|
2632
2871
|
if (unsettled(await invokeAction(held.name, held.input)))
|
|
@@ -2646,6 +2885,33 @@ export function composeRuntime(options) {
|
|
|
2646
2885
|
const held = replicas.get(locator);
|
|
2647
2886
|
return (held !== undefined && options.adapter.reached(locator, held.record.roomVersion, roomVersion));
|
|
2648
2887
|
}
|
|
2888
|
+
function shownAt(locator) {
|
|
2889
|
+
const held = replicas.get(locator)?.record.roomVersion;
|
|
2890
|
+
const raised = highest.get(locator);
|
|
2891
|
+
if (held === undefined || raised === undefined)
|
|
2892
|
+
return held ?? raised;
|
|
2893
|
+
return options.adapter.reached(locator, held, raised) ? held : raised;
|
|
2894
|
+
}
|
|
2895
|
+
function showing(locator, roomVersion) {
|
|
2896
|
+
const at = shownAt(locator);
|
|
2897
|
+
if (at === undefined || !options.adapter.reached(locator, at, roomVersion)) {
|
|
2898
|
+
highest.set(locator, roomVersion);
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
function older(locator, roomVersion) {
|
|
2902
|
+
if (beyondHorizon.has(locator))
|
|
2903
|
+
return false;
|
|
2904
|
+
const at = shownAt(locator);
|
|
2905
|
+
return (at !== undefined &&
|
|
2906
|
+
options.adapter.reached(locator, at, roomVersion) &&
|
|
2907
|
+
!options.adapter.reached(locator, roomVersion, at));
|
|
2908
|
+
}
|
|
2909
|
+
function replaced(locators) {
|
|
2910
|
+
for (const locator of locators) {
|
|
2911
|
+
highest.delete(locator);
|
|
2912
|
+
beyondHorizon.add(locator);
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2649
2915
|
async function writtenAfter(locator, roomVersion, path) {
|
|
2650
2916
|
const held = replicas.get(locator);
|
|
2651
2917
|
if (held === undefined)
|
|
@@ -2656,169 +2922,233 @@ export function composeRuntime(options) {
|
|
|
2656
2922
|
options.adapter.reached(locator, written, roomVersion) &&
|
|
2657
2923
|
!options.adapter.reached(locator, roomVersion, written));
|
|
2658
2924
|
}
|
|
2659
|
-
function
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
else
|
|
2669
|
-
known.set(path, entry.binary !== null ? null : entry.content);
|
|
2670
|
-
}
|
|
2671
|
-
return known;
|
|
2672
|
-
};
|
|
2673
|
-
}
|
|
2674
|
-
async function unreadableRoom(texts, walk, paths) {
|
|
2675
|
-
if (paths !== null) {
|
|
2676
|
-
const text = {};
|
|
2677
|
-
for (const [path, content] of await texts(paths))
|
|
2678
|
-
if (content !== null)
|
|
2679
|
-
text[path] = content;
|
|
2680
|
-
return stepped(unreadableIn(text, Object.keys(text)));
|
|
2681
|
-
}
|
|
2682
|
-
for (let after = null;;) {
|
|
2683
|
-
const page = await walk(after);
|
|
2684
|
-
const text = {};
|
|
2685
|
-
for (const { path, content } of page.entries)
|
|
2686
|
-
if (content !== null)
|
|
2687
|
-
text[path] = content;
|
|
2688
|
-
const refused = await stepped(unreadableIn(text, Object.keys(text)));
|
|
2689
|
-
if (refused !== null)
|
|
2690
|
-
return refused;
|
|
2691
|
-
if (page.next === null)
|
|
2692
|
-
return null;
|
|
2693
|
-
after = page.next;
|
|
2694
|
-
}
|
|
2695
|
-
}
|
|
2696
|
-
async function validatedRoom(local, record, view, previous, overrides = null) {
|
|
2697
|
-
const composition = compositionOf(record);
|
|
2698
|
-
const modeled = record.model.status !== 'unavailable' || record.selectionModel.status !== 'unavailable';
|
|
2699
|
-
const lookup = overrides === null
|
|
2700
|
-
? lookupOf(local, record.key, view)
|
|
2701
|
-
: overriddenLookup(lookupOf(local, record.key, view), overrides);
|
|
2702
|
-
const texts = overrides === null
|
|
2703
|
-
? textsOf(local, record.key, view)
|
|
2704
|
-
: overriddenTexts(textsOf(local, record.key, view), overrides);
|
|
2705
|
-
const walk = overrides === null
|
|
2706
|
-
? walkOf(local, record.key, view)
|
|
2707
|
-
: overriddenWalk(walkOf(local, record.key, view), overrides);
|
|
2708
|
-
let changes = null;
|
|
2709
|
-
let reshaped = false;
|
|
2710
|
-
if (previous !== null && previous.changed.length <= CLEAN_CHANGES) {
|
|
2711
|
-
const [was, now] = await Promise.all([
|
|
2712
|
-
local.entries(record.key, 'confirmed', previous.changed),
|
|
2713
|
-
overrides === null
|
|
2714
|
-
? local.entries(record.key, view, previous.changed)
|
|
2715
|
-
: Promise.resolve([]),
|
|
2716
|
-
]);
|
|
2717
|
-
const before = new Map(was.map((entry) => [entry.path, entry]));
|
|
2718
|
-
const after = overrides === null
|
|
2719
|
-
? new Map(now.map((entry) => [entry.path, entry]))
|
|
2720
|
-
: overrides;
|
|
2721
|
-
changes = previous.changed.flatMap((path) => {
|
|
2722
|
-
const one = before.get(path) ?? null;
|
|
2723
|
-
const other = after.get(path) ?? null;
|
|
2724
|
-
reshaped ||= textDiffers(one, other);
|
|
2725
|
-
const held = textOf(one);
|
|
2726
|
-
const next = textOf(other);
|
|
2727
|
-
return held === next ? [] : [{ path, was: held, now: next }];
|
|
2728
|
-
});
|
|
2729
|
-
}
|
|
2730
|
-
const named = changes === null ? null : changes.map(({ path }) => path);
|
|
2731
|
-
const textChanged = changes === null || changes.length > 0 || reshaped;
|
|
2732
|
-
const modelsChanged = previous === null || !sameComposition(compositionOf(previous.record), composition);
|
|
2733
|
-
if (modeled && (textChanged || modelsChanged)) {
|
|
2734
|
-
const clean = previous !== null &&
|
|
2735
|
-
changes !== null &&
|
|
2736
|
-
(await changedCleanly(changes, compositionOf(previous.record), composition, {
|
|
2737
|
-
lookup,
|
|
2738
|
-
texts,
|
|
2739
|
-
referrers: referrersOf(local, record.key, 'confirmed'),
|
|
2740
|
-
}));
|
|
2741
|
-
if (clean)
|
|
2742
|
-
return null;
|
|
2743
|
-
const verdict = await validatedCollection(walk, lookup, composition);
|
|
2744
|
-
if (verdict.verdict === 'accept')
|
|
2745
|
-
return null;
|
|
2746
|
-
if (verdict.rule !== 'model-unavailable')
|
|
2747
|
-
return { kind: 'refused', refusal: verdict };
|
|
2748
|
-
return unreadableRoom(texts, walk, named);
|
|
2749
|
-
}
|
|
2750
|
-
if (textChanged)
|
|
2751
|
-
return unreadableRoom(texts, walk, named);
|
|
2752
|
-
return null;
|
|
2925
|
+
async function writtenLater(local, locator, key, paths, roomVersion) {
|
|
2926
|
+
if (paths.length === 0 || !older(locator, roomVersion))
|
|
2927
|
+
return new Set();
|
|
2928
|
+
const entries = await local.entries(key, 'confirmed', paths);
|
|
2929
|
+
return new Set(entries.flatMap(({ path, written }) => written !== null &&
|
|
2930
|
+
options.adapter.reached(locator, written, roomVersion) &&
|
|
2931
|
+
!options.adapter.reached(locator, roomVersion, written)
|
|
2932
|
+
? [path]
|
|
2933
|
+
: []));
|
|
2753
2934
|
}
|
|
2754
|
-
function refreshingOf(local, key, liveness) {
|
|
2935
|
+
function refreshingOf(local, locator, key, composition, liveness, replaying = false) {
|
|
2936
|
+
const write = replaying ? finishingWrite : exclusive;
|
|
2755
2937
|
return {
|
|
2938
|
+
turn() {
|
|
2939
|
+
if (replaying)
|
|
2940
|
+
installing(locator);
|
|
2941
|
+
},
|
|
2756
2942
|
prior: async (paths) => new Map((await local.entries(key, 'refreshed', paths)).map((entry) => [entry.path, entry])),
|
|
2943
|
+
later: (paths, roomVersion) => writtenLater(local, locator, key, paths, roomVersion),
|
|
2757
2944
|
async stage(page) {
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
const last =
|
|
2761
|
-
const
|
|
2945
|
+
for (let written = 0;;) {
|
|
2946
|
+
const end = batchEnd(page.entries, written);
|
|
2947
|
+
const last = end === page.entries.length;
|
|
2948
|
+
const entries = page.entries.slice(written, end);
|
|
2949
|
+
const staged = await write(async () => {
|
|
2762
2950
|
if (!liveness())
|
|
2763
2951
|
return false;
|
|
2764
|
-
await local.refresh(key, last
|
|
2952
|
+
await timed(stageBudget, () => local.refresh(key, composition, last
|
|
2765
2953
|
? { ...page, entries }
|
|
2766
|
-
: { entries, removals: [], folders: [], folderRemovals: [] });
|
|
2954
|
+
: { entries, removals: [], folders: [], folderRemovals: [] }));
|
|
2767
2955
|
return true;
|
|
2768
2956
|
});
|
|
2769
2957
|
if (!staged)
|
|
2770
2958
|
return false;
|
|
2959
|
+
if (last)
|
|
2960
|
+
return true;
|
|
2961
|
+
written = end;
|
|
2771
2962
|
}
|
|
2772
|
-
return true;
|
|
2773
2963
|
},
|
|
2774
2964
|
};
|
|
2775
2965
|
}
|
|
2776
|
-
function
|
|
2777
|
-
|
|
2966
|
+
function batchEnd(entries, from) {
|
|
2967
|
+
let end = from;
|
|
2778
2968
|
let spent = 0;
|
|
2779
|
-
|
|
2780
|
-
const size = (
|
|
2781
|
-
if (spent + size >
|
|
2782
|
-
|
|
2783
|
-
spent = 0;
|
|
2784
|
-
}
|
|
2785
|
-
batches.at(-1).push(entry);
|
|
2969
|
+
while (end < entries.length) {
|
|
2970
|
+
const size = (entries[end].content?.length ?? 0) + ROW_WEIGHT;
|
|
2971
|
+
if (spent + size > stageBudget.bytes && end > from)
|
|
2972
|
+
break;
|
|
2786
2973
|
spent += size;
|
|
2787
|
-
|
|
2788
|
-
|
|
2974
|
+
end += 1;
|
|
2975
|
+
}
|
|
2976
|
+
return end;
|
|
2977
|
+
}
|
|
2978
|
+
async function timed(budget, work) {
|
|
2979
|
+
const started = performance.now();
|
|
2980
|
+
const done = await work();
|
|
2981
|
+
const spent = performance.now() - started;
|
|
2982
|
+
if (spent < budget.aim / 2)
|
|
2983
|
+
budget.bytes = Math.min(budget.ceiling, Math.round(budget.bytes * 1.5));
|
|
2984
|
+
else if (spent > budget.aim * 2) {
|
|
2985
|
+
budget.bytes = Math.max(budget.floor, Math.round(budget.bytes / 1.5));
|
|
2986
|
+
}
|
|
2987
|
+
return done;
|
|
2988
|
+
}
|
|
2989
|
+
function installWrite(work) {
|
|
2990
|
+
return installTurn(installTurns.starting, work);
|
|
2991
|
+
}
|
|
2992
|
+
function finishingWrite(work) {
|
|
2993
|
+
return installTurn(installTurns.finishing, work);
|
|
2994
|
+
}
|
|
2995
|
+
function installTurn(queue, work) {
|
|
2996
|
+
return new Promise((resolve, reject) => {
|
|
2997
|
+
queue.push(() => void exclusive(work)
|
|
2998
|
+
.then(resolve, reject)
|
|
2999
|
+
.finally(() => {
|
|
3000
|
+
installTurns.busy = false;
|
|
3001
|
+
nextInstallTurn();
|
|
3002
|
+
}));
|
|
3003
|
+
nextInstallTurn();
|
|
3004
|
+
});
|
|
2789
3005
|
}
|
|
2790
|
-
|
|
2791
|
-
if (
|
|
2792
|
-
return
|
|
2793
|
-
const turn =
|
|
3006
|
+
function nextInstallTurn() {
|
|
3007
|
+
if (installTurns.busy || installTurns.ahead > 0)
|
|
3008
|
+
return;
|
|
3009
|
+
const turn = installTurns.finishing.shift() ?? installTurns.starting.shift();
|
|
3010
|
+
if (turn === undefined)
|
|
3011
|
+
return;
|
|
3012
|
+
installTurns.busy = true;
|
|
3013
|
+
turn();
|
|
3014
|
+
}
|
|
3015
|
+
async function ahead(work) {
|
|
3016
|
+
let taken = false;
|
|
3017
|
+
const hold = {
|
|
3018
|
+
take() {
|
|
3019
|
+
if (taken)
|
|
3020
|
+
return;
|
|
3021
|
+
taken = true;
|
|
3022
|
+
installTurns.ahead += 1;
|
|
3023
|
+
},
|
|
3024
|
+
release() {
|
|
3025
|
+
if (!taken)
|
|
3026
|
+
return;
|
|
3027
|
+
taken = false;
|
|
3028
|
+
installTurns.ahead -= 1;
|
|
3029
|
+
resumeInstall();
|
|
3030
|
+
},
|
|
3031
|
+
};
|
|
3032
|
+
hold.take();
|
|
3033
|
+
try {
|
|
3034
|
+
return await work(hold);
|
|
3035
|
+
}
|
|
3036
|
+
finally {
|
|
3037
|
+
hold.release();
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
function resumeInstall() {
|
|
3041
|
+
if (installTurns.ahead > 0)
|
|
3042
|
+
return;
|
|
3043
|
+
void yielded().then(nextInstallTurn);
|
|
3044
|
+
}
|
|
3045
|
+
async function heldStream() {
|
|
3046
|
+
while (streamsFree === 0)
|
|
3047
|
+
await new Promise((admit) => installWaiters.push(admit));
|
|
3048
|
+
streamsFree -= 1;
|
|
3049
|
+
return once(() => {
|
|
3050
|
+
streamsFree += 1;
|
|
3051
|
+
});
|
|
3052
|
+
}
|
|
3053
|
+
async function unqueued() {
|
|
3054
|
+
while (queuedText >= QUEUED_TEXT) {
|
|
3055
|
+
await new Promise((admit) => installWaiters.push(admit));
|
|
3056
|
+
}
|
|
3057
|
+
}
|
|
3058
|
+
function once(release) {
|
|
3059
|
+
let released = false;
|
|
3060
|
+
return () => {
|
|
3061
|
+
if (released)
|
|
3062
|
+
return;
|
|
3063
|
+
released = true;
|
|
3064
|
+
release();
|
|
3065
|
+
for (const admit of installWaiters.splice(0))
|
|
3066
|
+
admit();
|
|
3067
|
+
};
|
|
3068
|
+
}
|
|
3069
|
+
function installing(locator) {
|
|
3070
|
+
installs.add(locator);
|
|
3071
|
+
queueReadiness();
|
|
3072
|
+
}
|
|
3073
|
+
function untransferred(locator) {
|
|
3074
|
+
transfers.delete(locator);
|
|
3075
|
+
arrivals.delete(locator);
|
|
3076
|
+
streaming.delete(locator);
|
|
3077
|
+
underived.delete(locator);
|
|
3078
|
+
unindexed.delete(locator);
|
|
3079
|
+
}
|
|
3080
|
+
function setBack(locator) {
|
|
3081
|
+
const now = bytesOf(locator, true);
|
|
3082
|
+
const earlier = startedOver.get(locator);
|
|
3083
|
+
startedOver.set(locator, {
|
|
3084
|
+
done: Math.max(now.done, earlier?.done ?? 0),
|
|
3085
|
+
total: Math.max(now.total, earlier?.total ?? 0),
|
|
3086
|
+
});
|
|
3087
|
+
}
|
|
3088
|
+
function owedIndexes(locator, bytes, text) {
|
|
3089
|
+
if (bytes <= 0 || text <= 0)
|
|
3090
|
+
return;
|
|
3091
|
+
unindexed.set(locator, [...(unindexed.get(locator) ?? []), { bytes, text }]);
|
|
3092
|
+
}
|
|
3093
|
+
function built(locator, { done, text }) {
|
|
3094
|
+
const owed = unindexed.get(locator) ?? [];
|
|
3095
|
+
let left = text;
|
|
3096
|
+
while (!done && left > 0 && owed.length > 0) {
|
|
3097
|
+
const [first] = owed;
|
|
3098
|
+
if (left >= first.text) {
|
|
3099
|
+
left -= first.text;
|
|
3100
|
+
owed.shift();
|
|
3101
|
+
continue;
|
|
3102
|
+
}
|
|
3103
|
+
owed[0] = {
|
|
3104
|
+
bytes: (first.bytes * (first.text - left)) / first.text,
|
|
3105
|
+
text: first.text - left,
|
|
3106
|
+
};
|
|
3107
|
+
left = 0;
|
|
3108
|
+
}
|
|
3109
|
+
if (done || owed.length === 0)
|
|
3110
|
+
unindexed.delete(locator);
|
|
3111
|
+
queueReadiness();
|
|
3112
|
+
}
|
|
3113
|
+
function installed(locator) {
|
|
3114
|
+
if (installs.delete(locator))
|
|
3115
|
+
queueReadiness();
|
|
3116
|
+
}
|
|
3117
|
+
async function pullRefresh(authorized, held, liveness, hold, refresh) {
|
|
3118
|
+
if (!liveness())
|
|
3119
|
+
return unconfirmed;
|
|
2794
3120
|
const { record } = held;
|
|
2795
|
-
const
|
|
3121
|
+
const from = standingOf(record.locator);
|
|
2796
3122
|
const local = await openLocal();
|
|
2797
3123
|
try {
|
|
2798
3124
|
await exclusive(() => local.unrefresh(record.key));
|
|
2799
|
-
const result = await advancement(
|
|
3125
|
+
const result = await advancement(async (authorization, request) => {
|
|
3126
|
+
hold.release();
|
|
3127
|
+
try {
|
|
3128
|
+
return await refresh(authorization, request);
|
|
3129
|
+
}
|
|
3130
|
+
finally {
|
|
3131
|
+
hold.take();
|
|
3132
|
+
}
|
|
3133
|
+
}, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.locator, record.key, composed(record), liveness));
|
|
2800
3134
|
if (result.kind !== 'advanced') {
|
|
2801
3135
|
journal.record(`transfer refresh ${record.locator} ${result.kind}`);
|
|
2802
3136
|
return result;
|
|
2803
3137
|
}
|
|
2804
|
-
const refused = await validatedRoom(local, result.record, 'refreshed', {
|
|
2805
|
-
record,
|
|
2806
|
-
changed: [...result.changed],
|
|
2807
|
-
});
|
|
2808
|
-
if (refused !== null) {
|
|
2809
|
-
journal.record(`transfer refresh ${record.locator} refused`);
|
|
2810
|
-
return refused;
|
|
2811
|
-
}
|
|
2812
3138
|
journal.record(`transfer refresh ${record.locator} ${result.record.roomVersion}`);
|
|
2813
|
-
return {
|
|
3139
|
+
return {
|
|
3140
|
+
kind: 'advanced',
|
|
3141
|
+
record: result.record,
|
|
3142
|
+
from,
|
|
3143
|
+
remodelled: result.record.model !== record.model,
|
|
3144
|
+
};
|
|
2814
3145
|
}
|
|
2815
3146
|
catch (error) {
|
|
3147
|
+
if (refusedForSpace(error))
|
|
3148
|
+
return unconfirmed;
|
|
2816
3149
|
journal.record(`transfer refresh ${record.locator} transport-uncertainty`);
|
|
2817
3150
|
throw error;
|
|
2818
3151
|
}
|
|
2819
|
-
finally {
|
|
2820
|
-
reads.finished(record.locator, turn);
|
|
2821
|
-
}
|
|
2822
3152
|
}
|
|
2823
3153
|
const heldContents = new Map();
|
|
2824
3154
|
function heldContentOf(local, key) {
|
|
@@ -2836,63 +3166,115 @@ export function composeRuntime(options) {
|
|
|
2836
3166
|
heldContents.set(key, made);
|
|
2837
3167
|
return made;
|
|
2838
3168
|
}
|
|
3169
|
+
async function abandoned(local, locator) {
|
|
3170
|
+
await local.abandon(locator);
|
|
3171
|
+
setBack(locator);
|
|
3172
|
+
untransferred(locator);
|
|
3173
|
+
liveQueries.recover();
|
|
3174
|
+
sweep();
|
|
3175
|
+
}
|
|
2839
3176
|
function staging(local, liveness) {
|
|
3177
|
+
const derived = async (locator, key, composition) => {
|
|
3178
|
+
underived.delete(locator);
|
|
3179
|
+
for (;;) {
|
|
3180
|
+
if (!liveness())
|
|
3181
|
+
return false;
|
|
3182
|
+
const done = await finishingWrite(async () => liveness()
|
|
3183
|
+
? timed(deriveBudget, () => local.rederive(key, composition, deriveBudget.bytes, DERIVE_LONGEST_MS, DERIVE_FLOOR))
|
|
3184
|
+
: null);
|
|
3185
|
+
if (done === null)
|
|
3186
|
+
return false;
|
|
3187
|
+
built(locator, done);
|
|
3188
|
+
if (done.done)
|
|
3189
|
+
return true;
|
|
3190
|
+
}
|
|
3191
|
+
};
|
|
2840
3192
|
return {
|
|
2841
3193
|
held: (locator) => transfers.get(locator) ?? null,
|
|
3194
|
+
async turn(locator, step) {
|
|
3195
|
+
installing(locator);
|
|
3196
|
+
return step === 'downloading' ? heldStream() : () => { };
|
|
3197
|
+
},
|
|
3198
|
+
unqueued,
|
|
3199
|
+
queued(text) {
|
|
3200
|
+
queuedText += text;
|
|
3201
|
+
return once(() => {
|
|
3202
|
+
queuedText -= text;
|
|
3203
|
+
});
|
|
3204
|
+
},
|
|
3205
|
+
arriving(locator) {
|
|
3206
|
+
let ended = false;
|
|
3207
|
+
return {
|
|
3208
|
+
at(bytes) {
|
|
3209
|
+
if (ended)
|
|
3210
|
+
return;
|
|
3211
|
+
arrivals.set(locator, bytes);
|
|
3212
|
+
streaming.add(locator);
|
|
3213
|
+
const now = performance.now();
|
|
3214
|
+
if (now - arrivalReported < ARRIVAL_REPORT_MS)
|
|
3215
|
+
return;
|
|
3216
|
+
arrivalReported = now;
|
|
3217
|
+
queueReadiness();
|
|
3218
|
+
},
|
|
3219
|
+
end() {
|
|
3220
|
+
if (ended)
|
|
3221
|
+
return;
|
|
3222
|
+
ended = true;
|
|
3223
|
+
if (streaming.delete(locator))
|
|
3224
|
+
queueReadiness();
|
|
3225
|
+
},
|
|
3226
|
+
};
|
|
3227
|
+
},
|
|
3228
|
+
streamed: options.adapter.roomStream !== undefined,
|
|
2842
3229
|
async stage(record, page, composition) {
|
|
2843
|
-
const
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
if (!liveness())
|
|
2850
|
-
return null;
|
|
2851
|
-
if (index === 0 && !(await admitted(local, record.locator, bytes)))
|
|
2852
|
-
return null;
|
|
2853
|
-
try {
|
|
2854
|
-
return await local.stage(record, composition, { entries, folders: last ? page.folders : [] }, key, last);
|
|
2855
|
-
}
|
|
2856
|
-
catch (error) {
|
|
2857
|
-
if (!exceeded(error))
|
|
2858
|
-
throw error;
|
|
2859
|
-
await exceededOn(record.locator, bytes);
|
|
2860
|
-
return null;
|
|
2861
|
-
}
|
|
2862
|
-
});
|
|
2863
|
-
if (staged === null)
|
|
3230
|
+
const { locator } = record;
|
|
3231
|
+
installing(locator);
|
|
3232
|
+
if (!liveness())
|
|
3233
|
+
return null;
|
|
3234
|
+
const key = await installWrite(async () => {
|
|
3235
|
+
if (!liveness())
|
|
2864
3236
|
return null;
|
|
2865
|
-
|
|
2866
|
-
}
|
|
2867
|
-
|
|
3237
|
+
return local.stage(record, composition, page);
|
|
3238
|
+
});
|
|
3239
|
+
if (key === null || !liveness())
|
|
3240
|
+
return null;
|
|
3241
|
+
const arrived = !transfers.has(locator);
|
|
3242
|
+
const prior = transfers.get(locator)?.done.bytes ?? 0;
|
|
3243
|
+
const held = { ...record, key };
|
|
3244
|
+
transfers.set(locator, held);
|
|
3245
|
+
if (arrived)
|
|
3246
|
+
liveQueries.recover();
|
|
3247
|
+
installing(locator);
|
|
2868
3248
|
liveQueries.recover();
|
|
2869
|
-
|
|
3249
|
+
const text = page.entries.reduce((sum, entry) => sum + (entry.content?.length ?? 0), 0);
|
|
3250
|
+
owedIndexes(locator, record.done.bytes - prior, text);
|
|
3251
|
+
const owed = text + (underived.get(locator) ?? 0);
|
|
3252
|
+
underived.set(locator, owed);
|
|
3253
|
+
if (owed < deriveBudget.bytes)
|
|
3254
|
+
return held;
|
|
3255
|
+
return (await derived(locator, key, composition)) ? held : null;
|
|
2870
3256
|
},
|
|
2871
|
-
|
|
3257
|
+
derive: derived,
|
|
3258
|
+
async refreshing(locator, key, composition) {
|
|
2872
3259
|
await exclusive(() => local.unrefresh(key));
|
|
2873
|
-
return refreshingOf(local, key, liveness);
|
|
3260
|
+
return refreshingOf(local, locator, key, composition, liveness, true);
|
|
2874
3261
|
},
|
|
2875
3262
|
abandon: (locator) => exclusive(async () => {
|
|
2876
|
-
if (
|
|
2877
|
-
|
|
2878
|
-
await local.abandon(locator);
|
|
2879
|
-
transfers.delete(locator);
|
|
2880
|
-
liveQueries.recover();
|
|
2881
|
-
sweep();
|
|
3263
|
+
if (liveness())
|
|
3264
|
+
await abandoned(local, locator);
|
|
2882
3265
|
}),
|
|
2883
|
-
setAside:
|
|
3266
|
+
setAside: async (locator) => {
|
|
2884
3267
|
if (!liveness())
|
|
2885
3268
|
return;
|
|
2886
|
-
|
|
2887
|
-
|
|
3269
|
+
setBack(locator);
|
|
3270
|
+
untransferred(locator);
|
|
2888
3271
|
liveQueries.recover();
|
|
2889
|
-
}
|
|
3272
|
+
},
|
|
2890
3273
|
release: (key) => exclusive(async () => {
|
|
2891
3274
|
await local.release(key);
|
|
2892
3275
|
sweep();
|
|
2893
3276
|
}),
|
|
2894
3277
|
content: (key) => heldContentOf(local, key),
|
|
2895
|
-
validate: (record, view) => validatedRoom(local, record, view, null),
|
|
2896
3278
|
};
|
|
2897
3279
|
}
|
|
2898
3280
|
function reusable(room, local) {
|
|
@@ -2904,30 +3286,35 @@ export function composeRuntime(options) {
|
|
|
2904
3286
|
async function pullBootstrap(authorized, room, local, liveness) {
|
|
2905
3287
|
if (!liveness())
|
|
2906
3288
|
return unconfirmed;
|
|
2907
|
-
const turn = generation;
|
|
2908
3289
|
if (replicas.has(room.locator)) {
|
|
2909
3290
|
catching.add(room.locator);
|
|
2910
3291
|
liveQueries.recover();
|
|
2911
3292
|
}
|
|
2912
3293
|
try {
|
|
2913
|
-
const result = await bootstrapRoom(reads, authorized, room, {
|
|
2914
|
-
collection: modelOf(room.locator),
|
|
2915
|
-
selection: modelOf(room.locator, 'typed-selection'),
|
|
2916
|
-
}, (models, held) => (liveness() ? remodel(room.locator, models, held) : held), liveness, staging(local, liveness), reusable(room, local));
|
|
3294
|
+
const result = await bootstrapRoom(reads, authorized, room, { collection: modelOf(room.locator) }, builtIn, typedPaths, (models, held) => (liveness() ? remodel(room.locator, models, held) : held), liveness, staging(local, liveness), reusable(room, local));
|
|
2917
3295
|
const detail = result.kind === 'staged' ? result.record.roomVersion : result.kind;
|
|
2918
3296
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
2919
|
-
if (result.kind
|
|
3297
|
+
if (result.kind === 'staged' && older(room.locator, result.record.roomVersion)) {
|
|
2920
3298
|
caughtUpTo(room.locator);
|
|
3299
|
+
installed(room.locator);
|
|
3300
|
+
await abandoned(local, room.locator);
|
|
3301
|
+
journal.record(`transfer bootstrap ${room.locator} older than shown`);
|
|
3302
|
+
return { kind: 'transport-uncertainty' };
|
|
3303
|
+
}
|
|
3304
|
+
if (result.kind !== 'staged') {
|
|
3305
|
+
caughtUpTo(room.locator);
|
|
3306
|
+
installed(room.locator);
|
|
3307
|
+
}
|
|
2921
3308
|
return result;
|
|
2922
3309
|
}
|
|
2923
3310
|
catch (error) {
|
|
2924
|
-
journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
|
|
2925
3311
|
caughtUpTo(room.locator);
|
|
3312
|
+
installed(room.locator);
|
|
3313
|
+
if (refusedForSpace(error))
|
|
3314
|
+
return unconfirmed;
|
|
3315
|
+
journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
|
|
2926
3316
|
throw error;
|
|
2927
3317
|
}
|
|
2928
|
-
finally {
|
|
2929
|
-
reads.finished(room.locator, turn);
|
|
2930
|
-
}
|
|
2931
3318
|
}
|
|
2932
3319
|
function caughtUpTo(locator) {
|
|
2933
3320
|
if (catching.delete(locator))
|
|
@@ -2935,72 +3322,83 @@ export function composeRuntime(options) {
|
|
|
2935
3322
|
}
|
|
2936
3323
|
function behind(room) {
|
|
2937
3324
|
const held = replicas.get(room.locator)?.record.roomVersion;
|
|
2938
|
-
return
|
|
2939
|
-
[announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
|
|
3325
|
+
return held !== undefined && !options.adapter.reached(room.locator, held, room.roomVersion);
|
|
2940
3326
|
}
|
|
2941
|
-
|
|
2942
|
-
const
|
|
2943
|
-
|
|
2944
|
-
if (withheld !== null || reads.carried(room.locator) === carried || !behind(room)) {
|
|
2945
|
-
return withheld;
|
|
2946
|
-
}
|
|
2947
|
-
return advanceOnce(room, local, authorized, live);
|
|
3327
|
+
function standingOf(locator) {
|
|
3328
|
+
const held = replicas.get(locator);
|
|
3329
|
+
return { key: held?.record.key ?? null, confirmed: held?.confirmed ?? 0 };
|
|
2948
3330
|
}
|
|
2949
|
-
|
|
2950
|
-
|
|
3331
|
+
function stands(locator, from) {
|
|
3332
|
+
const now = standingOf(locator);
|
|
3333
|
+
return now.key === from.key && now.confirmed === from.confirmed;
|
|
3334
|
+
}
|
|
3335
|
+
async function advance(room, local, authorized, running) {
|
|
3336
|
+
const live = () => running() && !full;
|
|
3337
|
+
if (!live())
|
|
2951
3338
|
return unconfirmed;
|
|
2952
3339
|
const held = replicas.get(room.locator);
|
|
2953
|
-
if (held !== undefined) {
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
emitRooms();
|
|
2965
|
-
}
|
|
2966
|
-
return null;
|
|
2967
|
-
}
|
|
2968
|
-
if (refreshed.kind === 'advanced') {
|
|
2969
|
-
const landing = { kind: 'refreshed', base: refreshed.base };
|
|
2970
|
-
if (!(await publish(local, room, refreshed.record, landing, live))) {
|
|
2971
|
-
const now = replicas.get(room.locator)?.record.roomVersion;
|
|
2972
|
-
const reached = now !== undefined &&
|
|
2973
|
-
options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
|
|
2974
|
-
return reached ? null : advance(room, local, authorized, live);
|
|
2975
|
-
}
|
|
2976
|
-
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
2977
|
-
return null;
|
|
2978
|
-
}
|
|
2979
|
-
if (refreshed.kind !== 'beyond-horizon')
|
|
3340
|
+
if (held !== undefined && options.adapter.roomStream !== undefined) {
|
|
3341
|
+
stale.delete(room.locator);
|
|
3342
|
+
name(room.locator);
|
|
3343
|
+
return null;
|
|
3344
|
+
}
|
|
3345
|
+
const refresh = reads.refresh;
|
|
3346
|
+
if (held !== undefined && refresh !== undefined) {
|
|
3347
|
+
const refreshed = await ahead((hold) => refreshedRoom(room, local, authorized, held, live, hold, refresh));
|
|
3348
|
+
if (refreshed === 'behind')
|
|
3349
|
+
return advance(room, local, authorized, live);
|
|
3350
|
+
if (refreshed !== 'beyond-horizon')
|
|
2980
3351
|
return refreshed;
|
|
2981
|
-
if (
|
|
2982
|
-
owed.add(room.locator);
|
|
2983
|
-
liveQueries.recover();
|
|
2984
|
-
return unconfirmed;
|
|
2985
|
-
}
|
|
2986
|
-
if (reloadsDue.get(room.locator) === generation)
|
|
3352
|
+
if (reloadsDue.has(room.locator))
|
|
2987
3353
|
return unconfirmed;
|
|
3354
|
+
replaced([room.locator]);
|
|
2988
3355
|
}
|
|
3356
|
+
const from = standingOf(room.locator);
|
|
2989
3357
|
const bootstrapped = await pullBootstrap(authorized, room, local, live);
|
|
3358
|
+
if (bootstrapped.kind !== 'staged')
|
|
3359
|
+
return live() ? bootstrapped : unconfirmed;
|
|
3360
|
+
const landing = { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from };
|
|
3361
|
+
if (await publish(local, room, bootstrapped.record, landing, live)) {
|
|
3362
|
+
return live() ? null : unconfirmed;
|
|
3363
|
+
}
|
|
3364
|
+
return reachedBy(room.locator, bootstrapped.record.roomVersion)
|
|
3365
|
+
? null
|
|
3366
|
+
: advance(room, local, authorized, live);
|
|
3367
|
+
}
|
|
3368
|
+
async function refreshedRoom(room, local, authorized, held, live, hold, refresh) {
|
|
3369
|
+
const refreshed = await pullRefresh(authorized, held, live, hold, refresh);
|
|
2990
3370
|
if (!live())
|
|
2991
3371
|
return unconfirmed;
|
|
2992
|
-
if (
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
3372
|
+
if (refreshed.kind === 'still-current') {
|
|
3373
|
+
stale.delete(room.locator);
|
|
3374
|
+
if (unreadable.delete(room.locator)) {
|
|
3375
|
+
scopedRefusals.delete(room.locator);
|
|
3376
|
+
list(room, true);
|
|
3377
|
+
emitRooms();
|
|
3378
|
+
}
|
|
3379
|
+
return null;
|
|
3380
|
+
}
|
|
3381
|
+
if (refreshed.kind === 'advanced') {
|
|
3382
|
+
const landing = {
|
|
3383
|
+
kind: 'refreshed',
|
|
3384
|
+
from: refreshed.from,
|
|
3385
|
+
remodelled: refreshed.remodelled,
|
|
3386
|
+
};
|
|
3387
|
+
if (!(await publish(local, room, refreshed.record, landing, live))) {
|
|
3388
|
+
const now = replicas.get(room.locator)?.record.roomVersion;
|
|
3389
|
+
const reached = now !== undefined &&
|
|
3390
|
+
options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
|
|
3391
|
+
return reached ? null : 'behind';
|
|
3392
|
+
}
|
|
3393
|
+
await retainModel(local, room, modelOf(room.locator));
|
|
3394
|
+
return null;
|
|
3395
|
+
}
|
|
3396
|
+
return refreshed.kind === 'beyond-horizon' ? 'beyond-horizon' : refreshed;
|
|
2996
3397
|
}
|
|
2997
3398
|
async function withhold(withheld, room, local, progress) {
|
|
2998
|
-
if (withheld.kind === 'unconfirmed')
|
|
3399
|
+
if (withheld.kind === 'unconfirmed' || isFenced(room.locator))
|
|
2999
3400
|
return;
|
|
3000
|
-
|
|
3001
|
-
stale.delete(room.locator);
|
|
3002
|
-
else
|
|
3003
|
-
stale.add(room.locator);
|
|
3401
|
+
stale.add(room.locator);
|
|
3004
3402
|
if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
|
|
3005
3403
|
const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
|
|
3006
3404
|
repairable.set(room.locator, reset);
|
|
@@ -3022,33 +3420,11 @@ export function composeRuntime(options) {
|
|
|
3022
3420
|
list(room, false);
|
|
3023
3421
|
emitRooms();
|
|
3024
3422
|
}
|
|
3025
|
-
else if (withheld.kind === 'refused') {
|
|
3026
|
-
refusedSource(room, withheld);
|
|
3027
|
-
}
|
|
3028
3423
|
else if (withheld.kind === 'revoked-authorization') {
|
|
3029
3424
|
await refuse('revoked-authorization');
|
|
3030
3425
|
}
|
|
3031
3426
|
}
|
|
3032
|
-
function refusedSource(room, { refusal }) {
|
|
3033
|
-
sourceRefusals.set(room.locator, {
|
|
3034
|
-
code: refusal.rule,
|
|
3035
|
-
retryable: false,
|
|
3036
|
-
fields: {
|
|
3037
|
-
file: refusal.file,
|
|
3038
|
-
...(refusal.location === undefined ? {} : { location: refusal.location }),
|
|
3039
|
-
...(refusal.referrers === undefined ? {} : { referrers: refusal.referrers }),
|
|
3040
|
-
},
|
|
3041
|
-
});
|
|
3042
|
-
declarationRefusals.delete(room.locator);
|
|
3043
|
-
unreadable.add(room.locator);
|
|
3044
|
-
list(room, false);
|
|
3045
|
-
emitRooms();
|
|
3046
|
-
moveTo({ ...status, ...attention() });
|
|
3047
|
-
journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
|
|
3048
|
-
}
|
|
3049
3427
|
function leaving(batch, result) {
|
|
3050
|
-
if (result.kind === 'expired')
|
|
3051
|
-
return { batch, returned: returnedBy('expired') };
|
|
3052
3428
|
return {
|
|
3053
3429
|
batch,
|
|
3054
3430
|
returned: {
|
|
@@ -3067,8 +3443,8 @@ export function composeRuntime(options) {
|
|
|
3067
3443
|
function settling(acceptance) {
|
|
3068
3444
|
return acceptance !== null && 'apply' in acceptance;
|
|
3069
3445
|
}
|
|
3070
|
-
function
|
|
3071
|
-
return settling(acceptance) ?
|
|
3446
|
+
function failureOf(acceptance) {
|
|
3447
|
+
return settling(acceptance) ? null : acceptance;
|
|
3072
3448
|
}
|
|
3073
3449
|
async function acceptance(room, local, submitted, accepted) {
|
|
3074
3450
|
if (!continuing())
|
|
@@ -3081,7 +3457,8 @@ export function composeRuntime(options) {
|
|
|
3081
3457
|
logAccepted(room.locator, submitted, accepted);
|
|
3082
3458
|
let record = current.record;
|
|
3083
3459
|
let changes;
|
|
3084
|
-
if (!options.adapter.reached(room.locator, held, accepted.roomVersion)
|
|
3460
|
+
if (!options.adapter.reached(room.locator, held, accepted.roomVersion) &&
|
|
3461
|
+
!older(room.locator, accepted.roomVersion)) {
|
|
3085
3462
|
const paths = [...returnedPaths(submitted, accepted)];
|
|
3086
3463
|
const confirmedLayer = await stepped(slicing(await local.entries(record.key, 'confirmed', paths), paths));
|
|
3087
3464
|
const back = await stepped(returning(confirmedLayer, submitted, accepted, accepted.roomVersion));
|
|
@@ -3091,34 +3468,6 @@ export function composeRuntime(options) {
|
|
|
3091
3468
|
? accepted.roomVersion
|
|
3092
3469
|
: held,
|
|
3093
3470
|
};
|
|
3094
|
-
const overrides = new Map([
|
|
3095
|
-
...back.set.map((entry) => [entry.path, entry]),
|
|
3096
|
-
...back.removed.map((path) => [path, null]),
|
|
3097
|
-
]);
|
|
3098
|
-
const refused = await validatedRoom(local, record, 'confirmed', { record: current.record, changed: [...overrides.keys()] }, overrides);
|
|
3099
|
-
if (refused !== null) {
|
|
3100
|
-
const key = pendingKey(room.locator, submitted.id);
|
|
3101
|
-
return {
|
|
3102
|
-
locator: room.locator,
|
|
3103
|
-
left: [],
|
|
3104
|
-
commit: {
|
|
3105
|
-
rooms: [],
|
|
3106
|
-
writes: [
|
|
3107
|
-
{
|
|
3108
|
-
locator: room.locator,
|
|
3109
|
-
accepted: [{ id: submitted.id, roomVersion: accepted.roomVersion }],
|
|
3110
|
-
},
|
|
3111
|
-
],
|
|
3112
|
-
},
|
|
3113
|
-
refused,
|
|
3114
|
-
apply() {
|
|
3115
|
-
const state = pendings.get(key);
|
|
3116
|
-
if (state !== undefined && !fences.has(room.locator))
|
|
3117
|
-
pendings.set(key, { ...state, accepted: accepted.roomVersion });
|
|
3118
|
-
return changeSet(room.locator, { queue: true });
|
|
3119
|
-
},
|
|
3120
|
-
};
|
|
3121
|
-
}
|
|
3122
3471
|
changes = back;
|
|
3123
3472
|
}
|
|
3124
3473
|
const settled = await settlement(local, room.locator, current, record, current.queue.filter((batch) => batch.id !== submitted.id), { room: true, ...(changes === undefined ? {} : { changes }) });
|
|
@@ -3126,10 +3475,9 @@ export function composeRuntime(options) {
|
|
|
3126
3475
|
...settled,
|
|
3127
3476
|
apply(answer) {
|
|
3128
3477
|
const set = settled.apply(answer);
|
|
3478
|
+
showing(room.locator, accepted.roomVersion);
|
|
3129
3479
|
pendings.delete(pendingKey(room.locator, submitted.id));
|
|
3130
3480
|
unreadable.delete(room.locator);
|
|
3131
|
-
sourceRefusals.delete(room.locator);
|
|
3132
|
-
declarationRefusals.delete(room.locator);
|
|
3133
3481
|
list(room, true);
|
|
3134
3482
|
return set;
|
|
3135
3483
|
},
|
|
@@ -3147,13 +3495,9 @@ export function composeRuntime(options) {
|
|
|
3147
3495
|
const failed = await exclusive(async () => {
|
|
3148
3496
|
const adopting = await acceptance(room, local, submitted, accepted);
|
|
3149
3497
|
await adoptedTogether(local, [adopting]);
|
|
3150
|
-
return
|
|
3498
|
+
return failureOf(adopting);
|
|
3151
3499
|
});
|
|
3152
|
-
|
|
3153
|
-
return true;
|
|
3154
|
-
if (failed.kind === 'refused' && continuing())
|
|
3155
|
-
refusedSource(room, failed);
|
|
3156
|
-
return false;
|
|
3500
|
+
return failed === null;
|
|
3157
3501
|
}
|
|
3158
3502
|
async function adoptedMove(room, partner, local, submitted, accepted) {
|
|
3159
3503
|
const other = discovered.get(partner);
|
|
@@ -3167,18 +3511,10 @@ export function composeRuntime(options) {
|
|
|
3167
3511
|
...accepted.destination,
|
|
3168
3512
|
});
|
|
3169
3513
|
await adoptedTogether(local, [left, arrived]);
|
|
3170
|
-
|
|
3171
|
-
if (other !== undefined && refusedArrival?.kind === 'refused' && continuing()) {
|
|
3172
|
-
refusedSource(other, refusedArrival);
|
|
3173
|
-
}
|
|
3174
|
-
return refusalOf(left);
|
|
3514
|
+
return failureOf(left);
|
|
3175
3515
|
});
|
|
3176
3516
|
submitLater(partner);
|
|
3177
|
-
|
|
3178
|
-
return true;
|
|
3179
|
-
if (failed.kind === 'refused' && continuing())
|
|
3180
|
-
refusedSource(room, failed);
|
|
3181
|
-
return false;
|
|
3517
|
+
return failed === null;
|
|
3182
3518
|
}
|
|
3183
3519
|
function queueIn(locator) {
|
|
3184
3520
|
return replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
@@ -3289,20 +3625,13 @@ export function composeRuntime(options) {
|
|
|
3289
3625
|
return;
|
|
3290
3626
|
moveTo({ ...status, ...attention() });
|
|
3291
3627
|
}
|
|
3292
|
-
async function discardHeld(head, room, local, signal) {
|
|
3293
|
-
const partner = partnerOf(head, room.locator);
|
|
3294
|
-
const other = partner === null ? undefined : discovered.get(partner);
|
|
3295
|
-
await discardPending(other === undefined ? [room] : [room, other], local, signal);
|
|
3296
|
-
}
|
|
3297
3628
|
async function submitRoom(room, local, authorized, progress, signal) {
|
|
3298
3629
|
while (continuing() && authorization !== undefined && !signal.aborted) {
|
|
3299
3630
|
const replica = replicas.get(room.locator);
|
|
3300
|
-
const head = replica?.queue
|
|
3631
|
+
const head = replica?.queue.find((batch) => !landedAhead.has(pendingKey(room.locator, batch.id)));
|
|
3301
3632
|
if (replica === undefined || head === undefined)
|
|
3302
3633
|
return 'done';
|
|
3303
3634
|
const state = pendings.get(pendingKey(room.locator, head.id));
|
|
3304
|
-
if (state?.accepted !== undefined)
|
|
3305
|
-
return 'done';
|
|
3306
3635
|
const partner = partnerOf(head, room.locator);
|
|
3307
3636
|
if (partner !== null && !leavingIn(head, room.locator)) {
|
|
3308
3637
|
submitLater(partner);
|
|
@@ -3326,10 +3655,6 @@ export function composeRuntime(options) {
|
|
|
3326
3655
|
return 'done';
|
|
3327
3656
|
}
|
|
3328
3657
|
const lost = earlier || submission.state === 'submitted';
|
|
3329
|
-
if (lost && pastHorizon(head.id)) {
|
|
3330
|
-
await discardHeld(head, room, local, signal);
|
|
3331
|
-
return 'done';
|
|
3332
|
-
}
|
|
3333
3658
|
let result;
|
|
3334
3659
|
journal.record(`submission ${room.locator} ${head.id}`);
|
|
3335
3660
|
await exclusive(() => mark(local, room.locator, head, { state: 'submitted' }, lost));
|
|
@@ -3407,32 +3732,12 @@ export function composeRuntime(options) {
|
|
|
3407
3732
|
const other = partner === null ? undefined : discovered.get(partner);
|
|
3408
3733
|
if (result.kind === 'conflicted' ||
|
|
3409
3734
|
result.kind === 'rejected' ||
|
|
3410
|
-
result.kind === 'expired' ||
|
|
3411
3735
|
result.kind === 'retryable-refusal') {
|
|
3412
|
-
if (result.kind === 'expired' && lost && result.restored !== true) {
|
|
3413
|
-
journal.record(`outcome ${room.locator} ${head.id} expired`);
|
|
3414
|
-
await discardHeld(head, room, local, signal);
|
|
3415
|
-
return 'done';
|
|
3416
|
-
}
|
|
3417
|
-
if (result.kind === 'expired') {
|
|
3418
|
-
await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, partner !== null && lost));
|
|
3419
|
-
const there = other === undefined ? null : await advance(other, local, authorized, continuing);
|
|
3420
|
-
if (other !== undefined && there !== null) {
|
|
3421
|
-
await withhold(there, other, local, progress);
|
|
3422
|
-
return 'done';
|
|
3423
|
-
}
|
|
3424
|
-
const withheld = await advance(room, local, authorized, continuing);
|
|
3425
|
-
if (withheld !== null) {
|
|
3426
|
-
await withhold(withheld, room, local, progress);
|
|
3427
|
-
return 'done';
|
|
3428
|
-
}
|
|
3429
|
-
}
|
|
3430
3736
|
journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` +
|
|
3431
3737
|
(result.kind === 'conflicted' && result.roomVersion !== undefined
|
|
3432
3738
|
? ` ${result.roomVersion}`
|
|
3433
3739
|
: ''));
|
|
3434
|
-
|
|
3435
|
-
journal.record(`refusal ${result.kind} ${result.code}`);
|
|
3740
|
+
journal.record(`refusal ${result.kind} ${result.code}`);
|
|
3436
3741
|
}
|
|
3437
3742
|
if (result.kind === 'retryable-refusal') {
|
|
3438
3743
|
const reset = result.reset;
|
|
@@ -3440,11 +3745,11 @@ export function composeRuntime(options) {
|
|
|
3440
3745
|
await exclusive(() => mark(local, room.locator, head, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
|
|
3441
3746
|
return reset === undefined ? 'again' : { at: Date.parse(reset) };
|
|
3442
3747
|
}
|
|
3443
|
-
if (result.kind === 'conflicted' || result.kind === 'rejected'
|
|
3748
|
+
if (result.kind === 'conflicted' || result.kind === 'rejected') {
|
|
3444
3749
|
const left = leaving(head, result);
|
|
3445
3750
|
const refreshed = async () => ({
|
|
3446
|
-
there: other === undefined ? null : await
|
|
3447
|
-
here: await
|
|
3751
|
+
there: other === undefined ? null : await advanced(other, local, authorized),
|
|
3752
|
+
here: await advanced(room, local, authorized),
|
|
3448
3753
|
});
|
|
3449
3754
|
const judged = result.kind === 'conflicted' && other !== undefined ? await refreshed() : null;
|
|
3450
3755
|
if (judged !== null && (judged.there !== null || judged.here !== null)) {
|
|
@@ -3464,16 +3769,19 @@ export function composeRuntime(options) {
|
|
|
3464
3769
|
return;
|
|
3465
3770
|
}
|
|
3466
3771
|
const confirmedLayer = await sliceAt(current.record, 'confirmed', [head]);
|
|
3467
|
-
const held =
|
|
3468
|
-
(await landedWhole(room.locator, head, confirmedLayer))
|
|
3469
|
-
|
|
3470
|
-
|
|
3772
|
+
const held = result.kind === 'conflicted' &&
|
|
3773
|
+
(await landedWhole(room.locator, head, confirmedLayer, result.affected));
|
|
3774
|
+
if (held &&
|
|
3775
|
+
result.kind === 'conflicted' &&
|
|
3776
|
+
partner === null &&
|
|
3777
|
+
!landed(head, confirmedLayer)) {
|
|
3778
|
+
landedAhead.set(pendingKey(room.locator, head.id), Object.fromEntries([...touchedBy([head])].map((path) => [path, confirmedLayer.revisions[path]])));
|
|
3779
|
+
return;
|
|
3780
|
+
}
|
|
3471
3781
|
publishChangeSet(await settle(local, room.locator, current, current.record, current.queue.filter((batch) => batch.id !== head.id), { leaving: [held ? { ...left, returned: null } : left] }));
|
|
3472
3782
|
await unpair(local);
|
|
3473
3783
|
moveTo({ ...status, ...attention() });
|
|
3474
3784
|
});
|
|
3475
|
-
if (result.kind === 'expired')
|
|
3476
|
-
continue;
|
|
3477
3785
|
const { there, here } = judged ?? (await refreshed());
|
|
3478
3786
|
if (other !== undefined && there !== null)
|
|
3479
3787
|
await withhold(there, other, local, progress);
|
|
@@ -3493,93 +3801,55 @@ export function composeRuntime(options) {
|
|
|
3493
3801
|
}
|
|
3494
3802
|
return 'done';
|
|
3495
3803
|
}
|
|
3496
|
-
function pastHorizon(identity) {
|
|
3497
|
-
return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
|
|
3498
|
-
}
|
|
3499
|
-
async function discardPending(rooms, local, signal) {
|
|
3500
|
-
const discarded = await exclusive(async () => {
|
|
3501
|
-
const held = rooms
|
|
3502
|
-
.filter(({ locator }) => !fences.has(locator))
|
|
3503
|
-
.map((room) => ({
|
|
3504
|
-
room,
|
|
3505
|
-
before: replicas.get(room.locator),
|
|
3506
|
-
queue: queueIn(room.locator),
|
|
3507
|
-
}));
|
|
3508
|
-
if (held.length === 0)
|
|
3509
|
-
return [];
|
|
3510
|
-
const answer = await local.commit({
|
|
3511
|
-
rooms: held.flatMap(({ room, before }) => before === undefined
|
|
3512
|
-
? []
|
|
3513
|
-
: [
|
|
3514
|
-
{
|
|
3515
|
-
locator: room.locator,
|
|
3516
|
-
key: before.record.key,
|
|
3517
|
-
roots: before.record.typedSelection?.roots ?? [],
|
|
3518
|
-
overlay: { set: [], cleared: [...before.overlaid] },
|
|
3519
|
-
},
|
|
3520
|
-
]),
|
|
3521
|
-
discards: held.map(({ room }) => room.locator),
|
|
3522
|
-
});
|
|
3523
|
-
for (const { room, before, queue } of held) {
|
|
3524
|
-
const { locator } = room;
|
|
3525
|
-
lastFenced.set(locator, ++fencings);
|
|
3526
|
-
for (const batch of queue)
|
|
3527
|
-
pendings.delete(pendingKey(locator, batch.id));
|
|
3528
|
-
queuesWithoutReplica.delete(locator);
|
|
3529
|
-
if (before !== undefined) {
|
|
3530
|
-
replicas.set(locator, {
|
|
3531
|
-
...before,
|
|
3532
|
-
queue: [],
|
|
3533
|
-
overlaid: new Set(),
|
|
3534
|
-
count: answer.counts[locator] ?? before.count,
|
|
3535
|
-
});
|
|
3536
|
-
}
|
|
3537
|
-
journal.record(`room discarded ${locator}`);
|
|
3538
|
-
}
|
|
3539
|
-
publishChangeSet(mergeChangeSets(held.map(({ room }) => changeSet(room.locator, {
|
|
3540
|
-
documents: answer.changed[room.locator] ?? [],
|
|
3541
|
-
queue: true,
|
|
3542
|
-
}))));
|
|
3543
|
-
moveTo({ ...status, ...attention() });
|
|
3544
|
-
return held.map(({ room }) => room);
|
|
3545
|
-
});
|
|
3546
|
-
for (const room of discarded) {
|
|
3547
|
-
if (signal.aborted)
|
|
3548
|
-
return;
|
|
3549
|
-
const progress = { complete: true, reachable: true };
|
|
3550
|
-
await roomTask(room.locator, async (live) => {
|
|
3551
|
-
await Promise.race([
|
|
3552
|
-
reload(room, local, progress, () => live() && !signal.aborted),
|
|
3553
|
-
abandonedOn(signal),
|
|
3554
|
-
]);
|
|
3555
|
-
});
|
|
3556
|
-
if (signal.aborted)
|
|
3557
|
-
return;
|
|
3558
|
-
await rest(progress);
|
|
3559
|
-
}
|
|
3560
|
-
}
|
|
3561
3804
|
function awaitingAnswer(locator) {
|
|
3805
|
+
answering.get(locator)?.release();
|
|
3562
3806
|
let release = () => { };
|
|
3563
|
-
const
|
|
3807
|
+
const answered = new Promise((resolve) => {
|
|
3564
3808
|
release = resolve;
|
|
3565
3809
|
});
|
|
3566
|
-
|
|
3810
|
+
const awaited = { answered, release };
|
|
3811
|
+
answering.set(locator, awaited);
|
|
3567
3812
|
return () => {
|
|
3568
|
-
if (answering.get(locator)
|
|
3813
|
+
if (answering.get(locator) === awaited)
|
|
3569
3814
|
answering.delete(locator);
|
|
3570
3815
|
release();
|
|
3571
3816
|
};
|
|
3572
3817
|
}
|
|
3573
|
-
|
|
3574
|
-
const
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3818
|
+
function declaredAsHeld(room) {
|
|
3819
|
+
const held = replicas.get(room.locator)?.record;
|
|
3820
|
+
return (held === undefined ||
|
|
3821
|
+
(JSON.stringify(held.operations) === JSON.stringify(room.operations) &&
|
|
3822
|
+
JSON.stringify(held.replica) === JSON.stringify(room.replica) &&
|
|
3823
|
+
JSON.stringify(held.typedSelection) === JSON.stringify(room.typedSelection ?? null)));
|
|
3824
|
+
}
|
|
3825
|
+
async function keepNaming(local, room) {
|
|
3826
|
+
const held = replicas.get(room.locator)?.record;
|
|
3827
|
+
if (held === undefined || (held.name === room.name && held.handle === room.handle))
|
|
3828
|
+
return;
|
|
3829
|
+
await exclusive(async () => {
|
|
3830
|
+
if (!continuing())
|
|
3831
|
+
return;
|
|
3832
|
+
const replica = replicas.get(room.locator);
|
|
3833
|
+
if (replica === undefined)
|
|
3834
|
+
return;
|
|
3835
|
+
const record = { ...replica.record, name: room.name, handle: room.handle };
|
|
3836
|
+
await local.commit({
|
|
3837
|
+
rooms: [
|
|
3838
|
+
{
|
|
3839
|
+
locator: room.locator,
|
|
3840
|
+
key: record.key,
|
|
3841
|
+
record: storedRecord(record),
|
|
3842
|
+
composed: composedOf(composed(record)),
|
|
3843
|
+
},
|
|
3844
|
+
],
|
|
3845
|
+
});
|
|
3846
|
+
replicas.set(room.locator, { ...replica, record });
|
|
3847
|
+
});
|
|
3848
|
+
}
|
|
3849
|
+
async function retainModel(local, room, next) {
|
|
3850
|
+
const key = room.locator;
|
|
3851
|
+
const previous = modelOf(room.locator);
|
|
3852
|
+
if (JSON.stringify(previous) === JSON.stringify(next) && declaredAsHeld(room))
|
|
3583
3853
|
return;
|
|
3584
3854
|
if (next.status === 'unavailable') {
|
|
3585
3855
|
invalidModels.add(key);
|
|
@@ -3591,26 +3861,15 @@ export function composeRuntime(options) {
|
|
|
3591
3861
|
const held = replicas.get(room.locator);
|
|
3592
3862
|
let changed = null;
|
|
3593
3863
|
if (held !== undefined) {
|
|
3594
|
-
const
|
|
3864
|
+
const record = {
|
|
3595
3865
|
...held.record,
|
|
3866
|
+
name: room.name,
|
|
3867
|
+
handle: room.handle,
|
|
3596
3868
|
operations: room.operations,
|
|
3597
3869
|
replica: room.replica,
|
|
3598
3870
|
typedSelection: room.typedSelection ?? null,
|
|
3599
|
-
|
|
3871
|
+
model: next,
|
|
3600
3872
|
};
|
|
3601
|
-
let read = 'staged';
|
|
3602
|
-
if (!sameMembership(declared, held.record)) {
|
|
3603
|
-
const verdict = await validatedCollection(walkOf(local, held.record.key, 'confirmed'), lookupOf(local, held.record.key, 'confirmed'), compositionOf(declared));
|
|
3604
|
-
read =
|
|
3605
|
-
verdict.verdict !== 'refuse'
|
|
3606
|
-
? 'staged'
|
|
3607
|
-
: verdict.rule === 'model-unavailable'
|
|
3608
|
-
? 'deferred'
|
|
3609
|
-
: { kind: 'refused', refusal: verdict };
|
|
3610
|
-
}
|
|
3611
|
-
const record = read === 'staged'
|
|
3612
|
-
? declared
|
|
3613
|
-
: { ...declared, typedSelection: held.record.typedSelection };
|
|
3614
3873
|
const confirmedLayer = await sliceAt(record, 'confirmed', held.queue, held.overlaid);
|
|
3615
3874
|
const replayedLayer = await replayed(record, confirmedLayer, held.queue);
|
|
3616
3875
|
const diff = await overlayDiff(record, held, replayedLayer.visible, replayedLayer.touched);
|
|
@@ -3621,41 +3880,33 @@ export function composeRuntime(options) {
|
|
|
3621
3880
|
key: record.key,
|
|
3622
3881
|
record: storedRecord(record),
|
|
3623
3882
|
roots: record.typedSelection?.roots ?? [],
|
|
3624
|
-
|
|
3883
|
+
composed: composedOf(composed(record)),
|
|
3625
3884
|
overlay: diff.overlay,
|
|
3626
3885
|
},
|
|
3627
3886
|
],
|
|
3628
3887
|
});
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
replicas.set(room.locator, { ...held, record, overlaid: diff.touched, count });
|
|
3888
|
+
const { count, indexed } = committedRoom(room.locator, answer);
|
|
3889
|
+
replicas.set(room.locator, { ...held, record, overlaid: diff.touched, count, indexed });
|
|
3632
3890
|
changed = changeSet(room.locator, {
|
|
3633
3891
|
documents: answer.changed[room.locator] ?? [],
|
|
3634
3892
|
room: true,
|
|
3635
3893
|
});
|
|
3636
|
-
if (read === 'staged' || read === 'deferred') {
|
|
3637
|
-
if (declarationRefusals.delete(room.locator)) {
|
|
3638
|
-
sourceRefusals.delete(room.locator);
|
|
3639
|
-
unreadable.delete(room.locator);
|
|
3640
|
-
}
|
|
3641
|
-
}
|
|
3642
|
-
else {
|
|
3643
|
-
refusedSource(room, read);
|
|
3644
|
-
declarationRefusals.add(room.locator);
|
|
3645
|
-
}
|
|
3646
3894
|
}
|
|
3647
|
-
|
|
3648
|
-
if (next.status === 'current') {
|
|
3649
|
-
liveModels.set(key, JSON.stringify(next.snapshot));
|
|
3650
|
-
invalidModels.delete(key);
|
|
3651
|
-
}
|
|
3652
|
-
list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
3895
|
+
heldModel(room, next);
|
|
3653
3896
|
if (changed === null)
|
|
3654
3897
|
emitRooms();
|
|
3655
3898
|
else
|
|
3656
3899
|
publishChangeSet(changed);
|
|
3657
3900
|
});
|
|
3658
3901
|
}
|
|
3902
|
+
function heldModel(room, next) {
|
|
3903
|
+
models.set(room.locator, next);
|
|
3904
|
+
if (next.status === 'current') {
|
|
3905
|
+
liveModels.set(room.locator, JSON.stringify(next.snapshot));
|
|
3906
|
+
invalidModels.delete(room.locator);
|
|
3907
|
+
}
|
|
3908
|
+
list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
3909
|
+
}
|
|
3659
3910
|
async function scoped(result, room, local) {
|
|
3660
3911
|
if (result.scope === 'content' && !result.retryable) {
|
|
3661
3912
|
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
@@ -3669,14 +3920,14 @@ export function composeRuntime(options) {
|
|
|
3669
3920
|
});
|
|
3670
3921
|
let scopedAway = room;
|
|
3671
3922
|
if (result.scope === 'model')
|
|
3672
|
-
await retainModel(local, room,
|
|
3923
|
+
await retainModel(local, room, {
|
|
3673
3924
|
status: 'unavailable',
|
|
3674
3925
|
reason: 'not-readable',
|
|
3675
3926
|
});
|
|
3676
3927
|
else if (result.scope !== 'operation') {
|
|
3677
3928
|
unreadable.add(room.locator);
|
|
3678
3929
|
scopedAway = { ...room, replica: { ...room.replica, admission: 'unavailable' } };
|
|
3679
|
-
await retainModel(local, scopedAway,
|
|
3930
|
+
await retainModel(local, scopedAway, modelOf(room.locator));
|
|
3680
3931
|
}
|
|
3681
3932
|
if (!continuing())
|
|
3682
3933
|
return;
|
|
@@ -3684,6 +3935,17 @@ export function composeRuntime(options) {
|
|
|
3684
3935
|
emitRooms();
|
|
3685
3936
|
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
3686
3937
|
}
|
|
3938
|
+
function name(locator) {
|
|
3939
|
+
const stream = options.adapter.roomStream;
|
|
3940
|
+
const record = replicas.get(locator)?.record;
|
|
3941
|
+
const room = discovered.get(locator);
|
|
3942
|
+
if (stream === undefined || record === undefined || room === undefined)
|
|
3943
|
+
return;
|
|
3944
|
+
if (!socketOpen || !handed || record.replica.selection !== room.replica.selection)
|
|
3945
|
+
return;
|
|
3946
|
+
naming += 1;
|
|
3947
|
+
stream.hold([{ locator, roomVersion: record.roomVersion }]);
|
|
3948
|
+
}
|
|
3687
3949
|
function heldRooms(listed) {
|
|
3688
3950
|
return listed.flatMap((room) => {
|
|
3689
3951
|
const record = replicas.get(room.locator)?.record;
|
|
@@ -3701,14 +3963,19 @@ export function composeRuntime(options) {
|
|
|
3701
3963
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
3702
3964
|
});
|
|
3703
3965
|
if (attempt === 'opened') {
|
|
3966
|
+
naming += 1;
|
|
3704
3967
|
options.adapter.roomStream?.hold(heldRooms(ready));
|
|
3705
3968
|
handed = true;
|
|
3706
3969
|
}
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3970
|
+
for (const room of ready) {
|
|
3971
|
+
if (attempt !== 'unstreamed' && replicas.has(room.locator)) {
|
|
3972
|
+
if (attempt === 'authorized')
|
|
3973
|
+
name(room.locator);
|
|
3974
|
+
continue;
|
|
3975
|
+
}
|
|
3976
|
+
void roomTask(room.locator, (live) => loadRoom(room, local, progress, live)).then(() => rest(progress));
|
|
3977
|
+
}
|
|
3978
|
+
const failed = prepared.find((room) => room.status === 'rejected');
|
|
3712
3979
|
if (failed !== undefined)
|
|
3713
3980
|
throw failed.reason;
|
|
3714
3981
|
}
|
|
@@ -3726,27 +3993,31 @@ export function composeRuntime(options) {
|
|
|
3726
3993
|
return false;
|
|
3727
3994
|
}
|
|
3728
3995
|
if (prior !== undefined)
|
|
3729
|
-
await retainModel(local, room,
|
|
3996
|
+
await retainModel(local, room, modelOf(room.locator));
|
|
3997
|
+
if (prior !== undefined)
|
|
3998
|
+
await keepNaming(local, room);
|
|
3730
3999
|
if (discoveryPaused || authorization === undefined)
|
|
3731
4000
|
return false;
|
|
3732
4001
|
return room.replica.admission === 'admitted';
|
|
3733
4002
|
}
|
|
3734
4003
|
async function caughtUp(room, local, progress, live) {
|
|
3735
|
-
if (
|
|
4004
|
+
if (options.adapter.roomStream !== undefined || authorization === undefined)
|
|
4005
|
+
return;
|
|
4006
|
+
if (!live() || !behind(room))
|
|
3736
4007
|
return;
|
|
3737
4008
|
const withheld = await advance(room, local, authorization, live);
|
|
3738
4009
|
if (withheld !== null)
|
|
3739
4010
|
await withhold(withheld, room, local, progress);
|
|
3740
4011
|
}
|
|
3741
4012
|
function revoked(locator) {
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
3745
|
-
});
|
|
4013
|
+
const model = modelOf(locator);
|
|
4014
|
+
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
3746
4015
|
}
|
|
3747
4016
|
async function loadRoom(room, local, progress, live) {
|
|
3748
4017
|
if (!live() || authorization === undefined)
|
|
3749
4018
|
return;
|
|
4019
|
+
if (replicas.has(room.locator) && !(await prepareRoom(room, local, progress)))
|
|
4020
|
+
return;
|
|
3750
4021
|
if (replicas.has(room.locator) && revoked(room.locator)) {
|
|
3751
4022
|
await reload(room, local, progress, live);
|
|
3752
4023
|
return;
|
|
@@ -3764,21 +4035,53 @@ export function composeRuntime(options) {
|
|
|
3764
4035
|
reloadsDue.delete(room.locator);
|
|
3765
4036
|
if (!live() || authorization === undefined)
|
|
3766
4037
|
return;
|
|
4038
|
+
const from = standingOf(room.locator);
|
|
3767
4039
|
const bootstrapped = await pullBootstrap(authorization, room, local, live);
|
|
3768
|
-
if (!live())
|
|
3769
|
-
return;
|
|
3770
4040
|
if (bootstrapped.kind !== 'staged') {
|
|
3771
|
-
|
|
4041
|
+
if (live())
|
|
4042
|
+
await withhold(bootstrapped, room, local, progress);
|
|
3772
4043
|
return;
|
|
3773
4044
|
}
|
|
3774
|
-
await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
|
|
4045
|
+
await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from }, live);
|
|
3775
4046
|
await caughtUp(room, local, progress, live);
|
|
3776
4047
|
}
|
|
3777
4048
|
finally {
|
|
3778
4049
|
caughtUpTo(room.locator);
|
|
3779
4050
|
}
|
|
3780
4051
|
}
|
|
3781
|
-
async function
|
|
4052
|
+
async function asked(authorized) {
|
|
4053
|
+
try {
|
|
4054
|
+
return (await options.adapter.discover?.(authorized)) ?? { kind: 'transport-uncertainty' };
|
|
4055
|
+
}
|
|
4056
|
+
catch {
|
|
4057
|
+
return { kind: 'transport-uncertainty' };
|
|
4058
|
+
}
|
|
4059
|
+
}
|
|
4060
|
+
function malformed(listed) {
|
|
4061
|
+
for (const room of listed) {
|
|
4062
|
+
if (room.operations === undefined ||
|
|
4063
|
+
!['write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
|
|
4064
|
+
room.operations[operation].every((scope) => typeof scope === 'string')) ||
|
|
4065
|
+
room.replica === undefined ||
|
|
4066
|
+
typeof room.replica.selection !== 'string' ||
|
|
4067
|
+
room.replica.selection.length === 0 ||
|
|
4068
|
+
!['admitted', 'unavailable'].includes(room.replica.admission) ||
|
|
4069
|
+
!['locked', 'open'].includes(room.replica.offline)) {
|
|
4070
|
+
return `invalid model or replica declaration ${room.locator}`;
|
|
4071
|
+
}
|
|
4072
|
+
if (!declaredSelection(room))
|
|
4073
|
+
return `invalid typed selection declaration ${room.locator}`;
|
|
4074
|
+
}
|
|
4075
|
+
return null;
|
|
4076
|
+
}
|
|
4077
|
+
async function signIn(local, signedIn) {
|
|
4078
|
+
if (JSON.stringify(signedIn) === JSON.stringify(member))
|
|
4079
|
+
return;
|
|
4080
|
+
await local.fact('member', signedIn);
|
|
4081
|
+
member = signedIn;
|
|
4082
|
+
emitMember();
|
|
4083
|
+
}
|
|
4084
|
+
async function discoverRooms(local, progress, attempt, live, found) {
|
|
3782
4085
|
if (closed || authorization === undefined) {
|
|
3783
4086
|
notifyReady();
|
|
3784
4087
|
return;
|
|
@@ -3786,13 +4089,9 @@ export function composeRuntime(options) {
|
|
|
3786
4089
|
moveTo(transferring(status));
|
|
3787
4090
|
discoveries += 1;
|
|
3788
4091
|
const started = discoveries;
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
}
|
|
3793
|
-
catch {
|
|
3794
|
-
result = { kind: 'transport-uncertainty' };
|
|
3795
|
-
}
|
|
4092
|
+
const result = found ?? (await asked(authorization));
|
|
4093
|
+
if (result.kind === 'discovery')
|
|
4094
|
+
roomsNamed = true;
|
|
3796
4095
|
if (!live()) {
|
|
3797
4096
|
progress.complete = false;
|
|
3798
4097
|
notifyReady();
|
|
@@ -3800,12 +4099,7 @@ export function composeRuntime(options) {
|
|
|
3800
4099
|
}
|
|
3801
4100
|
journal.record(`connection discover ${result.kind}`);
|
|
3802
4101
|
if (result.kind === 'transport-uncertainty') {
|
|
3803
|
-
|
|
3804
|
-
await retainModel(local, room, 'collection', cached(modelOf(room.locator)));
|
|
3805
|
-
if (room.typedSelection !== null) {
|
|
3806
|
-
await retainModel(local, room, 'typed-selection', cached(modelOf(room.locator, 'typed-selection')));
|
|
3807
|
-
}
|
|
3808
|
-
}
|
|
4102
|
+
await cachedModels(local);
|
|
3809
4103
|
progress.reachable = false;
|
|
3810
4104
|
progress.complete = false;
|
|
3811
4105
|
notifyReady();
|
|
@@ -3826,26 +4120,12 @@ export function composeRuntime(options) {
|
|
|
3826
4120
|
notifyReady();
|
|
3827
4121
|
return;
|
|
3828
4122
|
}
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
room.replica.selection.length === 0 ||
|
|
3836
|
-
!['admitted', 'unavailable'].includes(room.replica.admission) ||
|
|
3837
|
-
!['locked', 'open'].includes(room.replica.offline)) {
|
|
3838
|
-
journal.record(`invalid model or replica declaration ${room.locator}`);
|
|
3839
|
-
await refuse('missing-required-guarantee');
|
|
3840
|
-
notifyReady();
|
|
3841
|
-
return;
|
|
3842
|
-
}
|
|
3843
|
-
if (!declaredSelection(room)) {
|
|
3844
|
-
journal.record(`invalid typed selection declaration ${room.locator}`);
|
|
3845
|
-
await refuse('missing-required-guarantee');
|
|
3846
|
-
notifyReady();
|
|
3847
|
-
return;
|
|
3848
|
-
}
|
|
4123
|
+
const invalid = malformed(result.rooms);
|
|
4124
|
+
if (invalid !== null) {
|
|
4125
|
+
journal.record(invalid);
|
|
4126
|
+
await refuse('missing-required-guarantee');
|
|
4127
|
+
notifyReady();
|
|
4128
|
+
return;
|
|
3849
4129
|
}
|
|
3850
4130
|
if (refusal !== null) {
|
|
3851
4131
|
refusal = null;
|
|
@@ -3853,7 +4133,6 @@ export function composeRuntime(options) {
|
|
|
3853
4133
|
}
|
|
3854
4134
|
const previousCapabilities = capabilities;
|
|
3855
4135
|
capabilities = result.capabilities;
|
|
3856
|
-
outcomeRetention = result.retention.outcome;
|
|
3857
4136
|
notifyReady();
|
|
3858
4137
|
if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
|
|
3859
4138
|
JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
|
|
@@ -3861,13 +4140,8 @@ export function composeRuntime(options) {
|
|
|
3861
4140
|
await local.fact('providerLimits', limits);
|
|
3862
4141
|
await local.fact('capabilities', capabilities);
|
|
3863
4142
|
}
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
await local.fact('member', signedIn);
|
|
3867
|
-
member = signedIn;
|
|
3868
|
-
emitMember();
|
|
3869
|
-
}
|
|
3870
|
-
await synchronize(await applyDiscovery(result.rooms, started, result.covered), local, progress, attempt, live);
|
|
4143
|
+
await signIn(local, result.member ?? null);
|
|
4144
|
+
await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
|
|
3871
4145
|
}
|
|
3872
4146
|
async function ensureAuthorization() {
|
|
3873
4147
|
if (authorization !== undefined)
|
|
@@ -3882,23 +4156,45 @@ export function composeRuntime(options) {
|
|
|
3882
4156
|
}
|
|
3883
4157
|
}
|
|
3884
4158
|
async function admittedRead(work) {
|
|
3885
|
-
|
|
3886
|
-
|
|
4159
|
+
while (roomReads.free === 0) {
|
|
4160
|
+
await new Promise((admit) => roomReads.waiting.push(admit));
|
|
3887
4161
|
}
|
|
3888
|
-
|
|
3889
|
-
while (reads.free === 0)
|
|
3890
|
-
await new Promise((admit) => reads.waiting.push(admit));
|
|
3891
|
-
reads.free -= 1;
|
|
4162
|
+
roomReads.free -= 1;
|
|
3892
4163
|
try {
|
|
3893
4164
|
await work();
|
|
3894
4165
|
}
|
|
3895
4166
|
finally {
|
|
3896
|
-
|
|
3897
|
-
|
|
4167
|
+
roomReads.free += 1;
|
|
4168
|
+
roomReads.waiting.shift()?.();
|
|
3898
4169
|
}
|
|
3899
4170
|
}
|
|
4171
|
+
function inRoom(locator, work) {
|
|
4172
|
+
roomWork += 1;
|
|
4173
|
+
const ran = (roomLanes.get(locator) ?? Promise.resolve()).then(async () => {
|
|
4174
|
+
try {
|
|
4175
|
+
return await work(continuing);
|
|
4176
|
+
}
|
|
4177
|
+
finally {
|
|
4178
|
+
roomWork -= 1;
|
|
4179
|
+
}
|
|
4180
|
+
});
|
|
4181
|
+
const chain = ran.then(() => undefined, () => undefined);
|
|
4182
|
+
roomLanes.set(locator, chain);
|
|
4183
|
+
working.add(chain);
|
|
4184
|
+
void chain.finally(() => {
|
|
4185
|
+
working.delete(chain);
|
|
4186
|
+
if (roomLanes.get(locator) === chain)
|
|
4187
|
+
roomLanes.delete(locator);
|
|
4188
|
+
});
|
|
4189
|
+
return ran;
|
|
4190
|
+
}
|
|
3900
4191
|
function roomTask(locator, work) {
|
|
3901
|
-
|
|
4192
|
+
if (stopping)
|
|
4193
|
+
return Promise.resolve();
|
|
4194
|
+
return inRoom(locator, (live) => admittedRead(() => work(live))).catch(() => undefined);
|
|
4195
|
+
}
|
|
4196
|
+
function advanced(room, local, authorized) {
|
|
4197
|
+
return inRoom(room.locator, (live) => advance(room, local, authorized, live));
|
|
3902
4198
|
}
|
|
3903
4199
|
function task(lane, work) {
|
|
3904
4200
|
if (stopping)
|
|
@@ -3914,23 +4210,19 @@ export function composeRuntime(options) {
|
|
|
3914
4210
|
void run.finally(() => working.delete(run));
|
|
3915
4211
|
return run;
|
|
3916
4212
|
}
|
|
3917
|
-
function sync(attempt) {
|
|
4213
|
+
function sync(attempt, found) {
|
|
3918
4214
|
return task(OPENING_LANE, async (live) => {
|
|
3919
4215
|
if (!live()) {
|
|
3920
|
-
|
|
4216
|
+
readyFromStore();
|
|
3921
4217
|
return;
|
|
3922
4218
|
}
|
|
3923
4219
|
const progress = { complete: true, reachable: true };
|
|
3924
|
-
|
|
3925
|
-
if (attempt === 'opened')
|
|
3926
|
-
opened = generation;
|
|
3927
|
-
underway = attempt === 'opened' ? { generation, progress } : null;
|
|
4220
|
+
underway = attempt === 'opened' ? progress : null;
|
|
3928
4221
|
if (attempt === 'opened')
|
|
3929
4222
|
streamed = progress;
|
|
3930
4223
|
complete = false;
|
|
3931
4224
|
recorded = false;
|
|
3932
4225
|
moveTo(transferring(status));
|
|
3933
|
-
handed = false;
|
|
3934
4226
|
try {
|
|
3935
4227
|
const local = await openLocal();
|
|
3936
4228
|
if (!live())
|
|
@@ -3938,7 +4230,7 @@ export function composeRuntime(options) {
|
|
|
3938
4230
|
await ensureAuthorization();
|
|
3939
4231
|
if (!live())
|
|
3940
4232
|
return;
|
|
3941
|
-
await discoverRooms(local, progress, attempt, live);
|
|
4233
|
+
await discoverRooms(local, progress, attempt, live, found);
|
|
3942
4234
|
}
|
|
3943
4235
|
catch {
|
|
3944
4236
|
progress.complete = false;
|
|
@@ -3946,7 +4238,6 @@ export function composeRuntime(options) {
|
|
|
3946
4238
|
progress.reachable = false;
|
|
3947
4239
|
}
|
|
3948
4240
|
finally {
|
|
3949
|
-
gate.release();
|
|
3950
4241
|
notifyReady();
|
|
3951
4242
|
}
|
|
3952
4243
|
if (!live())
|
|
@@ -3958,35 +4249,59 @@ export function composeRuntime(options) {
|
|
|
3958
4249
|
if (handed || closed || progress.reachable)
|
|
3959
4250
|
return;
|
|
3960
4251
|
underway = null;
|
|
3961
|
-
opened = null;
|
|
3962
|
-
journal.record('connection reopen');
|
|
3963
4252
|
await rest(progress, live);
|
|
3964
|
-
options.adapter.roomStream?.reopen();
|
|
3965
4253
|
});
|
|
3966
4254
|
}
|
|
3967
|
-
function
|
|
4255
|
+
function resynchronize() {
|
|
3968
4256
|
return task(OPENING_LANE, async (live) => {
|
|
3969
|
-
if (!live() || !
|
|
4257
|
+
if (!live() || !announced)
|
|
3970
4258
|
return;
|
|
3971
4259
|
const progress = { complete: true, reachable: true };
|
|
3972
|
-
const gate = discover();
|
|
3973
4260
|
moveTo(transferring(status));
|
|
3974
4261
|
try {
|
|
3975
4262
|
const local = await openLocal();
|
|
3976
4263
|
if (!live())
|
|
3977
4264
|
return;
|
|
3978
|
-
await
|
|
4265
|
+
await synchronize([...discovered.values()], local, progress, 'unstreamed', live);
|
|
4266
|
+
}
|
|
4267
|
+
catch {
|
|
4268
|
+
progress.complete = false;
|
|
4269
|
+
}
|
|
4270
|
+
if (live())
|
|
4271
|
+
await finishOpening(progress, live);
|
|
4272
|
+
});
|
|
4273
|
+
}
|
|
4274
|
+
function countAuthorityChange() {
|
|
4275
|
+
authorityChanges += 1;
|
|
4276
|
+
for (const listener of authorityChangeListeners)
|
|
4277
|
+
listener(authorityChanges);
|
|
4278
|
+
}
|
|
4279
|
+
function changeAuthority(change) {
|
|
4280
|
+
return task(OPENING_LANE, async (live) => {
|
|
4281
|
+
if (!live() || !announced)
|
|
4282
|
+
return;
|
|
4283
|
+
countAuthorityChange();
|
|
4284
|
+
const progress = { complete: true, reachable: true };
|
|
4285
|
+
moveTo(transferring(status));
|
|
4286
|
+
try {
|
|
4287
|
+
const local = await openLocal();
|
|
3979
4288
|
if (!live())
|
|
3980
4289
|
return;
|
|
3981
|
-
|
|
4290
|
+
const invalid = malformed(change.rooms);
|
|
4291
|
+
if (invalid !== null) {
|
|
4292
|
+
journal.record(invalid);
|
|
4293
|
+
await refuse('missing-required-guarantee');
|
|
4294
|
+
return;
|
|
4295
|
+
}
|
|
4296
|
+
if (change.member !== undefined)
|
|
4297
|
+
await signIn(local, change.member);
|
|
4298
|
+
discoveries += 1;
|
|
4299
|
+
const listed = await applyDiscovery(change.rooms, discoveries, change.left);
|
|
4300
|
+
await synchronize(listed, local, progress, 'authorized', live);
|
|
3982
4301
|
}
|
|
3983
4302
|
catch {
|
|
3984
4303
|
progress.complete = false;
|
|
3985
4304
|
}
|
|
3986
|
-
finally {
|
|
3987
|
-
gate.release();
|
|
3988
|
-
notifyReady();
|
|
3989
|
-
}
|
|
3990
4305
|
if (!live())
|
|
3991
4306
|
return;
|
|
3992
4307
|
if (!progress.complete)
|
|
@@ -3994,14 +4309,17 @@ export function composeRuntime(options) {
|
|
|
3994
4309
|
await rest(progress, live);
|
|
3995
4310
|
});
|
|
3996
4311
|
}
|
|
3997
|
-
function closedSocket(revoked, lost) {
|
|
3998
|
-
|
|
4312
|
+
function closedSocket(revoked, lost, authorityChanged) {
|
|
4313
|
+
notifyReady();
|
|
4314
|
+
const opened = announced;
|
|
3999
4315
|
socketOpen = false;
|
|
4316
|
+
announced = false;
|
|
4317
|
+
missedChange ||= authorityChanged;
|
|
4318
|
+
handed = false;
|
|
4319
|
+
naming = 0;
|
|
4000
4320
|
connectionLost();
|
|
4001
|
-
if (lost)
|
|
4002
|
-
reads.lost();
|
|
4321
|
+
if (lost)
|
|
4003
4322
|
conversations.lost();
|
|
4004
|
-
}
|
|
4005
4323
|
if (revoked) {
|
|
4006
4324
|
void refuse('revoked-authorization');
|
|
4007
4325
|
return;
|
|
@@ -4012,18 +4330,32 @@ export function composeRuntime(options) {
|
|
|
4012
4330
|
moveTo({
|
|
4013
4331
|
...status,
|
|
4014
4332
|
state: status.state === 'synced' ? 'syncing' : status.state,
|
|
4015
|
-
providerUnreachable: status.providerUnreachable || !opened,
|
|
4333
|
+
providerUnreachable: status.providerUnreachable || (!opened && lost),
|
|
4334
|
+
});
|
|
4335
|
+
void closing(opened);
|
|
4336
|
+
}
|
|
4337
|
+
function closing(opened) {
|
|
4338
|
+
return task(OPENING_LANE, async (live) => {
|
|
4339
|
+
if (!live())
|
|
4340
|
+
return;
|
|
4341
|
+
const local = await openLocal();
|
|
4342
|
+
if (!opened && !socketOpen)
|
|
4343
|
+
await cachedModels(local);
|
|
4344
|
+
await rest(null, live);
|
|
4016
4345
|
});
|
|
4017
4346
|
}
|
|
4018
|
-
function
|
|
4019
|
-
|
|
4347
|
+
function cachedModels(local) {
|
|
4348
|
+
return settleModels([...rooms.values()].map((room) => discovered.get(room.locator) ?? room), 'cached', local);
|
|
4349
|
+
}
|
|
4350
|
+
function caughtUpRooms(expired) {
|
|
4351
|
+
const progress = underway;
|
|
4352
|
+
underway = null;
|
|
4353
|
+
naming = Math.max(0, naming - 1);
|
|
4020
4354
|
const cycle = progress ?? { complete: true, reachable: true };
|
|
4021
|
-
|
|
4022
|
-
opened = null;
|
|
4355
|
+
replaced(expired);
|
|
4023
4356
|
const reloading = progress === null ? [] : owedReloads(expired);
|
|
4024
4357
|
const replacing = reloadNow((room) => expired.includes(room.locator) || reloading.includes(room.locator), cycle);
|
|
4025
4358
|
return task(OPENING_LANE, async (live) => {
|
|
4026
|
-
underway = null;
|
|
4027
4359
|
if (!live())
|
|
4028
4360
|
return;
|
|
4029
4361
|
if (progress === null) {
|
|
@@ -4031,24 +4363,14 @@ export function composeRuntime(options) {
|
|
|
4031
4363
|
await rest(cycle, live);
|
|
4032
4364
|
return;
|
|
4033
4365
|
}
|
|
4034
|
-
await roomsCarried([...expired, ...reloading]);
|
|
4035
|
-
if (!live())
|
|
4036
|
-
return;
|
|
4037
4366
|
try {
|
|
4038
4367
|
const local = await openLocal();
|
|
4039
|
-
|
|
4368
|
+
await replacing;
|
|
4040
4369
|
if (progress.reachable) {
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
!reloading.includes(room.locator)) {
|
|
4045
|
-
await confirmModels(room, local);
|
|
4046
|
-
}
|
|
4047
|
-
}
|
|
4370
|
+
await settleModels([...discovered.values()].filter((room) => replicas.has(room.locator) &&
|
|
4371
|
+
!expired.includes(room.locator) &&
|
|
4372
|
+
!reloading.includes(room.locator)), 'current', local);
|
|
4048
4373
|
}
|
|
4049
|
-
const failed = reloaded.find((outcome) => outcome.status === 'rejected');
|
|
4050
|
-
if (failed !== undefined)
|
|
4051
|
-
throw failed.reason;
|
|
4052
4374
|
}
|
|
4053
4375
|
catch {
|
|
4054
4376
|
progress.complete = false;
|
|
@@ -4060,11 +4382,8 @@ export function composeRuntime(options) {
|
|
|
4060
4382
|
}
|
|
4061
4383
|
function owedReloads(expired) {
|
|
4062
4384
|
const reloading = [...discovered.values()]
|
|
4063
|
-
.filter((room) => replicas.has(room.locator) &&
|
|
4064
|
-
!expired.includes(room.locator) &&
|
|
4065
|
-
(owed.has(room.locator) || revoked(room.locator)))
|
|
4385
|
+
.filter((room) => replicas.has(room.locator) && !expired.includes(room.locator) && revoked(room.locator))
|
|
4066
4386
|
.map((room) => room.locator);
|
|
4067
|
-
owed.clear();
|
|
4068
4387
|
for (const locator of reloading)
|
|
4069
4388
|
catching.add(locator);
|
|
4070
4389
|
liveQueries.recover();
|
|
@@ -4073,24 +4392,43 @@ export function composeRuntime(options) {
|
|
|
4073
4392
|
async function reloadNow(replaced, progress) {
|
|
4074
4393
|
const reloaded = [...discovered.values()].filter((room) => replicas.has(room.locator) && replaced(room));
|
|
4075
4394
|
for (const room of reloaded)
|
|
4076
|
-
reloadsDue.
|
|
4395
|
+
reloadsDue.add(room.locator);
|
|
4077
4396
|
try {
|
|
4078
4397
|
const local = await openLocal();
|
|
4079
|
-
|
|
4398
|
+
for (const room of reloaded) {
|
|
4399
|
+
void roomTask(room.locator, (live) => reload(room, local, progress, live)).then(() => rest(progress));
|
|
4400
|
+
}
|
|
4080
4401
|
}
|
|
4081
4402
|
catch {
|
|
4082
4403
|
for (const room of reloaded)
|
|
4083
4404
|
reloadsDue.delete(room.locator);
|
|
4084
4405
|
progress.reachable = false;
|
|
4085
|
-
return [];
|
|
4086
4406
|
}
|
|
4087
4407
|
}
|
|
4088
|
-
async function
|
|
4089
|
-
|
|
4090
|
-
const model = modelOf(room.locator
|
|
4091
|
-
|
|
4092
|
-
|
|
4408
|
+
async function settleModels(held, status, local) {
|
|
4409
|
+
const owed = (room) => {
|
|
4410
|
+
const model = modelOf(room.locator);
|
|
4411
|
+
return model.status !== 'unavailable' && model.status !== status;
|
|
4412
|
+
};
|
|
4413
|
+
const settled = await exclusive(async () => {
|
|
4414
|
+
if (!continuing())
|
|
4415
|
+
return [];
|
|
4416
|
+
const unchanged = held.filter((room) => owed(room) && declaredAsHeld(room));
|
|
4417
|
+
for (const room of unchanged) {
|
|
4418
|
+
const next = { ...modelOf(room.locator), status };
|
|
4419
|
+
const replica = replicas.get(room.locator);
|
|
4420
|
+
if (replica !== undefined)
|
|
4421
|
+
replicas.set(room.locator, { ...replica, record: { ...replica.record, model: next } });
|
|
4422
|
+
heldModel(room, next);
|
|
4093
4423
|
}
|
|
4424
|
+
if (unchanged.length > 0)
|
|
4425
|
+
emitRooms();
|
|
4426
|
+
return unchanged;
|
|
4427
|
+
});
|
|
4428
|
+
for (const room of held) {
|
|
4429
|
+
if (settled.includes(room) || !owed(room))
|
|
4430
|
+
continue;
|
|
4431
|
+
await retainModel(local, room, { ...modelOf(room.locator), status });
|
|
4094
4432
|
}
|
|
4095
4433
|
}
|
|
4096
4434
|
function submitLater(locator) {
|
|
@@ -4135,14 +4473,10 @@ export function composeRuntime(options) {
|
|
|
4135
4473
|
try {
|
|
4136
4474
|
const local = await openLocal();
|
|
4137
4475
|
await roomTask(locator, async (live) => {
|
|
4138
|
-
|
|
4139
|
-
if (!current() || authorization === undefined)
|
|
4476
|
+
if (!live() || authorization === undefined)
|
|
4140
4477
|
return;
|
|
4141
|
-
const withheld = await
|
|
4142
|
-
|
|
4143
|
-
abandonedOn(signal),
|
|
4144
|
-
]);
|
|
4145
|
-
if (withheld !== null && current())
|
|
4478
|
+
const withheld = await advance(room, local, authorization, live);
|
|
4479
|
+
if (withheld !== null && live())
|
|
4146
4480
|
await withhold(withheld, room, local, progress);
|
|
4147
4481
|
});
|
|
4148
4482
|
}
|
|
@@ -4197,14 +4531,11 @@ export function composeRuntime(options) {
|
|
|
4197
4531
|
progress.complete = false;
|
|
4198
4532
|
return;
|
|
4199
4533
|
}
|
|
4200
|
-
reads.applied();
|
|
4201
4534
|
submitAll();
|
|
4202
4535
|
for (const locator of stale)
|
|
4203
4536
|
if (repairable.has(locator))
|
|
4204
4537
|
repairLater(locator, {});
|
|
4205
4538
|
await conversations.reconcile();
|
|
4206
|
-
if (status.providerUnreachable)
|
|
4207
|
-
conversations.reconnected();
|
|
4208
4539
|
const reading = { complete: true, reachable: true };
|
|
4209
4540
|
await conversations.catchUp(reading);
|
|
4210
4541
|
if (progress.reachable && reading.reachable)
|
|
@@ -4230,7 +4561,7 @@ export function composeRuntime(options) {
|
|
|
4230
4561
|
if (!live())
|
|
4231
4562
|
return;
|
|
4232
4563
|
let lastCompleted = status.lastCompleted;
|
|
4233
|
-
let synced = complete && stale.size === 0 && !pending();
|
|
4564
|
+
let synced = complete && roomWork === 0 && naming === 0 && stale.size === 0 && !pending();
|
|
4234
4565
|
if (synced && !recorded) {
|
|
4235
4566
|
const completedAt = new Date().toISOString();
|
|
4236
4567
|
try {
|
|
@@ -4245,74 +4576,78 @@ export function composeRuntime(options) {
|
|
|
4245
4576
|
}
|
|
4246
4577
|
}
|
|
4247
4578
|
moveTo({
|
|
4248
|
-
state: pending()
|
|
4579
|
+
state: pending()
|
|
4580
|
+
? 'pending'
|
|
4581
|
+
: synced && complete && roomWork === 0 && naming === 0
|
|
4582
|
+
? 'synced'
|
|
4583
|
+
: 'syncing',
|
|
4249
4584
|
lastCompleted,
|
|
4250
|
-
providerUnreachable: !progress.reachable,
|
|
4585
|
+
providerUnreachable: progress === null ? status.providerUnreachable : !progress.reachable,
|
|
4251
4586
|
...attention(),
|
|
4252
4587
|
});
|
|
4253
4588
|
}
|
|
4254
|
-
function
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
}
|
|
4262
|
-
function applyRoom(locator, roomVersion) {
|
|
4263
|
-
announced.set(locator, roomVersion);
|
|
4264
|
-
return task(roomLane(locator), async (live) => {
|
|
4265
|
-
if (discovering?.generation === generation)
|
|
4266
|
-
await discovering.done;
|
|
4267
|
-
const saving = answering.get(locator);
|
|
4268
|
-
if (saving?.generation === generation)
|
|
4269
|
-
await saving.answered;
|
|
4270
|
-
if (!live() || authorization === undefined)
|
|
4271
|
-
return;
|
|
4272
|
-
const room = discovered.get(locator);
|
|
4273
|
-
const held = replicas.get(locator);
|
|
4274
|
-
if (room === undefined || room.replica.admission !== 'admitted')
|
|
4275
|
-
return;
|
|
4276
|
-
if (held !== undefined &&
|
|
4277
|
-
(held.record.replica.selection !== room.replica.selection ||
|
|
4278
|
-
options.adapter.reached(locator, held.record.roomVersion, roomVersion))) {
|
|
4279
|
-
return;
|
|
4280
|
-
}
|
|
4281
|
-
const during = underway?.generation === generation ? underway.progress : null;
|
|
4282
|
-
const progress = during ?? { complete: true, reachable: true };
|
|
4283
|
-
if (during === null)
|
|
4284
|
-
moveTo(transferring(status));
|
|
4589
|
+
function applyRoom(change) {
|
|
4590
|
+
const { locator } = change;
|
|
4591
|
+
if (stopping)
|
|
4592
|
+
return Promise.resolve();
|
|
4593
|
+
return inRoom(locator, async (live) => {
|
|
4594
|
+
await answering.get(locator)?.answered;
|
|
4595
|
+
const progress = underway ?? { complete: true, reachable: true };
|
|
4285
4596
|
try {
|
|
4286
4597
|
const local = await openLocal();
|
|
4287
|
-
|
|
4288
|
-
if (!live()
|
|
4289
|
-
return;
|
|
4290
|
-
const
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4598
|
+
for (;;) {
|
|
4599
|
+
if (!live())
|
|
4600
|
+
return null;
|
|
4601
|
+
const room = discovered.get(locator);
|
|
4602
|
+
const held = replicas.get(locator);
|
|
4603
|
+
if (room === undefined ||
|
|
4604
|
+
held === undefined ||
|
|
4605
|
+
room.replica.admission !== 'admitted' ||
|
|
4606
|
+
held.record.replica.selection !== room.replica.selection) {
|
|
4607
|
+
return null;
|
|
4608
|
+
}
|
|
4609
|
+
const at = held.record.roomVersion;
|
|
4610
|
+
if (options.adapter.reached(locator, at, change.roomVersion))
|
|
4611
|
+
return progress;
|
|
4612
|
+
if (!options.adapter.reached(locator, at, change.from)) {
|
|
4613
|
+
name(locator);
|
|
4614
|
+
return progress;
|
|
4615
|
+
}
|
|
4616
|
+
if (underway === null)
|
|
4617
|
+
moveTo(transferring(status));
|
|
4618
|
+
const writing = () => live() && !full;
|
|
4619
|
+
let outlined = false;
|
|
4620
|
+
const refreshed = await ahead((hold) => pullRefresh(authorization ?? {}, held, writing, hold, async () => {
|
|
4621
|
+
const page = await change.read();
|
|
4622
|
+
const paths = Object.keys(page.outlines ?? {});
|
|
4623
|
+
outlined = paths.length > 0;
|
|
4624
|
+
await local.remember(locator, held.record.key, paths);
|
|
4625
|
+
return page;
|
|
4626
|
+
}));
|
|
4627
|
+
if (refreshed.kind !== 'advanced')
|
|
4628
|
+
return progress;
|
|
4629
|
+
const landing = {
|
|
4630
|
+
kind: 'refreshed',
|
|
4631
|
+
from: refreshed.from,
|
|
4632
|
+
remodelled: refreshed.remodelled,
|
|
4633
|
+
};
|
|
4634
|
+
if (await publish(local, room, refreshed.record, landing, writing)) {
|
|
4635
|
+
stale.delete(locator);
|
|
4636
|
+
await retainModel(local, room, modelOf(locator));
|
|
4637
|
+
if (outlined)
|
|
4638
|
+
void heldAgain(locator);
|
|
4639
|
+
return progress;
|
|
4640
|
+
}
|
|
4641
|
+
}
|
|
4294
4642
|
}
|
|
4295
4643
|
catch {
|
|
4644
|
+
if (!stale.has(locator))
|
|
4645
|
+
name(locator);
|
|
4296
4646
|
stale.add(locator);
|
|
4647
|
+
return progress;
|
|
4297
4648
|
}
|
|
4298
|
-
|
|
4299
|
-
if (during === null)
|
|
4300
|
-
await rest(progress, live);
|
|
4301
|
-
}
|
|
4302
|
-
});
|
|
4649
|
+
}).then((progress) => rest(progress), () => undefined);
|
|
4303
4650
|
}
|
|
4304
|
-
function roomsCarried(replacing) {
|
|
4305
|
-
const replaced = new Set(replacing.map(roomLane));
|
|
4306
|
-
return Promise.all([...lanes]
|
|
4307
|
-
.filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
|
|
4308
|
-
.map(([, held]) => held.chain));
|
|
4309
|
-
}
|
|
4310
|
-
const receipts = {
|
|
4311
|
-
heads: async () => (await openLocal()).receivedHeads(),
|
|
4312
|
-
parts: async (key, count) => (await openLocal()).receivedParts(key, count),
|
|
4313
|
-
keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
|
|
4314
|
-
drop: async (keys) => (await openLocal()).dropReceived(keys),
|
|
4315
|
-
};
|
|
4316
4651
|
function observeStream() {
|
|
4317
4652
|
const stream = options.adapter.roomStream;
|
|
4318
4653
|
if (stream === undefined || withdrawStream !== undefined)
|
|
@@ -4322,29 +4657,35 @@ export function composeRuntime(options) {
|
|
|
4322
4657
|
return;
|
|
4323
4658
|
if (event.kind === 'opened') {
|
|
4324
4659
|
socketOpen = true;
|
|
4660
|
+
announced = false;
|
|
4661
|
+
handed = false;
|
|
4662
|
+
naming = 0;
|
|
4325
4663
|
connectionLive();
|
|
4326
4664
|
generation += 1;
|
|
4327
|
-
reads.opened();
|
|
4328
4665
|
conversations.reopened();
|
|
4329
4666
|
recovery.resume();
|
|
4330
|
-
|
|
4667
|
+
}
|
|
4668
|
+
else if (event.kind === 'discovered') {
|
|
4669
|
+
if (!socketOpen)
|
|
4670
|
+
return;
|
|
4671
|
+
announced = true;
|
|
4672
|
+
if (missedChange) {
|
|
4673
|
+
missedChange = false;
|
|
4674
|
+
countAuthorityChange();
|
|
4675
|
+
}
|
|
4676
|
+
void sync('opened', event.discovery);
|
|
4677
|
+
for (const locator of replicas.keys())
|
|
4678
|
+
void heldAgain(locator);
|
|
4331
4679
|
}
|
|
4332
4680
|
else if (event.kind === 'closed')
|
|
4333
|
-
closedSocket(event.revoked, event.lost);
|
|
4334
|
-
else if (event.kind === '
|
|
4335
|
-
void
|
|
4681
|
+
closedSocket(event.revoked, event.lost, event.authorityChanged);
|
|
4682
|
+
else if (event.kind === 'caught-up')
|
|
4683
|
+
void caughtUpRooms(event.expired);
|
|
4336
4684
|
else if (event.kind === 'authority-changed')
|
|
4337
|
-
void
|
|
4685
|
+
void changeAuthority(event);
|
|
4338
4686
|
else
|
|
4339
|
-
void applyRoom(event
|
|
4340
|
-
}
|
|
4341
|
-
}
|
|
4342
|
-
async function heldRevisions(locator, paths) {
|
|
4343
|
-
const held = replicas.get(locator);
|
|
4344
|
-
if (held === undefined || paths.length === 0)
|
|
4345
|
-
return new Map();
|
|
4346
|
-
const entries = await (await openLocal()).entries(held.record.key, 'confirmed', paths);
|
|
4347
|
-
return new Map(entries.flatMap(({ path, revision }) => revision === null ? [] : [[path, revision]]));
|
|
4687
|
+
void applyRoom(event);
|
|
4688
|
+
});
|
|
4348
4689
|
}
|
|
4349
4690
|
function writesHeld() {
|
|
4350
4691
|
return serviceMode === 'write-halted' || haltedRefusal;
|
|
@@ -4414,8 +4755,9 @@ export function composeRuntime(options) {
|
|
|
4414
4755
|
if (closed)
|
|
4415
4756
|
return;
|
|
4416
4757
|
closed = true;
|
|
4417
|
-
|
|
4418
|
-
|
|
4758
|
+
notifyDiscovered();
|
|
4759
|
+
if (probing !== null)
|
|
4760
|
+
clearInterval(probing);
|
|
4419
4761
|
connectionLive();
|
|
4420
4762
|
lockListeners.clear();
|
|
4421
4763
|
readinessWatches.clear();
|
|
@@ -4427,11 +4769,13 @@ export function composeRuntime(options) {
|
|
|
4427
4769
|
conversations.close();
|
|
4428
4770
|
void recovery.retire();
|
|
4429
4771
|
roomListeners.clear();
|
|
4772
|
+
unlistedRooms.clear();
|
|
4430
4773
|
refusalListeners.clear();
|
|
4431
4774
|
storageLossListeners.clear();
|
|
4432
4775
|
statusListeners.clear();
|
|
4433
4776
|
serviceModeListeners.clear();
|
|
4434
4777
|
memberListeners.clear();
|
|
4778
|
+
authorityChangeListeners.clear();
|
|
4435
4779
|
liveQueries.close();
|
|
4436
4780
|
queueSubscriptions.clear();
|
|
4437
4781
|
roomDraftListeners.clear();
|
|
@@ -4439,6 +4783,7 @@ export function composeRuntime(options) {
|
|
|
4439
4783
|
}
|
|
4440
4784
|
async function stop() {
|
|
4441
4785
|
stopping = true;
|
|
4786
|
+
inFlight.abort();
|
|
4442
4787
|
await recovery.retire();
|
|
4443
4788
|
await Promise.all(working);
|
|
4444
4789
|
await mutations;
|
|
@@ -4452,20 +4797,20 @@ export function composeRuntime(options) {
|
|
|
4452
4797
|
observeStream();
|
|
4453
4798
|
void openLocal().then(() => {
|
|
4454
4799
|
if (capabilities.length > 0)
|
|
4455
|
-
|
|
4800
|
+
readyFromStore();
|
|
4456
4801
|
}, () => {
|
|
4457
|
-
|
|
4802
|
+
readyFromStore();
|
|
4458
4803
|
});
|
|
4459
4804
|
if (options.adapter.roomStream === undefined)
|
|
4460
|
-
void sync('unstreamed');
|
|
4805
|
+
void sync('unstreamed', null);
|
|
4461
4806
|
function planned(input, reach) {
|
|
4462
|
-
const plan =
|
|
4807
|
+
const plan = planWithin(input, roomFacts(reach), planReach(reach));
|
|
4463
4808
|
const named = plan.paths !== null &&
|
|
4464
4809
|
plan.rooms !== null &&
|
|
4465
4810
|
plan.paths.length * plan.rooms.length <= MAXIMUM_WINDOW;
|
|
4466
4811
|
return plan.limit === null && named ? plan : { ...plan, limit: capped(plan.limit) };
|
|
4467
4812
|
}
|
|
4468
|
-
function watchPlan(plan, deliver) {
|
|
4813
|
+
function watchPlan(plan, deliver, fail) {
|
|
4469
4814
|
let seeded = false;
|
|
4470
4815
|
let pending = true;
|
|
4471
4816
|
const shown = () => {
|
|
@@ -4491,7 +4836,7 @@ export function composeRuntime(options) {
|
|
|
4491
4836
|
}
|
|
4492
4837
|
seeded = false;
|
|
4493
4838
|
deliver(message);
|
|
4494
|
-
});
|
|
4839
|
+
}, fail);
|
|
4495
4840
|
return () => {
|
|
4496
4841
|
watchers.delete(shown);
|
|
4497
4842
|
if (watchers.size === 0)
|
|
@@ -4499,61 +4844,59 @@ export function composeRuntime(options) {
|
|
|
4499
4844
|
stop();
|
|
4500
4845
|
};
|
|
4501
4846
|
}
|
|
4502
|
-
async function searched(input, reach
|
|
4847
|
+
async function searched(input, reach) {
|
|
4503
4848
|
const asked = searchPlanOf(input);
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
rooms: answered.coverage.rooms,
|
|
4548
|
-
conversations: named,
|
|
4549
|
-
overlay: answered.coverage.overlay,
|
|
4849
|
+
await ready;
|
|
4850
|
+
const search = options.adapter.search;
|
|
4851
|
+
if (search === undefined) {
|
|
4852
|
+
return {
|
|
4853
|
+
kind: 'undeclared',
|
|
4854
|
+
code: 'action-undeclared',
|
|
4855
|
+
fields: { name: 'search' },
|
|
4856
|
+
retryable: false,
|
|
4857
|
+
};
|
|
4858
|
+
}
|
|
4859
|
+
if (closed || refusal !== null || connectionOf() !== 'connected')
|
|
4860
|
+
return OFFLINE;
|
|
4861
|
+
const reached = reach === null ? null : new Set(reach.rooms);
|
|
4862
|
+
const named = asked.rooms === null
|
|
4863
|
+
? reached === null
|
|
4864
|
+
? null
|
|
4865
|
+
: [...reached]
|
|
4866
|
+
: asked.rooms.filter((room) => reached === null || reached.has(room));
|
|
4867
|
+
const rooms = named === null ? null : named.filter((room) => replicas.has(room)).sort();
|
|
4868
|
+
if (rooms !== null && rooms.length === 0)
|
|
4869
|
+
return { kind: 'found', documents: [] };
|
|
4870
|
+
try {
|
|
4871
|
+
await ensureAuthorization();
|
|
4872
|
+
}
|
|
4873
|
+
catch {
|
|
4874
|
+
return UNCERTAIN;
|
|
4875
|
+
}
|
|
4876
|
+
if (closed || authorization === undefined || refusal !== null)
|
|
4877
|
+
return UNCERTAIN;
|
|
4878
|
+
journal.record('transfer search');
|
|
4879
|
+
try {
|
|
4880
|
+
const result = await search(authorization, { text: asked.text, rooms });
|
|
4881
|
+
if (result.kind === 'revoked-authorization')
|
|
4882
|
+
await refuse('revoked-authorization');
|
|
4883
|
+
journal.record(`outcome search ${result.kind}`);
|
|
4884
|
+
if (result.kind === 'rejected')
|
|
4885
|
+
return result;
|
|
4886
|
+
if (result.kind !== 'found')
|
|
4887
|
+
return failure(result);
|
|
4888
|
+
const kept = rooms === null ? null : new Set(rooms);
|
|
4889
|
+
return {
|
|
4890
|
+
kind: 'found',
|
|
4891
|
+
documents: result.documents.filter((hit) => kept === null || kept.has(hit.room)),
|
|
4550
4892
|
};
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4893
|
+
}
|
|
4894
|
+
catch {
|
|
4895
|
+
journal.record('outcome search transport-uncertainty');
|
|
4896
|
+
return UNCERTAIN;
|
|
4554
4897
|
}
|
|
4555
4898
|
}
|
|
4556
|
-
const watch = (topic, input, deliver) => {
|
|
4899
|
+
const watch = (topic, input, deliver, fail) => {
|
|
4557
4900
|
if (topic === 'room-drafts') {
|
|
4558
4901
|
const listener = { notify: deliver, ready: false };
|
|
4559
4902
|
roomDraftListeners.add(listener);
|
|
@@ -4602,12 +4945,13 @@ export function composeRuntime(options) {
|
|
|
4602
4945
|
};
|
|
4603
4946
|
}
|
|
4604
4947
|
if (topic === 'readiness') {
|
|
4605
|
-
return watchReadiness({ target: '', code: 'stored'
|
|
4948
|
+
return watchReadiness({ target: '', code: 'stored' }, null, deliver);
|
|
4606
4949
|
}
|
|
4607
4950
|
if (topic === 'rooms') {
|
|
4608
|
-
|
|
4609
|
-
|
|
4951
|
+
unlistedRooms.add(deliver);
|
|
4952
|
+
emitKnownRooms();
|
|
4610
4953
|
return () => {
|
|
4954
|
+
unlistedRooms.delete(deliver);
|
|
4611
4955
|
roomListeners.delete(deliver);
|
|
4612
4956
|
};
|
|
4613
4957
|
}
|
|
@@ -4646,6 +4990,13 @@ export function composeRuntime(options) {
|
|
|
4646
4990
|
memberListeners.delete(deliver);
|
|
4647
4991
|
};
|
|
4648
4992
|
}
|
|
4993
|
+
if (topic === 'authority-changes') {
|
|
4994
|
+
authorityChangeListeners.add(deliver);
|
|
4995
|
+
deliver(authorityChanges);
|
|
4996
|
+
return () => {
|
|
4997
|
+
authorityChangeListeners.delete(deliver);
|
|
4998
|
+
};
|
|
4999
|
+
}
|
|
4649
5000
|
if (topic === 'queue') {
|
|
4650
5001
|
const queue = { locator: input[0], notify: deliver };
|
|
4651
5002
|
queueSubscriptions.add(queue);
|
|
@@ -4654,24 +5005,34 @@ export function composeRuntime(options) {
|
|
|
4654
5005
|
queueSubscriptions.delete(queue);
|
|
4655
5006
|
};
|
|
4656
5007
|
}
|
|
4657
|
-
return watchPlan(planned(input[0], null), deliver);
|
|
5008
|
+
return watchPlan(planned(input[0], null), deliver, fail);
|
|
4658
5009
|
};
|
|
4659
5010
|
const md = {
|
|
4660
5011
|
loaded() {
|
|
4661
5012
|
return openLocal().then(() => undefined);
|
|
4662
5013
|
},
|
|
4663
5014
|
rooms() {
|
|
4664
|
-
|
|
5015
|
+
if (closed)
|
|
5016
|
+
return Promise.resolve([]);
|
|
5017
|
+
return new Promise((settle) => {
|
|
5018
|
+
const answer = (value) => {
|
|
5019
|
+
unlistedRooms.delete(answer);
|
|
5020
|
+
roomListeners.delete(answer);
|
|
5021
|
+
settle(value);
|
|
5022
|
+
};
|
|
5023
|
+
unlistedRooms.add(answer);
|
|
5024
|
+
emitKnownRooms();
|
|
5025
|
+
});
|
|
4665
5026
|
},
|
|
4666
5027
|
query: ((input) => rowsQuery(planned(input, null))),
|
|
4667
|
-
search: (input) => searched(input, null
|
|
5028
|
+
search: (input) => searched(input, null),
|
|
4668
5029
|
types(locator) {
|
|
4669
5030
|
return {
|
|
4670
5031
|
collection: modelTypes(modelOf(locator)),
|
|
4671
5032
|
typedSelection: rooms.get(locator)?.typedSelection === undefined ||
|
|
4672
5033
|
rooms.get(locator)?.typedSelection === null
|
|
4673
5034
|
? null
|
|
4674
|
-
: modelTypes(
|
|
5035
|
+
: modelTypes(builtIn),
|
|
4675
5036
|
};
|
|
4676
5037
|
},
|
|
4677
5038
|
roomDraft: {
|
|
@@ -4688,33 +5049,17 @@ export function composeRuntime(options) {
|
|
|
4688
5049
|
const record = roomDrafts.get(room)?.get(path);
|
|
4689
5050
|
return record === undefined ? null : ((await roomDraftsOf([record]))[0] ?? null);
|
|
4690
5051
|
},
|
|
4691
|
-
write(room, path, revision, input) {
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
const
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
? undefined
|
|
4703
|
-
: await heldFile(parts.file.blob, binaryBase(await visibleOf(room, [path]), path));
|
|
4704
|
-
const record = {
|
|
4705
|
-
locator: room,
|
|
4706
|
-
path,
|
|
4707
|
-
revision: roomDraftRevision(),
|
|
4708
|
-
parts: held(current?.parts, parts, file),
|
|
4709
|
-
returned: current?.returned ?? null,
|
|
4710
|
-
};
|
|
4711
|
-
await local.keepRoomDraft(record, parts.file === undefined || file === undefined
|
|
4712
|
-
? []
|
|
4713
|
-
: [{ key: file.localId, blob: parts.file.blob }]);
|
|
4714
|
-
rememberRoomDraft(record);
|
|
4715
|
-
publishChangeSet(changeSet(room, { drafts: [path] }));
|
|
4716
|
-
return { outcome: 'written', roomDraft: (await roomDraftsOf([record]))[0] ?? null };
|
|
4717
|
-
});
|
|
5052
|
+
write(room, path, revision, input, next = roomDraftRevision()) {
|
|
5053
|
+
if (typeof next !== 'string' || next === '')
|
|
5054
|
+
return Promise.reject(runtimeError('room-draft-malformed'));
|
|
5055
|
+
return gathered(`room-draft\u0000${room}\u0000${path}`, { revision, input, next }, (earlier, later) => {
|
|
5056
|
+
if (later.revision !== earlier.next)
|
|
5057
|
+
return null;
|
|
5058
|
+
const parts = gatheredParts(earlier.input, later.input);
|
|
5059
|
+
return parts === null
|
|
5060
|
+
? null
|
|
5061
|
+
: { revision: earlier.revision, input: parts, next: later.next };
|
|
5062
|
+
}, (written) => writtenRoomDraft(room, path, written.revision, written.input, written.next));
|
|
4718
5063
|
},
|
|
4719
5064
|
discard(room, path, revision) {
|
|
4720
5065
|
return exclusive(async () => {
|
|
@@ -4741,13 +5086,13 @@ export function composeRuntime(options) {
|
|
|
4741
5086
|
});
|
|
4742
5087
|
},
|
|
4743
5088
|
write(app, value) {
|
|
4744
|
-
return
|
|
5089
|
+
return exclusive(() => keptPresentation(app, () => structuredClone(value)));
|
|
4745
5090
|
},
|
|
4746
5091
|
edit(app, members) {
|
|
4747
|
-
return
|
|
4748
|
-
...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(
|
|
4749
|
-
...Object.entries(
|
|
4750
|
-
]));
|
|
5092
|
+
return gathered(`presentation\u0000${app}`, structuredClone(members), (earlier, later) => ({ ...earlier, ...later }), (edited) => keptPresentation(app, (held) => Object.fromEntries([
|
|
5093
|
+
...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(edited, member)),
|
|
5094
|
+
...Object.entries(edited).filter(([, value]) => value !== null),
|
|
5095
|
+
])));
|
|
4751
5096
|
},
|
|
4752
5097
|
},
|
|
4753
5098
|
save(room, named, writtenBy) {
|
|
@@ -4766,7 +5111,8 @@ export function composeRuntime(options) {
|
|
|
4766
5111
|
part: readPart,
|
|
4767
5112
|
read: readBinaryPath,
|
|
4768
5113
|
link: linkBinaryPath,
|
|
4769
|
-
|
|
5114
|
+
show: showBinaryPath,
|
|
5115
|
+
attach: attachBinary,
|
|
4770
5116
|
},
|
|
4771
5117
|
async call(name, input) {
|
|
4772
5118
|
await ready;
|
|
@@ -4789,7 +5135,8 @@ export function composeRuntime(options) {
|
|
|
4789
5135
|
journal.record('retry');
|
|
4790
5136
|
unpreparable.clear();
|
|
4791
5137
|
queueReadiness();
|
|
4792
|
-
|
|
5138
|
+
if (full)
|
|
5139
|
+
await probed();
|
|
4793
5140
|
},
|
|
4794
5141
|
observe: observing(watch),
|
|
4795
5142
|
async reauthorize() {
|
|
@@ -4803,7 +5150,7 @@ export function composeRuntime(options) {
|
|
|
4803
5150
|
authorization = undefined;
|
|
4804
5151
|
discoveryPaused = false;
|
|
4805
5152
|
if (options.adapter.roomStream === undefined)
|
|
4806
|
-
await sync('unstreamed');
|
|
5153
|
+
await sync('unstreamed', null);
|
|
4807
5154
|
else
|
|
4808
5155
|
await task(OPENING_LANE, ensureAuthorization);
|
|
4809
5156
|
},
|
|
@@ -4811,7 +5158,6 @@ export function composeRuntime(options) {
|
|
|
4811
5158
|
const reachable = {
|
|
4812
5159
|
md,
|
|
4813
5160
|
watch,
|
|
4814
|
-
referrerScope: (referrer) => options.adapter.referrerScope(referrer),
|
|
4815
5161
|
plan: planned,
|
|
4816
5162
|
query: (plan) => rowsQuery(plan),
|
|
4817
5163
|
watchPlan,
|
|
@@ -4821,12 +5167,12 @@ export function composeRuntime(options) {
|
|
|
4821
5167
|
lockedRooms,
|
|
4822
5168
|
watchLocks,
|
|
4823
5169
|
located,
|
|
5170
|
+
room: (locator) => listed().find((one) => one.locator === locator),
|
|
4824
5171
|
};
|
|
4825
5172
|
return {
|
|
4826
5173
|
md,
|
|
4827
5174
|
seed,
|
|
4828
5175
|
stop,
|
|
4829
|
-
lose: loseRooms,
|
|
4830
5176
|
watch,
|
|
4831
5177
|
persisted(answer) {
|
|
4832
5178
|
if (answer === persistence)
|
|
@@ -4835,6 +5181,10 @@ export function composeRuntime(options) {
|
|
|
4835
5181
|
journal.record(`persistence ${answer}`);
|
|
4836
5182
|
queueReadiness();
|
|
4837
5183
|
},
|
|
4838
|
-
within: (reach, port) =>
|
|
5184
|
+
within: (reach, port) => {
|
|
5185
|
+
if (full)
|
|
5186
|
+
void probed();
|
|
5187
|
+
return narrowed(reachable, reach, port);
|
|
5188
|
+
},
|
|
4839
5189
|
};
|
|
4840
5190
|
}
|