@agentshouse/mdruntime 0.1.0-c2e8a6f99866d → 0.1.0-c2ef9303a058d
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 +95 -67
- package/dist/adapter.d.ts.map +1 -1
- package/dist/attached-model.d.ts +5 -11
- package/dist/attached-model.d.ts.map +1 -1
- package/dist/attached-model.js +36 -47
- 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/change-set.d.ts +1 -7
- package/dist/change-set.d.ts.map +1 -1
- package/dist/change-set.js +1 -4
- package/dist/client.d.ts +15 -12
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +56 -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.map +1 -1
- package/dist/compile.js +117 -243
- package/dist/connection.d.ts +14 -12
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +1466 -1431
- 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 +273 -185
- package/dist/durability.d.ts +6 -4
- 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 +17 -8
- 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 -7
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +55 -38
- package/dist/identity.d.ts +0 -2
- package/dist/identity.d.ts.map +1 -1
- package/dist/identity.js +0 -6
- 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 +12 -6
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +209 -101
- package/dist/messages.d.ts +28 -0
- package/dist/messages.d.ts.map +1 -0
- package/dist/messages.js +89 -0
- package/dist/model.d.ts +8 -8
- 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 -206
- 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 +84 -72
- package/dist/react.d.ts +3 -1
- package/dist/react.d.ts.map +1 -1
- package/dist/readiness.d.ts +14 -17
- 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 +88 -30
- package/dist/replica.d.ts +11 -8
- package/dist/replica.d.ts.map +1 -1
- package/dist/replica.js +30 -15
- 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 +18 -16
- package/dist/rows.d.ts.map +1 -1
- package/dist/rows.js +45 -153
- 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 -95
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +10 -349
- 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 +25 -62
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +43 -282
- package/dist/stored.d.ts +12 -26
- package/dist/stored.d.ts.map +1 -1
- package/dist/stored.js +189 -226
- package/dist/tables.d.ts +55 -48
- package/dist/tables.d.ts.map +1 -1
- package/dist/tables.js +107 -57
- 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 +14 -9
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +5 -3
- package/package.json +14 -18
- package/AGENTS.md +0 -312
- package/dist/chat/acts.d.ts +0 -33
- package/dist/chat/acts.d.ts.map +0 -1
- package/dist/chat/acts.js +0 -36
- package/dist/chat/context.d.ts +0 -3
- package/dist/chat/context.d.ts.map +0 -1
- package/dist/chat/earlier.d.ts +0 -10
- package/dist/chat/earlier.d.ts.map +0 -1
- package/dist/chat/earlier.js +0 -54
- package/dist/chat/house.d.ts +0 -25
- package/dist/chat/house.d.ts.map +0 -1
- package/dist/chat/house.js +0 -38
- package/dist/chat/input.d.ts +0 -11
- package/dist/chat/input.d.ts.map +0 -1
- package/dist/chat/input.js +0 -92
- package/dist/chat/kit.d.ts +0 -61
- package/dist/chat/kit.d.ts.map +0 -1
- package/dist/chat/kit.js +0 -123
- package/dist/chat/lasting.d.ts +0 -6
- package/dist/chat/lasting.d.ts.map +0 -1
- package/dist/chat/lasting.js +0 -19
- package/dist/chat/list.d.ts +0 -28
- package/dist/chat/list.d.ts.map +0 -1
- package/dist/chat/list.js +0 -103
- package/dist/chat/markdown.d.ts +0 -21
- package/dist/chat/markdown.d.ts.map +0 -1
- package/dist/chat/markdown.js +0 -246
- package/dist/chat/message.d.ts +0 -7
- package/dist/chat/message.d.ts.map +0 -1
- package/dist/chat/message.js +0 -244
- package/dist/chat/messages.d.ts +0 -30
- package/dist/chat/messages.d.ts.map +0 -1
- package/dist/chat/messages.js +0 -198
- package/dist/chat/name.d.ts +0 -7
- package/dist/chat/name.d.ts.map +0 -1
- package/dist/chat/name.js +0 -37
- package/dist/chat/opened.d.ts +0 -12
- package/dist/chat/opened.d.ts.map +0 -1
- package/dist/chat/opened.js +0 -19
- package/dist/chat/overlay.d.ts +0 -32
- package/dist/chat/overlay.d.ts.map +0 -1
- package/dist/chat/overlay.js +0 -103
- package/dist/chat/parts.d.ts +0 -58
- package/dist/chat/parts.d.ts.map +0 -1
- package/dist/chat/parts.js +0 -120
- package/dist/chat/pieces.d.ts +0 -62
- package/dist/chat/pieces.d.ts.map +0 -1
- package/dist/chat/pieces.js +0 -1110
- package/dist/chat/process.d.ts +0 -20
- package/dist/chat/process.d.ts.map +0 -1
- package/dist/chat/process.js +0 -32
- package/dist/chat/provider.d.ts +0 -70
- package/dist/chat/provider.d.ts.map +0 -1
- package/dist/chat/provider.js +0 -635
- package/dist/chat/question.d.ts +0 -9
- package/dist/chat/question.d.ts.map +0 -1
- package/dist/chat/question.js +0 -69
- package/dist/chat/questions.d.ts +0 -31
- package/dist/chat/questions.d.ts.map +0 -1
- package/dist/chat/questions.js +0 -111
- package/dist/chat/read.d.ts +0 -5
- package/dist/chat/read.d.ts.map +0 -1
- package/dist/chat/read.js +0 -13
- package/dist/chat/ready.d.ts +0 -37
- package/dist/chat/ready.d.ts.map +0 -1
- package/dist/chat/ready.js +0 -93
- package/dist/chat/room-file.d.ts +0 -8
- package/dist/chat/room-file.d.ts.map +0 -1
- package/dist/chat/room-file.js +0 -72
- package/dist/chat/steady.d.ts +0 -3
- package/dist/chat/steady.d.ts.map +0 -1
- package/dist/chat/steady.js +0 -26
- package/dist/chat/style.d.ts +0 -4
- package/dist/chat/style.d.ts.map +0 -1
- package/dist/chat/style.js +0 -306
- package/dist/chat.d.ts +0 -15
- package/dist/chat.d.ts.map +0 -1
- package/dist/chat.js +0 -8
- package/dist/reads.d.ts +0 -11
- package/dist/reads.d.ts.map +0 -1
- package/dist/reads.js +0 -98
package/dist/connection.js
CHANGED
|
@@ -1,51 +1,41 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { attachedModel,
|
|
1
|
+
import { identityOf, parseFrontmatter, plain, queryKey, roomTarget, target, } from '@agentshouse/mdmodel';
|
|
2
|
+
import { attachedModel, builtInModel, roomTypesOf, schemaTypes } 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';
|
|
9
|
-
import {
|
|
9
|
+
import { searchPlanOf } from './search.js';
|
|
10
10
|
import { capped, MAXIMUM_WINDOW } from './windows.js';
|
|
11
11
|
import { bootstrapRoom } from './bootstrap.js';
|
|
12
12
|
import { changeSet, mergeChangeSets } from './change-set.js';
|
|
13
13
|
import { createConversationStore, } from './conversation.js';
|
|
14
14
|
import { leaving as leavingIn, partnerOf, queuedFile, } from './change.js';
|
|
15
|
-
import { available, collectionOf,
|
|
15
|
+
import { available, collectionOf, composedOf, validRoot, } from './collection.js';
|
|
16
16
|
import { runtimeError } from './failure.js';
|
|
17
17
|
import { createLog } from './log.js';
|
|
18
18
|
import { binaryRecord } from './binary.js';
|
|
19
|
+
import { attachedFile } from './file.js';
|
|
19
20
|
import { openedFiles } from './opened.js';
|
|
20
|
-
import { ABSENT_TARGET, absentBinary,
|
|
21
|
+
import { ABSENT_TARGET, absentBinary, attachAnswer, binaryAnswer, binaryLinkAnswer, calledResult, failure, historyAnswer, LOCKED_UNREADABLE, lockedRoom, OFFLINE, undeclared, UNCERTAIN, UNREADABLE, } from './outcome.js';
|
|
21
22
|
import { createRecovery } from './recovery.js';
|
|
22
|
-
import { doorReads } from './reads.js';
|
|
23
23
|
import { advancement } from './refresh.js';
|
|
24
24
|
import { buildBatch, buildMove, joined, limiting, queuedDocuments, requeuing, } from './save.js';
|
|
25
|
-
import { compositionOf, entryAt, landed, movedBy, overlayOf, replaying, returning, returnedPaths,
|
|
26
|
-
import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, moveTarget, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
|
|
25
|
+
import { compositionOf, entryAt, landed, movedBy, overlayOf, replaying, returning, returnedPaths, slicing, slicingWith, standing, storedRecord, touchedBy, } from './replica.js';
|
|
26
|
+
import { binaryBase, changedParts, changesOf, gatheredParts, held, heldFile, joinedDraft, moveTarget, readParts, readSavedParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
|
|
27
27
|
import { same, saving, STARTING, transferring, } from './status.js';
|
|
28
|
-
import { detectStorageLoss, nextGeneration } from './durability.js';
|
|
29
|
-
import { readinessOf,
|
|
28
|
+
import { detectStorageLoss, nextGeneration, refusedForSpace, } from './durability.js';
|
|
29
|
+
import { readinessOf, } from './readiness.js';
|
|
30
30
|
import { FencedMap } from './fence.js';
|
|
31
|
-
import { fingerprint } from './stored.js';
|
|
32
31
|
import { sameBinary } from './overlay.js';
|
|
33
32
|
import { paced, ROW_WEIGHT, stepped, yielded } from './turn.js';
|
|
34
|
-
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
|
-
function roomsAlone(plan) {
|
|
41
|
-
return (plan.reach === null &&
|
|
42
|
-
plan.root.types === null &&
|
|
43
|
-
plan.root.where === null &&
|
|
44
|
-
plan.root.pathPrefix === null &&
|
|
45
|
-
plan.paths === null &&
|
|
46
|
-
plan.hop === null &&
|
|
47
|
-
plan.backlinks === null);
|
|
48
|
-
}
|
|
49
39
|
function within(root, path) {
|
|
50
40
|
return root === '' || beneath(root, path);
|
|
51
41
|
}
|
|
@@ -63,39 +53,46 @@ const OPENING_LANE = 'opening';
|
|
|
63
53
|
const CONVERSATION_LANE = 'conversations';
|
|
64
54
|
const ROOM_READS = 256;
|
|
65
55
|
const ROOM_ATTEMPTS = 16;
|
|
66
|
-
const SEARCH_BATCH = 256;
|
|
67
56
|
const ERASE_BATCH = 512;
|
|
68
|
-
const
|
|
69
|
-
const
|
|
57
|
+
const DERIVE_BYTES = 1 << 23;
|
|
58
|
+
const DERIVE_FLOOR = 1 << 18;
|
|
59
|
+
// An index build aims at this much database work, so a Save, a query or the next part's progress waits behind at most one short build.
|
|
60
|
+
const DERIVE_AIM_MS = 250;
|
|
61
|
+
// A build past its floor of text stops after a second and a half of work, its full-text writes included, so a Room whose text costs ten times the usual holds a Save, a query or the next part's progress behind no longer build.
|
|
62
|
+
const DERIVE_LONGEST_MS = 1_500;
|
|
70
63
|
const QUERY_BYTES = 1 << 20;
|
|
71
64
|
const STAGE_BYTES = 1 << 20;
|
|
72
|
-
const
|
|
65
|
+
const STAGE_FLOOR = 1 << 16;
|
|
66
|
+
// A batch aims at this much database work, so a Save or a query waits behind at most one short write.
|
|
67
|
+
const STAGE_AIM_MS = 50;
|
|
68
|
+
const STREAMS_HELD = 4;
|
|
69
|
+
const NOTHING_HELD = { bytes: 0 };
|
|
70
|
+
const QUEUED_TEXT = 1 << 23;
|
|
71
|
+
// Bytes still arriving are reported at most this often, so readiness is not rebuilt for every chunk the network hands over while a slow link still moves the figure.
|
|
72
|
+
const ARRIVAL_REPORT_MS = 250;
|
|
73
73
|
// A Room that locks offline stays readable this long after the connection drops, so a laptop waking or changing network keeps working while a disconnected device locks soon after.
|
|
74
74
|
const LOCK_GRACE_MS = 120_000;
|
|
75
75
|
function operationOf(input) {
|
|
76
76
|
const operation = input?.operation_id;
|
|
77
77
|
return typeof operation === 'string' ? operation : null;
|
|
78
78
|
}
|
|
79
|
-
function
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
79
|
+
function readUntil(read, signal) {
|
|
80
|
+
if (signal.aborted)
|
|
81
|
+
return Promise.resolve({ kind: 'transport-uncertainty' });
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
const stop = () => resolve({ kind: 'transport-uncertainty' });
|
|
84
|
+
signal.addEventListener('abort', stop, { once: true });
|
|
85
|
+
read.then(resolve, reject).finally(() => signal.removeEventListener('abort', stop));
|
|
85
86
|
});
|
|
86
87
|
}
|
|
87
|
-
function roomLane(locator) {
|
|
88
|
-
return `room ${locator}`;
|
|
89
|
-
}
|
|
90
88
|
function pendingKey(locator, id) {
|
|
91
89
|
return `${locator}\u0000${id}`;
|
|
92
90
|
}
|
|
93
91
|
export function composeRuntime(options) {
|
|
94
92
|
const localSaves = options.adapter.localSaves === true;
|
|
95
|
-
const roomTypes = roomTypesOf(options.adapter.roomTypes);
|
|
93
|
+
const roomTypes = roomTypesOf(options.adapter.roomTypes, options.adapter.nameableRoomTypes);
|
|
96
94
|
let authorization = options.authorization;
|
|
97
95
|
let opening;
|
|
98
|
-
let store;
|
|
99
96
|
const fences = new Map();
|
|
100
97
|
const isFenced = (locator) => fences.has(locator);
|
|
101
98
|
let fencings = 0;
|
|
@@ -103,44 +100,53 @@ export function composeRuntime(options) {
|
|
|
103
100
|
const fencedSince = (locator, since) => fences.has(locator) || (lastFenced.get(locator) ?? 0) > since;
|
|
104
101
|
let discoveries = 0;
|
|
105
102
|
const replicas = new FencedMap(isFenced);
|
|
103
|
+
const reopened = new Set();
|
|
104
|
+
const usableWhenHeld = options.usableWhenHeld ?? [];
|
|
106
105
|
const queuesWithoutReplica = new FencedMap(isFenced);
|
|
107
106
|
const pendings = new Map();
|
|
107
|
+
const landedAhead = new Map();
|
|
108
108
|
const seeds = new Map();
|
|
109
109
|
const seedWatchers = new Map();
|
|
110
110
|
const roomDrafts = new FencedMap(isFenced);
|
|
111
|
+
const attachedFiles = new FencedMap(isFenced);
|
|
111
112
|
const presentations = new Map();
|
|
112
113
|
let limits = [];
|
|
113
114
|
let capabilities = [];
|
|
114
115
|
const rooms = new FencedMap(isFenced);
|
|
115
116
|
const counted = new Map();
|
|
116
117
|
const unreadable = new Set();
|
|
117
|
-
const
|
|
118
|
-
const
|
|
119
|
-
const
|
|
118
|
+
const builtIn = builtInModel(roomTypes);
|
|
119
|
+
const typedPaths = options.adapter.typedPaths ?? [];
|
|
120
|
+
const models = new FencedMap(isFenced);
|
|
121
|
+
const liveModels = new FencedMap(isFenced);
|
|
120
122
|
const invalidModels = new Set();
|
|
121
123
|
const discovered = new Map();
|
|
122
|
-
|
|
124
|
+
let inFlight = new AbortController();
|
|
125
|
+
const refreshed = options.adapter.refresh?.bind(options.adapter);
|
|
126
|
+
const reads = {
|
|
127
|
+
bootstrap: (authorized, request) => readUntil(options.adapter.bootstrap(authorized, request), inFlight.signal),
|
|
128
|
+
...(refreshed === undefined
|
|
129
|
+
? {}
|
|
130
|
+
: {
|
|
131
|
+
refresh: (authorized, request) => readUntil(refreshed(authorized, request), inFlight.signal),
|
|
132
|
+
}),
|
|
133
|
+
reached: (locator, roomVersion, target) => options.adapter.reached(locator, roomVersion, target),
|
|
134
|
+
};
|
|
123
135
|
let roomsListed = false;
|
|
124
136
|
const scopedRefusals = new Map();
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
function modelKey(locator, attached) {
|
|
128
|
-
return `${attached} ${locator}`;
|
|
137
|
+
function modelOf(locator) {
|
|
138
|
+
return models.get(locator) ?? NO_MODEL;
|
|
129
139
|
}
|
|
130
|
-
function
|
|
131
|
-
return
|
|
132
|
-
}
|
|
133
|
-
function savedModel(locator, attached) {
|
|
134
|
-
return invalidModels.has(modelKey(locator, attached))
|
|
140
|
+
function savedModel(locator) {
|
|
141
|
+
return invalidModels.has(locator)
|
|
135
142
|
? { status: 'unavailable', reason: 'revoked' }
|
|
136
|
-
: modelOf(locator
|
|
143
|
+
: modelOf(locator);
|
|
144
|
+
}
|
|
145
|
+
function composed(record) {
|
|
146
|
+
return compositionOf(record, builtIn, typedPaths);
|
|
137
147
|
}
|
|
138
148
|
function compositionFor(record) {
|
|
139
|
-
return
|
|
140
|
-
...record,
|
|
141
|
-
model: savedModel(record.locator, 'collection'),
|
|
142
|
-
selectionModel: savedModel(record.locator, 'typed-selection'),
|
|
143
|
-
});
|
|
149
|
+
return composed({ ...record, model: savedModel(record.locator) });
|
|
144
150
|
}
|
|
145
151
|
let refusal = null;
|
|
146
152
|
let storageLoss = null;
|
|
@@ -150,29 +156,31 @@ export function composeRuntime(options) {
|
|
|
150
156
|
let closed = false;
|
|
151
157
|
let stopping = false;
|
|
152
158
|
let withdrawStream;
|
|
153
|
-
const announced = new Map();
|
|
154
159
|
let underway = null;
|
|
155
160
|
let streamed = null;
|
|
156
161
|
let handed = false;
|
|
162
|
+
let naming = 0;
|
|
157
163
|
const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
|
|
158
164
|
const repairing = new Set();
|
|
159
165
|
const calling = new Set();
|
|
160
166
|
const repairable = new Map();
|
|
161
|
-
let outcomeRetention = null;
|
|
162
167
|
const answering = new Map();
|
|
163
168
|
let complete = false;
|
|
164
169
|
let recorded = false;
|
|
165
170
|
const stale = new Set();
|
|
166
171
|
let generation = 0;
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const
|
|
170
|
-
const reloadsDue = new Map();
|
|
172
|
+
const reloadsDue = new Set();
|
|
173
|
+
const highest = new Map();
|
|
174
|
+
const beyondHorizon = new Set();
|
|
171
175
|
const lanes = new Map();
|
|
172
|
-
|
|
176
|
+
const roomLanes = new Map();
|
|
177
|
+
let roomWork = 0;
|
|
178
|
+
const roomReads = { free: ROOM_READS, waiting: [] };
|
|
173
179
|
const working = new Set();
|
|
174
180
|
let mutations = Promise.resolve();
|
|
181
|
+
let gathering = null;
|
|
175
182
|
const roomListeners = new Set();
|
|
183
|
+
const unlistedRooms = new Set();
|
|
176
184
|
const refusalListeners = new Set();
|
|
177
185
|
const storageLossListeners = new Set();
|
|
178
186
|
const statusListeners = new Set();
|
|
@@ -183,6 +191,8 @@ export function composeRuntime(options) {
|
|
|
183
191
|
let withdrawConversations;
|
|
184
192
|
let currencyQueued = null;
|
|
185
193
|
let socketOpen = false;
|
|
194
|
+
let announced = false;
|
|
195
|
+
let missedChange = false;
|
|
186
196
|
const confirmed = new Set();
|
|
187
197
|
let lockGrace = null;
|
|
188
198
|
const lockListeners = new Set();
|
|
@@ -192,73 +202,81 @@ export function composeRuntime(options) {
|
|
|
192
202
|
const serviceModeListeners = new Set();
|
|
193
203
|
let member = null;
|
|
194
204
|
const memberListeners = new Set();
|
|
195
|
-
|
|
205
|
+
let authorityChanges = 0;
|
|
206
|
+
const authorityChangeListeners = new Set();
|
|
196
207
|
const unpreparedDemand = new Set();
|
|
197
|
-
const
|
|
208
|
+
const unpreparable = new Set();
|
|
198
209
|
let preparing = false;
|
|
199
210
|
let targets = {};
|
|
200
211
|
let persistence = options.shell.persistence;
|
|
201
|
-
let
|
|
202
|
-
|
|
203
|
-
let
|
|
212
|
+
let full = false;
|
|
213
|
+
let probing = null;
|
|
214
|
+
let testing = null;
|
|
215
|
+
const database = {
|
|
216
|
+
async ask(request) {
|
|
217
|
+
try {
|
|
218
|
+
return await options.database.ask(request);
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
if (refusedForSpace(error))
|
|
222
|
+
filled();
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
};
|
|
204
227
|
const readinessWatches = new Set();
|
|
205
228
|
let readinessQueued = false;
|
|
206
229
|
const catching = new Set();
|
|
207
230
|
const transfers = new FencedMap(isFenced);
|
|
231
|
+
const installs = new Set();
|
|
232
|
+
const arrivals = new Map();
|
|
233
|
+
const streaming = new Set();
|
|
234
|
+
const underived = new Map();
|
|
235
|
+
const unindexed = new Map();
|
|
236
|
+
const startedOver = new Map();
|
|
237
|
+
let arrivalReported = 0;
|
|
238
|
+
let streamsFree = STREAMS_HELD;
|
|
239
|
+
let queuedText = 0;
|
|
240
|
+
const installWaiters = [];
|
|
241
|
+
const stageBudget = {
|
|
242
|
+
bytes: STAGE_FLOOR * 4,
|
|
243
|
+
floor: STAGE_FLOOR,
|
|
244
|
+
ceiling: STAGE_BYTES,
|
|
245
|
+
aim: STAGE_AIM_MS,
|
|
246
|
+
};
|
|
247
|
+
const deriveBudget = {
|
|
248
|
+
bytes: DERIVE_FLOOR * 4,
|
|
249
|
+
floor: DERIVE_FLOOR,
|
|
250
|
+
ceiling: DERIVE_BYTES,
|
|
251
|
+
aim: DERIVE_AIM_MS,
|
|
252
|
+
};
|
|
253
|
+
const installTurns = { busy: false, ahead: 0, finishing: [], starting: [] };
|
|
208
254
|
const conversations = createConversationStore({
|
|
209
|
-
agents: declaredAgents,
|
|
210
|
-
documents: (declaration, after) => rowsQuery(planOf({
|
|
211
|
-
rooms: [declaration.locator],
|
|
212
|
-
types: [declaration.type],
|
|
213
|
-
select: {
|
|
214
|
-
fields: [
|
|
215
|
-
declaration.identity,
|
|
216
|
-
declaration.name,
|
|
217
|
-
...(declaration.retired === undefined ? [] : [declaration.retired]),
|
|
218
|
-
].map((field) => `/${escapePointer(field)}`),
|
|
219
|
-
},
|
|
220
|
-
limit: MAXIMUM_WINDOW,
|
|
221
|
-
...(after === undefined ? {} : { after }),
|
|
222
|
-
}, roomFacts())),
|
|
223
255
|
call: (name, input) => invokeAction(name, input),
|
|
256
|
+
history: (request) => readConversationHistory(request),
|
|
224
257
|
durably,
|
|
225
258
|
stream: () => options.adapter.conversations,
|
|
226
259
|
publish: publishChangeSet,
|
|
227
260
|
online: () => connectionOf() !== 'unreachable' && refusal === null,
|
|
261
|
+
full: () => full,
|
|
228
262
|
closed: () => closed,
|
|
229
263
|
unanswered: resendLater,
|
|
230
|
-
expired: (operation) => pastHorizon(operation),
|
|
231
|
-
});
|
|
232
|
-
const searchIndex = createSearchIndex({
|
|
233
|
-
async *room(room) {
|
|
234
|
-
const local = await openLocal();
|
|
235
|
-
for (let after = null;;) {
|
|
236
|
-
const key = replicas.get(room)?.record.key;
|
|
237
|
-
if (key === undefined)
|
|
238
|
-
return;
|
|
239
|
-
const page = await local.searched(key, room, after, SEARCH_BATCH, null);
|
|
240
|
-
yield page.documents;
|
|
241
|
-
if (page.next === null)
|
|
242
|
-
return;
|
|
243
|
-
after = page.next;
|
|
244
|
-
}
|
|
245
|
-
},
|
|
246
|
-
async documents(room, paths) {
|
|
247
|
-
const key = replicas.get(room)?.record.key;
|
|
248
|
-
if (key === undefined || paths.length === 0)
|
|
249
|
-
return [];
|
|
250
|
-
return (await (await openLocal()).searched(key, room, null, paths.length, paths)).documents;
|
|
251
|
-
},
|
|
252
264
|
});
|
|
253
|
-
const roomsToIndex = new Set();
|
|
254
|
-
const conversationsToIndex = new Set();
|
|
255
|
-
let indexing = false;
|
|
256
265
|
const journal = createLog();
|
|
257
|
-
const keptFiles = openedFiles(
|
|
258
|
-
let
|
|
266
|
+
const keptFiles = openedFiles(database, isFenced);
|
|
267
|
+
let readyFromStore = () => { };
|
|
259
268
|
const ready = new Promise((resolve) => {
|
|
260
|
-
|
|
269
|
+
readyFromStore = resolve;
|
|
261
270
|
});
|
|
271
|
+
let notifyDiscovered = () => { };
|
|
272
|
+
const firstDiscovery = new Promise((resolve) => {
|
|
273
|
+
notifyDiscovered = resolve;
|
|
274
|
+
});
|
|
275
|
+
let roomsNamed = false;
|
|
276
|
+
function notifyReady() {
|
|
277
|
+
notifyDiscovered();
|
|
278
|
+
readyFromStore();
|
|
279
|
+
}
|
|
262
280
|
function rememberRoomDraft(record) {
|
|
263
281
|
const room = roomDrafts.get(record.locator) ?? new Map();
|
|
264
282
|
room.set(record.path, record);
|
|
@@ -278,7 +296,7 @@ export function composeRuntime(options) {
|
|
|
278
296
|
return undefined;
|
|
279
297
|
const local = await openLocal();
|
|
280
298
|
const slice = await stepped(slicing(await local.entries(held.record.key, 'visible', paths), paths));
|
|
281
|
-
return { ...slice, collection: collectionOf(slice.documents,
|
|
299
|
+
return { ...slice, collection: collectionOf(slice.documents, composed(held.record)) };
|
|
282
300
|
}
|
|
283
301
|
function roomDraftOf(record, visible) {
|
|
284
302
|
return structuredClone({
|
|
@@ -334,14 +352,40 @@ export function composeRuntime(options) {
|
|
|
334
352
|
const value = presentations.get(app);
|
|
335
353
|
return value === undefined ? null : structuredClone(value);
|
|
336
354
|
}
|
|
337
|
-
function
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
355
|
+
async function writtenRoomDraft(room, path, revision, input, next) {
|
|
356
|
+
const parts = readParts(input, path);
|
|
357
|
+
if (path === '' || normalize(path) !== path)
|
|
358
|
+
throw runtimeError('room-draft-malformed', `room-draft-malformed at ${path}: a path inside the Room, such as notes/a.md`);
|
|
359
|
+
const local = await openLocal();
|
|
360
|
+
const current = roomDrafts.get(room)?.get(path);
|
|
361
|
+
if ((current?.revision ?? null) !== revision) {
|
|
362
|
+
return staleWrite(current === undefined ? null : ((await roomDraftsOf([current]))[0] ?? null));
|
|
363
|
+
}
|
|
364
|
+
const file = parts.file === undefined
|
|
365
|
+
? undefined
|
|
366
|
+
: await heldFile(parts.file.blob, parts.file.base === undefined
|
|
367
|
+
? binaryBase(await visibleOf(room, [path]), path)
|
|
368
|
+
: parts.file.base);
|
|
369
|
+
const record = {
|
|
370
|
+
locator: room,
|
|
371
|
+
path,
|
|
372
|
+
revision: next,
|
|
373
|
+
parts: held(current?.parts, parts, file),
|
|
374
|
+
returned: current?.returned ?? null,
|
|
375
|
+
};
|
|
376
|
+
await local.keepRoomDraft(record, parts.file === undefined || file === undefined
|
|
377
|
+
? []
|
|
378
|
+
: [{ key: file.localId, blob: parts.file.blob }]);
|
|
379
|
+
rememberRoomDraft(record);
|
|
380
|
+
publishChangeSet(changeSet(room, { drafts: [path] }));
|
|
381
|
+
return { outcome: 'written', roomDraft: (await roomDraftsOf([record]))[0] ?? null };
|
|
382
|
+
}
|
|
383
|
+
async function keptPresentation(app, next) {
|
|
384
|
+
const local = await openLocal();
|
|
385
|
+
const stored = next(presentations.get(app));
|
|
386
|
+
await local.present({ app, value: stored });
|
|
387
|
+
presentations.set(app, stored);
|
|
388
|
+
publishChangeSet(changeSet('', { presentation: app }));
|
|
345
389
|
}
|
|
346
390
|
function emitPresentation(app) {
|
|
347
391
|
if (closed)
|
|
@@ -356,6 +400,20 @@ export function composeRuntime(options) {
|
|
|
356
400
|
(queuesWithoutReplica.size > 0 ||
|
|
357
401
|
[...replicas.values()].some((replica) => replica.queue.length > 0)));
|
|
358
402
|
}
|
|
403
|
+
function unsent() {
|
|
404
|
+
const changed = new Map();
|
|
405
|
+
for (const room of [...replicas.keys(), ...queuesWithoutReplica.keys()]) {
|
|
406
|
+
for (const { batch, state } of entriesOf(room)) {
|
|
407
|
+
if (state === 'local')
|
|
408
|
+
continue;
|
|
409
|
+
for (const { path } of batch.changes)
|
|
410
|
+
changed.set(`${room}\u0000${path}`, { room, path });
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return [...changed]
|
|
414
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
415
|
+
.map(([, change]) => change);
|
|
416
|
+
}
|
|
359
417
|
function attention() {
|
|
360
418
|
const attentionRequired = [];
|
|
361
419
|
for (const record of [...roomDrafts.values()].flatMap((room) => [...room.values()])) {
|
|
@@ -367,16 +425,9 @@ export function composeRuntime(options) {
|
|
|
367
425
|
returned: structuredClone(record.returned),
|
|
368
426
|
});
|
|
369
427
|
}
|
|
370
|
-
for (const [room, refusal] of sourceRefusals)
|
|
371
|
-
attentionRequired.push({ room, refusal: structuredClone(refusal) });
|
|
372
428
|
attentionRequired.sort((left, right) => {
|
|
373
429
|
if (left.room !== right.room)
|
|
374
430
|
return left.room < right.room ? -1 : 1;
|
|
375
|
-
if ('refusal' in left || 'refusal' in right) {
|
|
376
|
-
if ('refusal' in left && 'refusal' in right)
|
|
377
|
-
return 0;
|
|
378
|
-
return 'refusal' in left ? -1 : 1;
|
|
379
|
-
}
|
|
380
431
|
if (left.path !== right.path)
|
|
381
432
|
return left.path < right.path ? -1 : 1;
|
|
382
433
|
return 0;
|
|
@@ -388,7 +439,7 @@ export function composeRuntime(options) {
|
|
|
388
439
|
if (reset === null || submission.reset < reset)
|
|
389
440
|
reset = submission.reset;
|
|
390
441
|
}
|
|
391
|
-
return { attentionRequired, reset };
|
|
442
|
+
return { attentionRequired, unsent: unsent(), reset };
|
|
392
443
|
}
|
|
393
444
|
function documentsIn(locator) {
|
|
394
445
|
return replicas.get(locator)?.count ?? 0;
|
|
@@ -461,7 +512,7 @@ export function composeRuntime(options) {
|
|
|
461
512
|
const governed = governs(reach, room);
|
|
462
513
|
const attached = [
|
|
463
514
|
...(governed.collection ? [modelOf(room.locator)] : []),
|
|
464
|
-
...(governed.selection ? [
|
|
515
|
+
...(governed.selection ? [builtIn] : []),
|
|
465
516
|
];
|
|
466
517
|
return {
|
|
467
518
|
locator: room.locator,
|
|
@@ -477,11 +528,26 @@ export function composeRuntime(options) {
|
|
|
477
528
|
const value = listed();
|
|
478
529
|
for (const listener of roomListeners)
|
|
479
530
|
listener(value);
|
|
531
|
+
emitKnownRooms();
|
|
532
|
+
}
|
|
533
|
+
function emitKnownRooms() {
|
|
534
|
+
conversations.learned();
|
|
535
|
+
if (unlistedRooms.size === 0)
|
|
536
|
+
return;
|
|
537
|
+
if (!roomsListed && rooms.size === 0 && refusal === null && !status.providerUnreachable)
|
|
538
|
+
return;
|
|
539
|
+
const value = listed();
|
|
540
|
+
for (const listener of [...unlistedRooms]) {
|
|
541
|
+
unlistedRooms.delete(listener);
|
|
542
|
+
roomListeners.add(listener);
|
|
543
|
+
listener(value);
|
|
544
|
+
}
|
|
480
545
|
}
|
|
481
546
|
function emitRefusal() {
|
|
482
547
|
queueReadiness();
|
|
483
548
|
for (const listener of refusalListeners)
|
|
484
549
|
listener(refusal);
|
|
550
|
+
emitKnownRooms();
|
|
485
551
|
}
|
|
486
552
|
function emitStorageLoss() {
|
|
487
553
|
queueReadiness();
|
|
@@ -502,6 +568,7 @@ export function composeRuntime(options) {
|
|
|
502
568
|
if (closed)
|
|
503
569
|
return;
|
|
504
570
|
queueReadiness();
|
|
571
|
+
emitKnownRooms();
|
|
505
572
|
const next = status.state === 'synced' && !conversations.current()
|
|
506
573
|
? { ...status, state: 'syncing' }
|
|
507
574
|
: status;
|
|
@@ -515,9 +582,6 @@ export function composeRuntime(options) {
|
|
|
515
582
|
if (localSaves)
|
|
516
583
|
return Object.freeze({ batch, state: 'local' });
|
|
517
584
|
const state = pendings.get(pendingKey(locator, batch.id));
|
|
518
|
-
if (state?.accepted !== undefined) {
|
|
519
|
-
return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
|
|
520
|
-
}
|
|
521
585
|
return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
|
|
522
586
|
}
|
|
523
587
|
function entriesOf(locator) {
|
|
@@ -532,23 +596,29 @@ export function composeRuntime(options) {
|
|
|
532
596
|
}
|
|
533
597
|
function applyChangeSet(set) {
|
|
534
598
|
liveQueries.touched(set);
|
|
535
|
-
searchIndex.changed(set);
|
|
536
599
|
}
|
|
537
600
|
function unpublished(error) {
|
|
538
601
|
journal.record(`publication failed ${error instanceof Error ? error.name : 'unknown'}`);
|
|
539
602
|
}
|
|
603
|
+
function indexed(locator) {
|
|
604
|
+
return replicas.get(locator)?.indexed === true;
|
|
605
|
+
}
|
|
540
606
|
async function prepareReplica(locator) {
|
|
541
607
|
for (;;) {
|
|
542
608
|
const held = replicas.get(locator);
|
|
543
|
-
if (held === undefined || closed ||
|
|
609
|
+
if (held === undefined || closed || held.indexed)
|
|
544
610
|
return;
|
|
545
611
|
const key = held.record.key;
|
|
546
|
-
const done = await exclusive(async () =>
|
|
547
|
-
|
|
612
|
+
const done = await exclusive(async () => {
|
|
613
|
+
const local = await openLocal();
|
|
614
|
+
return timed(deriveBudget, () => local.rederive(key, composed(held.record), deriveBudget.bytes, DERIVE_LONGEST_MS, DERIVE_FLOOR));
|
|
615
|
+
});
|
|
616
|
+
const now = replicas.get(locator);
|
|
617
|
+
if (now?.record.key !== key)
|
|
548
618
|
continue;
|
|
549
|
-
if (!done)
|
|
619
|
+
if (!done.done)
|
|
550
620
|
continue;
|
|
551
|
-
|
|
621
|
+
replicas.set(locator, { ...now, indexed: true });
|
|
552
622
|
publishChangeSet(changeSet(locator, { documents: null }));
|
|
553
623
|
liveQueries.recover();
|
|
554
624
|
queueReadiness();
|
|
@@ -556,11 +626,11 @@ export function composeRuntime(options) {
|
|
|
556
626
|
}
|
|
557
627
|
}
|
|
558
628
|
function unprepared() {
|
|
559
|
-
return [...unpreparedDemand, ...replicas.keys()].find((locator) => !
|
|
629
|
+
return [...unpreparedDemand, ...replicas.keys()].find((locator) => !indexed(locator) && !unpreparable.has(locator));
|
|
560
630
|
}
|
|
561
|
-
function
|
|
562
|
-
|
|
563
|
-
journal.record(`
|
|
631
|
+
function unpreparedBy(locator, error) {
|
|
632
|
+
unpreparable.add(locator);
|
|
633
|
+
journal.record(`preparation failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
|
|
564
634
|
queueReadiness();
|
|
565
635
|
}
|
|
566
636
|
function prepare(locators = []) {
|
|
@@ -576,7 +646,7 @@ export function composeRuntime(options) {
|
|
|
576
646
|
if (closed)
|
|
577
647
|
return;
|
|
578
648
|
unpreparedDemand.delete(waiting);
|
|
579
|
-
await prepareReplica(waiting).catch((error) =>
|
|
649
|
+
await prepareReplica(waiting).catch((error) => unpreparedBy(waiting, error));
|
|
580
650
|
}
|
|
581
651
|
}
|
|
582
652
|
finally {
|
|
@@ -617,14 +687,14 @@ export function composeRuntime(options) {
|
|
|
617
687
|
pending.push({ room: locator, awaits: 'acquisition' });
|
|
618
688
|
continue;
|
|
619
689
|
}
|
|
620
|
-
if (!
|
|
690
|
+
if (!replica.indexed) {
|
|
621
691
|
pending.push({ room: locator, awaits: 'preparation' });
|
|
622
692
|
continue;
|
|
623
693
|
}
|
|
624
694
|
coverage.push({
|
|
625
695
|
room: locator,
|
|
626
696
|
generation: replica.record.transfer,
|
|
627
|
-
current: !(catching.has(locator) ||
|
|
697
|
+
current: !(catching.has(locator) || transfers.has(locator)),
|
|
628
698
|
});
|
|
629
699
|
read.push({ locator, key: replica.record.key });
|
|
630
700
|
for (const batch of replica.queue)
|
|
@@ -650,9 +720,7 @@ export function composeRuntime(options) {
|
|
|
650
720
|
return { listed: true, rooms: named.filter((room) => rooms.has(room)) };
|
|
651
721
|
return { listed: named.every((room) => replicas.has(room)), rooms: named };
|
|
652
722
|
}
|
|
653
|
-
function roomState(locator
|
|
654
|
-
if (sourceRefusals.has(locator))
|
|
655
|
-
return 'refused';
|
|
723
|
+
function roomState(locator) {
|
|
656
724
|
const listedRoom = discovered.get(locator);
|
|
657
725
|
if (roomsListed &&
|
|
658
726
|
(listedRoom === undefined ||
|
|
@@ -661,27 +729,17 @@ export function composeRuntime(options) {
|
|
|
661
729
|
return 'unadmitted';
|
|
662
730
|
if (!replicas.has(locator))
|
|
663
731
|
return 'acquiring';
|
|
664
|
-
if (!
|
|
732
|
+
if (!indexed(locator))
|
|
665
733
|
return 'preparing';
|
|
666
|
-
if (search && !searchIndex.rooms().has(locator))
|
|
667
|
-
return 'indexing';
|
|
668
734
|
return 'complete';
|
|
669
735
|
}
|
|
670
|
-
function conversationCoverage(reach
|
|
736
|
+
function conversationCoverage(reach) {
|
|
671
737
|
const declared = reach === null || reach.conversations;
|
|
672
|
-
if (!declared || options.adapter.conversations === undefined
|
|
738
|
+
if (!declared || options.adapter.conversations === undefined)
|
|
673
739
|
return 'undeclared';
|
|
674
740
|
if (!conversations.known() || !conversations.current())
|
|
675
741
|
return 'acquiring';
|
|
676
|
-
|
|
677
|
-
return 'complete';
|
|
678
|
-
const unindexed = conversations
|
|
679
|
-
.searchable()
|
|
680
|
-
.filter((one) => conversations.acquired(one) && !searchIndex.conversations().has(one));
|
|
681
|
-
if (unindexed.length === 0)
|
|
682
|
-
return 'complete';
|
|
683
|
-
index([], unindexed);
|
|
684
|
-
return 'indexing';
|
|
742
|
+
return 'complete';
|
|
685
743
|
}
|
|
686
744
|
function connectionOf() {
|
|
687
745
|
if (socketOpen)
|
|
@@ -692,58 +750,77 @@ export function composeRuntime(options) {
|
|
|
692
750
|
? 'connected'
|
|
693
751
|
: 'connecting';
|
|
694
752
|
}
|
|
695
|
-
function
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
}
|
|
753
|
+
function bytesOf(room, underway) {
|
|
754
|
+
const transfer = transfers.get(room);
|
|
755
|
+
if (transfer !== undefined) {
|
|
756
|
+
const owed = (unindexed.get(room) ?? []).reduce((sum, part) => sum + part.bytes, 0);
|
|
757
|
+
return {
|
|
758
|
+
done: transfer.done.bytes - Math.ceil(owed),
|
|
759
|
+
total: Math.max(arrivals.get(room) ?? 0, transfer.done.bytes),
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
const held = underway ? NOTHING_HELD : (replicas.get(room)?.record.inventory ?? NOTHING_HELD);
|
|
763
|
+
return { done: held.bytes, total: arrivals.get(room) ?? held.bytes };
|
|
764
|
+
}
|
|
765
|
+
function installationOf(coverage) {
|
|
766
|
+
const rooms = { done: 0, total: 0 };
|
|
767
|
+
const bytes = { done: 0, total: 0 };
|
|
768
|
+
let installing = 0;
|
|
769
|
+
let downloading = false;
|
|
770
|
+
for (const { room, state } of coverage) {
|
|
771
|
+
if (state === 'unadmitted')
|
|
772
|
+
continue;
|
|
773
|
+
rooms.total += 1;
|
|
774
|
+
if (state === 'complete')
|
|
775
|
+
rooms.done += 1;
|
|
776
|
+
const underway = state === 'acquiring' || (installs.has(room) && state !== 'complete');
|
|
777
|
+
if (underway)
|
|
778
|
+
installing += 1;
|
|
779
|
+
if (underway && (streaming.has(room) || !arrivals.has(room)))
|
|
780
|
+
downloading = true;
|
|
781
|
+
const now = bytesOf(room, underway);
|
|
782
|
+
const earlier = underway ? startedOver.get(room) : undefined;
|
|
783
|
+
bytes.done += Math.max(now.done, earlier?.done ?? 0);
|
|
784
|
+
bytes.total += Math.max(now.total, earlier?.total ?? 0);
|
|
785
|
+
}
|
|
786
|
+
const step = installing === 0
|
|
787
|
+
? null
|
|
788
|
+
: downloading
|
|
789
|
+
? 'downloading'
|
|
790
|
+
: bytes.done < bytes.total
|
|
791
|
+
? 'writing'
|
|
792
|
+
: 'replaying';
|
|
793
|
+
return { step, rooms, bytes };
|
|
710
794
|
}
|
|
711
795
|
function readinessFor(port, reach) {
|
|
712
796
|
const required = requiredRooms(reach);
|
|
713
|
-
const
|
|
714
|
-
const
|
|
715
|
-
|
|
716
|
-
.filter((room) => room.state === state && !unindexable.has(room.room))
|
|
797
|
+
const coverage = required.rooms.map((room) => ({ room, state: roomState(room) }));
|
|
798
|
+
const preparing = coverage
|
|
799
|
+
.filter((room) => room.state === 'preparing' && !unpreparable.has(room.room))
|
|
717
800
|
.map(({ room }) => room);
|
|
718
|
-
const preparing = building('preparing');
|
|
719
801
|
if (preparing.length > 0)
|
|
720
802
|
prepare(preparing);
|
|
721
|
-
const indexing = building('indexing');
|
|
722
|
-
if (indexing.length > 0)
|
|
723
|
-
index(indexing, []);
|
|
724
803
|
return readinessOf({
|
|
725
804
|
code: port.code,
|
|
726
805
|
coverage: {
|
|
727
806
|
listed: required.listed,
|
|
728
807
|
rooms: coverage,
|
|
729
|
-
conversations: conversationCoverage(reach
|
|
730
|
-
search: search ? 'required' : 'unused',
|
|
808
|
+
conversations: conversationCoverage(reach),
|
|
731
809
|
},
|
|
732
810
|
persistence,
|
|
733
811
|
connection: connectionOf(),
|
|
734
812
|
house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
? capacity
|
|
738
|
-
: port.capacity,
|
|
739
|
-
refused: refusedIn(required.rooms),
|
|
813
|
+
space: full ? 'full' : null,
|
|
814
|
+
installation: installationOf(coverage),
|
|
740
815
|
lost: storageLoss !== null,
|
|
741
|
-
established: targets[port.target]?.established === true
|
|
816
|
+
established: targets[port.target]?.established === true ||
|
|
817
|
+
(usableWhenHeld.includes(port.target) && required.rooms.some((room) => reopened.has(room))),
|
|
742
818
|
});
|
|
743
819
|
}
|
|
744
820
|
function establish(name) {
|
|
745
821
|
targets = { ...targets, [name]: { established: true } };
|
|
746
822
|
queueReadiness();
|
|
823
|
+
probeEachMinute();
|
|
747
824
|
void durably((local) => local.fact('targets', targets)).catch(() => {
|
|
748
825
|
journal.record(`target record failed ${name}`);
|
|
749
826
|
});
|
|
@@ -781,87 +858,56 @@ export function composeRuntime(options) {
|
|
|
781
858
|
readinessWatches.delete(watch);
|
|
782
859
|
};
|
|
783
860
|
}
|
|
784
|
-
function
|
|
785
|
-
|
|
786
|
-
for (const { path, content } of page.entries)
|
|
787
|
-
bytes += (path.length + (content?.length ?? 0)) * 2;
|
|
788
|
-
return bytes;
|
|
789
|
-
}
|
|
790
|
-
function exceeded(error) {
|
|
791
|
-
return (typeof error === 'object' &&
|
|
792
|
-
error !== null &&
|
|
793
|
-
error.name === 'QuotaExceededError');
|
|
794
|
-
}
|
|
795
|
-
async function estimated() {
|
|
796
|
-
const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
|
|
797
|
-
return estimate === null ? null : Math.max(0, estimate.total - estimate.usage);
|
|
798
|
-
}
|
|
799
|
-
function pause(locator, cause) {
|
|
800
|
-
paused.add(locator);
|
|
801
|
-
capacity = cause;
|
|
802
|
-
journal.record(`capacity ${cause.cause} ${locator} ${cause.needed} ${cause.available ?? '-'}`);
|
|
803
|
-
queueReadiness();
|
|
804
|
-
if (rechecking !== null)
|
|
861
|
+
function filled() {
|
|
862
|
+
if (full || closed)
|
|
805
863
|
return;
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
864
|
+
full = true;
|
|
865
|
+
journal.record('space full');
|
|
866
|
+
queueReadiness();
|
|
867
|
+
probeEachMinute();
|
|
810
868
|
}
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
return;
|
|
815
|
-
const available = await estimated().catch(() => null);
|
|
816
|
-
if (available === null || available < (held.needed ?? 0))
|
|
817
|
-
return;
|
|
818
|
-
if (held.cause === 'storage-exceeded' &&
|
|
819
|
-
(held.available === null || available <= held.available))
|
|
820
|
-
return;
|
|
821
|
-
resume();
|
|
869
|
+
function installedTarget() {
|
|
870
|
+
return ((usableWhenHeld.length > 0 && reopened.size > 0) ||
|
|
871
|
+
Object.values(targets).some((target) => target.established));
|
|
822
872
|
}
|
|
823
|
-
function
|
|
824
|
-
if (
|
|
825
|
-
clearInterval(rechecking);
|
|
826
|
-
rechecking = null;
|
|
827
|
-
if (paused.size === 0 && capacity === null)
|
|
873
|
+
function probeEachMinute() {
|
|
874
|
+
if (!full || probing !== null || closed || !installedTarget())
|
|
828
875
|
return;
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
if (!closed && authorization !== undefined)
|
|
834
|
-
void sync('unstreamed');
|
|
835
|
-
}
|
|
836
|
-
async function reclaimed(local) {
|
|
837
|
-
const unreferenced = [...transfers.keys()].filter((locator) => !discovered.has(locator));
|
|
838
|
-
for (const locator of unreferenced) {
|
|
839
|
-
await local.abandon(locator);
|
|
840
|
-
transfers.delete(locator);
|
|
841
|
-
}
|
|
842
|
-
if (unreferenced.length > 0)
|
|
843
|
-
journal.record(`capacity reclaimed ${unreferenced.length}`);
|
|
844
|
-
return unreferenced.length > 0;
|
|
876
|
+
// A device whose storage filled after its install tries a write each minute, so space freed elsewhere ends its read-only mode without a Retry.
|
|
877
|
+
probing = setInterval(() => {
|
|
878
|
+
void probed();
|
|
879
|
+
}, 60_000);
|
|
845
880
|
}
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
if (available === null || available >= needed)
|
|
851
|
-
return true;
|
|
852
|
-
if (await reclaimed(local))
|
|
853
|
-
available = await estimated().catch(() => null);
|
|
854
|
-
if (available === null || available >= needed)
|
|
881
|
+
function probed() {
|
|
882
|
+
testing ??= database.ask({ kind: 'space.probe' }).then(() => {
|
|
883
|
+
testing = null;
|
|
884
|
+
spaceFreed();
|
|
855
885
|
return true;
|
|
856
|
-
|
|
857
|
-
|
|
886
|
+
}, () => {
|
|
887
|
+
testing = null;
|
|
888
|
+
return false;
|
|
889
|
+
});
|
|
890
|
+
return testing;
|
|
858
891
|
}
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
892
|
+
function spaceFreed() {
|
|
893
|
+
if (!full)
|
|
894
|
+
return;
|
|
895
|
+
full = false;
|
|
896
|
+
if (probing !== null)
|
|
897
|
+
clearInterval(probing);
|
|
898
|
+
probing = null;
|
|
899
|
+
unpreparable.clear();
|
|
900
|
+
journal.record('space freed');
|
|
901
|
+
queueReadiness();
|
|
902
|
+
if (closed || authorization === undefined)
|
|
903
|
+
return;
|
|
904
|
+
if (options.adapter.roomStream === undefined)
|
|
905
|
+
void sync('unstreamed', null);
|
|
906
|
+
else
|
|
907
|
+
void resynchronize();
|
|
862
908
|
}
|
|
863
909
|
function seedRoomHeld(room, version) {
|
|
864
|
-
return
|
|
910
|
+
return indexed(room) && reachedBy(room, version);
|
|
865
911
|
}
|
|
866
912
|
function seedHeld(at) {
|
|
867
913
|
return Object.entries(at).every(([room, version]) => (roomsListed && !rooms.has(room)) || seedRoomHeld(room, version));
|
|
@@ -982,83 +1028,12 @@ export function composeRuntime(options) {
|
|
|
982
1028
|
const held = answered.cover;
|
|
983
1029
|
return { ...answered.rows, coverage: held.coverage };
|
|
984
1030
|
}
|
|
985
|
-
async function everyRow(plan) {
|
|
986
|
-
const window = { ...plan, limit: MAXIMUM_WINDOW, after: null, total: false };
|
|
987
|
-
const first = await rowsQuery(window);
|
|
988
|
-
if ('pending' in first || first.next === undefined)
|
|
989
|
-
return first;
|
|
990
|
-
const documents = 'documents' in first ? [...first.documents] : [];
|
|
991
|
-
const groups = 'groups' in first ? [...first.groups] : [];
|
|
992
|
-
for (let next = first.next; next !== undefined;) {
|
|
993
|
-
const values = JSON.parse(next);
|
|
994
|
-
const page = await rowsQuery({
|
|
995
|
-
...window,
|
|
996
|
-
after: { text: next, values: values.length === 0 ? null : values },
|
|
997
|
-
});
|
|
998
|
-
if ('pending' in page)
|
|
999
|
-
return page;
|
|
1000
|
-
if ('documents' in page)
|
|
1001
|
-
documents.push(...page.documents);
|
|
1002
|
-
else
|
|
1003
|
-
groups.push(...page.groups);
|
|
1004
|
-
if (JSON.stringify(page.coverage) !== JSON.stringify(first.coverage))
|
|
1005
|
-
return everyRow(plan);
|
|
1006
|
-
next = page.next;
|
|
1007
|
-
}
|
|
1008
|
-
return 'documents' in first
|
|
1009
|
-
? { documents, coverage: first.coverage }
|
|
1010
|
-
: { groups, coverage: first.coverage };
|
|
1011
|
-
}
|
|
1012
|
-
async function indexConversation(conversation) {
|
|
1013
|
-
searchIndex.indexMessages(conversation, [], false);
|
|
1014
|
-
let after = null;
|
|
1015
|
-
for (;;) {
|
|
1016
|
-
const batch = await durably((local) => local.messagesAfter(conversation, after));
|
|
1017
|
-
if (closed)
|
|
1018
|
-
return;
|
|
1019
|
-
searchIndex.indexMessages(conversation, batch.map(({ identity, text }) => ({ conversation, identity, text })), batch.length === 0);
|
|
1020
|
-
if (batch.length === 0)
|
|
1021
|
-
return;
|
|
1022
|
-
after = batch[batch.length - 1].identity;
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
function indexNext() {
|
|
1026
|
-
const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator) && !unindexable.has(locator));
|
|
1027
|
-
if (room !== undefined) {
|
|
1028
|
-
roomsToIndex.delete(room);
|
|
1029
|
-
return () => prepared.has(room)
|
|
1030
|
-
? searchIndex.indexRoom(room).catch((error) => unbuilt(room, error))
|
|
1031
|
-
: undefined;
|
|
1032
|
-
}
|
|
1033
|
-
const conversation = [...conversationsToIndex].find((one) => !searchIndex.conversations().has(one));
|
|
1034
|
-
if (conversation === undefined)
|
|
1035
|
-
return null;
|
|
1036
|
-
conversationsToIndex.delete(conversation);
|
|
1037
|
-
return () => indexConversation(conversation);
|
|
1038
|
-
}
|
|
1039
|
-
function index(rooms, named) {
|
|
1040
|
-
for (const room of rooms)
|
|
1041
|
-
roomsToIndex.add(room);
|
|
1042
|
-
for (const conversation of named)
|
|
1043
|
-
conversationsToIndex.add(conversation);
|
|
1044
|
-
if (indexing)
|
|
1045
|
-
return;
|
|
1046
|
-
indexing = true;
|
|
1047
|
-
void (async () => {
|
|
1048
|
-
try {
|
|
1049
|
-
for (let unit = indexNext(); unit !== null && !closed; unit = indexNext()) {
|
|
1050
|
-
await yielded();
|
|
1051
|
-
await unit();
|
|
1052
|
-
queueReadiness();
|
|
1053
|
-
}
|
|
1054
|
-
}
|
|
1055
|
-
finally {
|
|
1056
|
-
indexing = false;
|
|
1057
|
-
}
|
|
1058
|
-
})().catch(() => { });
|
|
1059
|
-
}
|
|
1060
1031
|
function publishChangeSet(set) {
|
|
1061
1032
|
applyChangeSet(set);
|
|
1033
|
+
for (const { locator, path } of set.documents) {
|
|
1034
|
+
if (path !== null)
|
|
1035
|
+
attachedFiles.get(locator)?.delete(path);
|
|
1036
|
+
}
|
|
1062
1037
|
if (set.rooms.length > 0 ||
|
|
1063
1038
|
[...new Set(set.documents.map(({ locator }) => locator))].some((locator) => rooms.has(locator) && counted.get(locator) !== documentsIn(locator)))
|
|
1064
1039
|
emitRooms();
|
|
@@ -1077,6 +1052,8 @@ export function composeRuntime(options) {
|
|
|
1077
1052
|
}
|
|
1078
1053
|
async function refuse(reason, provider = null) {
|
|
1079
1054
|
discoveryPaused = true;
|
|
1055
|
+
inFlight.abort();
|
|
1056
|
+
inFlight = new AbortController();
|
|
1080
1057
|
try {
|
|
1081
1058
|
if (reason === 'revoked-authorization') {
|
|
1082
1059
|
await exclusive(async () => {
|
|
@@ -1084,27 +1061,24 @@ export function composeRuntime(options) {
|
|
|
1084
1061
|
const records = [...replicas].map(([locator, held]) => ({
|
|
1085
1062
|
locator,
|
|
1086
1063
|
held,
|
|
1087
|
-
record: { ...held.record, model: revoked
|
|
1064
|
+
record: { ...held.record, model: revoked },
|
|
1088
1065
|
}));
|
|
1066
|
+
let answer = null;
|
|
1089
1067
|
try {
|
|
1090
1068
|
const local = await openLocal();
|
|
1091
|
-
await local.commit({
|
|
1069
|
+
answer = await local.commit({
|
|
1092
1070
|
rooms: records.map(({ locator, record }) => ({
|
|
1093
1071
|
locator,
|
|
1094
1072
|
key: record.key,
|
|
1095
1073
|
record: storedRecord(record),
|
|
1096
|
-
|
|
1074
|
+
composed: composedOf(composed(record)),
|
|
1097
1075
|
})),
|
|
1098
1076
|
});
|
|
1099
1077
|
}
|
|
1100
1078
|
catch {
|
|
1101
1079
|
journal.record('model revocation persistence failed');
|
|
1102
1080
|
}
|
|
1103
|
-
const keys = new Set(models.keys());
|
|
1104
|
-
for (const locator of rooms.keys()) {
|
|
1105
|
-
keys.add(modelKey(locator, 'collection'));
|
|
1106
|
-
keys.add(modelKey(locator, 'typed-selection'));
|
|
1107
|
-
}
|
|
1081
|
+
const keys = new Set([...models.keys(), ...rooms.keys()]);
|
|
1108
1082
|
for (const key of keys) {
|
|
1109
1083
|
invalidModels.add(key);
|
|
1110
1084
|
models.set(key, revoked);
|
|
@@ -1114,8 +1088,8 @@ export function composeRuntime(options) {
|
|
|
1114
1088
|
for (const { locator, held, record } of records) {
|
|
1115
1089
|
if (replicas.get(locator) !== held)
|
|
1116
1090
|
continue;
|
|
1117
|
-
|
|
1118
|
-
replicas.set(locator, { ...held, record });
|
|
1091
|
+
const indexed = answer === null ? held.indexed : committedRoom(locator, answer).indexed;
|
|
1092
|
+
replicas.set(locator, { ...held, record, indexed });
|
|
1119
1093
|
}
|
|
1120
1094
|
for (const locator of [...rooms.keys()]) {
|
|
1121
1095
|
const room = rooms.get(locator);
|
|
@@ -1159,15 +1133,15 @@ export function composeRuntime(options) {
|
|
|
1159
1133
|
? null
|
|
1160
1134
|
: {
|
|
1161
1135
|
roots: selection.roots,
|
|
1162
|
-
model: modelTypes(
|
|
1136
|
+
model: modelTypes(builtIn),
|
|
1163
1137
|
},
|
|
1164
1138
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
1165
1139
|
roomVersion: replicas.get(room.locator)?.record.roomVersion ?? null,
|
|
1166
1140
|
synchronizationAvailable,
|
|
1167
|
-
refusal: scopedRefusals.get(room.locator) ??
|
|
1141
|
+
refusal: scopedRefusals.get(room.locator) ?? null,
|
|
1168
1142
|
});
|
|
1169
1143
|
}
|
|
1170
|
-
async function applyDiscovery(found, started,
|
|
1144
|
+
async function applyDiscovery(found, started, left) {
|
|
1171
1145
|
const lockedBefore = lockedRooms();
|
|
1172
1146
|
for (const room of found) {
|
|
1173
1147
|
const fence = fences.get(room.locator);
|
|
@@ -1181,18 +1155,11 @@ export function composeRuntime(options) {
|
|
|
1181
1155
|
const listedLocators = new Set(listed.map((room) => room.locator));
|
|
1182
1156
|
for (const locator of listedLocators)
|
|
1183
1157
|
scopedRefusals.delete(locator);
|
|
1184
|
-
if (
|
|
1158
|
+
if (left === undefined)
|
|
1185
1159
|
discovered.clear();
|
|
1186
|
-
else
|
|
1187
|
-
for (const locator of covered)
|
|
1188
|
-
discovered.delete(locator);
|
|
1189
1160
|
for (const room of listed)
|
|
1190
1161
|
discovered.set(room.locator, room);
|
|
1191
|
-
|
|
1192
|
-
if (options.adapter.exhaustiveDiscovery === true)
|
|
1193
|
-
await loseRooms(unlisted);
|
|
1194
|
-
else
|
|
1195
|
-
unlistedKept(unlisted);
|
|
1162
|
+
await loseRooms(left ?? unlistedHeld(new Set(found.map((room) => room.locator))));
|
|
1196
1163
|
for (const room of listed) {
|
|
1197
1164
|
const held = replicas.get(room.locator)?.record;
|
|
1198
1165
|
if (held !== undefined && held.replica.selection !== room.replica.selection) {
|
|
@@ -1223,15 +1190,6 @@ export function composeRuntime(options) {
|
|
|
1223
1190
|
]),
|
|
1224
1191
|
].filter((locator) => !listed.has(locator));
|
|
1225
1192
|
}
|
|
1226
|
-
function unlistedKept(unlisted) {
|
|
1227
|
-
for (const locator of unlisted) {
|
|
1228
|
-
const held = replicas.get(locator)?.record;
|
|
1229
|
-
if (held !== undefined)
|
|
1230
|
-
list(held, false);
|
|
1231
|
-
else
|
|
1232
|
-
rooms.delete(locator);
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
1193
|
function fence(locators) {
|
|
1236
1194
|
const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
|
|
1237
1195
|
const removed = fresh.map((locator) => ({
|
|
@@ -1242,36 +1200,33 @@ export function composeRuntime(options) {
|
|
|
1242
1200
|
fences.set(locator, { at: discoveries, erased: false });
|
|
1243
1201
|
lastFenced.set(locator, ++fencings);
|
|
1244
1202
|
confirmed.delete(locator);
|
|
1245
|
-
for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
|
|
1203
|
+
for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []) {
|
|
1246
1204
|
pendings.delete(pendingKey(locator, batch.id));
|
|
1205
|
+
landedAhead.delete(pendingKey(locator, batch.id));
|
|
1206
|
+
}
|
|
1247
1207
|
replicas.delete(locator);
|
|
1208
|
+
highest.delete(locator);
|
|
1209
|
+
beyondHorizon.delete(locator);
|
|
1248
1210
|
queuesWithoutReplica.delete(locator);
|
|
1249
1211
|
rooms.delete(locator);
|
|
1250
1212
|
roomDrafts.delete(locator);
|
|
1251
|
-
|
|
1213
|
+
attachedFiles.delete(locator);
|
|
1214
|
+
untransferred(locator);
|
|
1215
|
+
startedOver.delete(locator);
|
|
1252
1216
|
catching.delete(locator);
|
|
1253
1217
|
discovered.delete(locator);
|
|
1254
|
-
announced.delete(locator);
|
|
1255
1218
|
unreadable.delete(locator);
|
|
1256
1219
|
stale.delete(locator);
|
|
1257
1220
|
repairable.delete(locator);
|
|
1258
|
-
|
|
1259
|
-
paused.delete(locator);
|
|
1260
|
-
prepared.delete(locator);
|
|
1221
|
+
unpreparable.delete(locator);
|
|
1261
1222
|
scopedRefusals.delete(locator);
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
const key = modelKey(locator, attached);
|
|
1266
|
-
models.delete(key);
|
|
1267
|
-
liveModels.delete(key);
|
|
1268
|
-
invalidModels.delete(key);
|
|
1269
|
-
}
|
|
1223
|
+
models.delete(locator);
|
|
1224
|
+
liveModels.delete(locator);
|
|
1225
|
+
invalidModels.delete(locator);
|
|
1270
1226
|
journal.record(`room lost ${locator}`);
|
|
1271
1227
|
}
|
|
1272
1228
|
if (fresh.length === 0)
|
|
1273
1229
|
return fresh;
|
|
1274
|
-
searchIndex.forget(fresh);
|
|
1275
1230
|
publishChangeSet(mergeChangeSets(removed.map(({ locator, drafts }) => changeSet(locator, { documents: null, room: true, queue: true, drafts }))));
|
|
1276
1231
|
liveQueries.recover();
|
|
1277
1232
|
moveTo({ ...status, ...attention() });
|
|
@@ -1315,7 +1270,13 @@ export function composeRuntime(options) {
|
|
|
1315
1270
|
});
|
|
1316
1271
|
await eraseFenced(fresh);
|
|
1317
1272
|
}
|
|
1318
|
-
function
|
|
1273
|
+
function retyped(persisted) {
|
|
1274
|
+
if (persisted.status === 'unavailable')
|
|
1275
|
+
return persisted;
|
|
1276
|
+
return { ...persisted, types: schemaTypes(persisted.snapshot.schema, roomTypes) };
|
|
1277
|
+
}
|
|
1278
|
+
function restored(key, held) {
|
|
1279
|
+
const persisted = retyped(held);
|
|
1319
1280
|
const prior = models.get(key);
|
|
1320
1281
|
if (prior !== undefined &&
|
|
1321
1282
|
prior.status !== 'unavailable' &&
|
|
@@ -1346,8 +1307,9 @@ export function composeRuntime(options) {
|
|
|
1346
1307
|
const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
|
|
1347
1308
|
targets = held.targets;
|
|
1348
1309
|
transfers.clear();
|
|
1349
|
-
|
|
1350
|
-
|
|
1310
|
+
arrivals.clear();
|
|
1311
|
+
streaming.clear();
|
|
1312
|
+
startedOver.clear();
|
|
1351
1313
|
limits = held.providerLimits;
|
|
1352
1314
|
capabilities = held.capabilities;
|
|
1353
1315
|
member = held.member;
|
|
@@ -1357,15 +1319,16 @@ export function composeRuntime(options) {
|
|
|
1357
1319
|
presentations.clear();
|
|
1358
1320
|
for (const record of held.presentation)
|
|
1359
1321
|
presentations.set(record.app, record.value);
|
|
1360
|
-
conversations.hydrate(heldConversations, chatDrafts);
|
|
1322
|
+
conversations.hydrate(heldConversations, chatDrafts, held.conversationList, held.conversationCursor);
|
|
1361
1323
|
pendings.clear();
|
|
1324
|
+
landedAhead.clear();
|
|
1362
1325
|
for (const queue of queues) {
|
|
1363
1326
|
for (const { batch, state } of queue.pending) {
|
|
1364
1327
|
pendings.set(pendingKey(queue.locator, batch.id), state);
|
|
1365
1328
|
}
|
|
1366
1329
|
}
|
|
1367
1330
|
replicas.clear();
|
|
1368
|
-
|
|
1331
|
+
reopened.clear();
|
|
1369
1332
|
queuesWithoutReplica.clear();
|
|
1370
1333
|
for (const [locator, queue] of queuedBatches) {
|
|
1371
1334
|
if (!held.rooms.has(locator))
|
|
@@ -1374,39 +1337,40 @@ export function composeRuntime(options) {
|
|
|
1374
1337
|
records.forEach((stored, index) => {
|
|
1375
1338
|
if (fences.has(stored.locator))
|
|
1376
1339
|
return;
|
|
1377
|
-
const
|
|
1378
|
-
|
|
1379
|
-
const record = { ...stored, model, selectionModel };
|
|
1340
|
+
const record = { ...stored, model: restored(stored.locator, stored.model) };
|
|
1341
|
+
reopened.add(record.locator);
|
|
1380
1342
|
replicas.set(record.locator, {
|
|
1381
1343
|
record,
|
|
1382
1344
|
queue: queuedBatches.get(record.locator) ?? [],
|
|
1383
1345
|
overlaid: held.overlaid.get(record.key) ?? new Set(),
|
|
1384
1346
|
count: counts[index] ?? 0,
|
|
1385
1347
|
confirmed: 0,
|
|
1348
|
+
indexed: !held.unindexed.has(record.key),
|
|
1386
1349
|
});
|
|
1387
|
-
if (!held.unprepared.has(record.key))
|
|
1388
|
-
prepared.add(record.locator);
|
|
1389
1350
|
const existing = rooms.get(record.locator);
|
|
1390
1351
|
if (existing === undefined)
|
|
1391
|
-
list(record,
|
|
1352
|
+
list(record, record.roomVersion !== null);
|
|
1392
1353
|
else {
|
|
1393
1354
|
rooms.set(record.locator, {
|
|
1394
1355
|
...existing,
|
|
1395
1356
|
roomVersion: record.roomVersion,
|
|
1396
1357
|
operations: record.operations,
|
|
1397
1358
|
replica: record.replica,
|
|
1398
|
-
model: modelTypes(model),
|
|
1359
|
+
model: modelTypes(record.model),
|
|
1399
1360
|
typedSelection: record.typedSelection === null
|
|
1400
1361
|
? null
|
|
1401
|
-
: {
|
|
1402
|
-
roots: record.typedSelection.roots,
|
|
1403
|
-
model: modelTypes(selectionModel),
|
|
1404
|
-
},
|
|
1362
|
+
: { roots: record.typedSelection.roots, model: modelTypes(builtIn) },
|
|
1405
1363
|
});
|
|
1406
1364
|
}
|
|
1407
1365
|
});
|
|
1408
|
-
|
|
1409
|
-
|
|
1366
|
+
try {
|
|
1367
|
+
for (const key of held.orphans)
|
|
1368
|
+
await opened.release(key);
|
|
1369
|
+
}
|
|
1370
|
+
catch (error) {
|
|
1371
|
+
if (!refusedForSpace(error))
|
|
1372
|
+
throw error;
|
|
1373
|
+
}
|
|
1410
1374
|
if (held.erasing || held.orphans.length > 0)
|
|
1411
1375
|
queueMicrotask(sweep);
|
|
1412
1376
|
return {
|
|
@@ -1431,8 +1395,7 @@ export function composeRuntime(options) {
|
|
|
1431
1395
|
async function open() {
|
|
1432
1396
|
if (closed)
|
|
1433
1397
|
throw new Error('runtime closed');
|
|
1434
|
-
const opened =
|
|
1435
|
-
store = opened;
|
|
1398
|
+
const opened = openRuntimeStore(isFenced, database);
|
|
1436
1399
|
const interrupted = await opened.fences();
|
|
1437
1400
|
for (const locator of interrupted) {
|
|
1438
1401
|
fences.set(locator, { at: discoveries, erased: false });
|
|
@@ -1478,24 +1441,41 @@ export function composeRuntime(options) {
|
|
|
1478
1441
|
return opening;
|
|
1479
1442
|
}
|
|
1480
1443
|
function exclusive(work) {
|
|
1444
|
+
gathering = null;
|
|
1481
1445
|
const next = mutations.then(work, work);
|
|
1482
1446
|
mutations = next.then(() => undefined, () => undefined);
|
|
1483
1447
|
return next;
|
|
1484
1448
|
}
|
|
1449
|
+
function gathered(key, input, merge, work) {
|
|
1450
|
+
const tail = gathering;
|
|
1451
|
+
if (tail !== null && tail.key === key) {
|
|
1452
|
+
const merged = merge(tail.input, input);
|
|
1453
|
+
if (merged !== null) {
|
|
1454
|
+
tail.input = merged;
|
|
1455
|
+
return tail.done;
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
const entry = { key, input, done: Promise.resolve() };
|
|
1459
|
+
entry.done = exclusive(() => {
|
|
1460
|
+
if (gathering === entry)
|
|
1461
|
+
gathering = null;
|
|
1462
|
+
return work(entry.input);
|
|
1463
|
+
});
|
|
1464
|
+
gathering = entry;
|
|
1465
|
+
return entry.done;
|
|
1466
|
+
}
|
|
1485
1467
|
function withdrawable(locator) {
|
|
1486
1468
|
return (batch) => {
|
|
1487
1469
|
const state = pendings.get(pendingKey(locator, batch.id));
|
|
1488
1470
|
return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
|
|
1489
1471
|
};
|
|
1490
1472
|
}
|
|
1491
|
-
const REWRITE_LINKS = ['field', 'body'];
|
|
1492
|
-
const CHECKED_LINKS = ['field', 'section'];
|
|
1493
1473
|
async function sliceAt(record, view, queue, extra = [], moved = []) {
|
|
1494
1474
|
const local = await openLocal();
|
|
1495
1475
|
const needed = new Set([...touchedBy(queue), ...extra]);
|
|
1496
1476
|
const movedPaths = [...new Set([...movedBy(queue, record.locator), ...moved])];
|
|
1497
1477
|
if (movedPaths.length > 0) {
|
|
1498
|
-
for (const { path } of await local.referrers(record.key, view, movedPaths
|
|
1478
|
+
for (const { path } of await local.referrers(record.key, view, movedPaths)) {
|
|
1499
1479
|
needed.add(path);
|
|
1500
1480
|
}
|
|
1501
1481
|
}
|
|
@@ -1563,26 +1543,14 @@ export function composeRuntime(options) {
|
|
|
1563
1543
|
},
|
|
1564
1544
|
};
|
|
1565
1545
|
}
|
|
1566
|
-
function
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
fingerprint(compositionOf(before)) === fingerprint(compositionOf(after))) {
|
|
1570
|
-
return;
|
|
1571
|
-
}
|
|
1572
|
-
if (before !== undefined && before.key === after.key)
|
|
1573
|
-
prepared.delete(locator);
|
|
1574
|
-
}
|
|
1575
|
-
function committedRoom(locator, record, answer) {
|
|
1576
|
-
if (answer.prepared[locator] === false)
|
|
1577
|
-
prepared.delete(locator);
|
|
1578
|
-
else if (answer.prepared[locator] === true)
|
|
1579
|
-
prepared.add(locator);
|
|
1580
|
-
if (!prepared.has(locator))
|
|
1546
|
+
function committedRoom(locator, answer) {
|
|
1547
|
+
const indexed = answer.indexed[locator] ?? replicas.get(locator)?.indexed ?? false;
|
|
1548
|
+
if (!indexed)
|
|
1581
1549
|
prepare([locator]);
|
|
1582
|
-
return { count: answer.counts[locator] ?? replicas.get(locator)?.count ?? 0 };
|
|
1550
|
+
return { count: answer.counts[locator] ?? replicas.get(locator)?.count ?? 0, indexed };
|
|
1583
1551
|
}
|
|
1584
1552
|
function replayed(record, confirmedLayer, queue) {
|
|
1585
|
-
return stepped(replaying(confirmedLayer, queue, record.locator,
|
|
1553
|
+
return stepped(replaying(confirmedLayer, queue, record.locator, composed(record)));
|
|
1586
1554
|
}
|
|
1587
1555
|
async function overlayDiff(record, before, visible, touched) {
|
|
1588
1556
|
const kept = before !== undefined && before.record.key === record.key
|
|
@@ -1602,8 +1570,6 @@ export function composeRuntime(options) {
|
|
|
1602
1570
|
function unpairedIn(locator, batch) {
|
|
1603
1571
|
if (!queueIn(locator).some((held) => held.id === batch.id))
|
|
1604
1572
|
return false;
|
|
1605
|
-
if (pendings.get(pendingKey(locator, batch.id))?.accepted !== undefined)
|
|
1606
|
-
return false;
|
|
1607
1573
|
return !leavingIn(batch, locator) || withdrawable(locator)(batch);
|
|
1608
1574
|
}
|
|
1609
1575
|
async function together(local, settlements) {
|
|
@@ -1645,11 +1611,17 @@ export function composeRuntime(options) {
|
|
|
1645
1611
|
const confirmedLayer = overrides === undefined
|
|
1646
1612
|
? read
|
|
1647
1613
|
: await stepped(slicingWith(read, overrides.set, overrides.removed));
|
|
1648
|
-
const composition =
|
|
1649
|
-
const
|
|
1650
|
-
|
|
1614
|
+
const composition = composed(record);
|
|
1615
|
+
const shown = queue.filter((batch) => {
|
|
1616
|
+
const held = landedAhead.get(pendingKey(locator, batch.id));
|
|
1617
|
+
return (held !== undefined &&
|
|
1618
|
+
Object.entries(held).some(([path, revision]) => confirmedLayer.revisions[path] !== revision));
|
|
1619
|
+
});
|
|
1620
|
+
const unshown = queue.filter((batch) => !shown.includes(batch));
|
|
1621
|
+
const { withdrawn, kept } = await stepped(standing(confirmedLayer, unshown, withdrawable(locator), localSaves, locator, composition));
|
|
1622
|
+
const founded = kept.filter((batch) => !unshown.includes(batch));
|
|
1651
1623
|
const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
|
|
1652
|
-
const unqueued =
|
|
1624
|
+
const unqueued = shown.map((batch) => ({ batch, landed: true }));
|
|
1653
1625
|
for (const batch of withdrawn) {
|
|
1654
1626
|
unqueued.push({ batch, landed: await landedWhole(locator, batch, confirmedLayer) });
|
|
1655
1627
|
}
|
|
@@ -1688,6 +1660,7 @@ export function composeRuntime(options) {
|
|
|
1688
1660
|
key: record.key,
|
|
1689
1661
|
record: storedRecord(record),
|
|
1690
1662
|
roots: record.typedSelection?.roots ?? [],
|
|
1663
|
+
composed: composedOf(composition),
|
|
1691
1664
|
...settling.commit,
|
|
1692
1665
|
...(overrides === undefined
|
|
1693
1666
|
? {}
|
|
@@ -1713,8 +1686,10 @@ export function composeRuntime(options) {
|
|
|
1713
1686
|
commit,
|
|
1714
1687
|
apply: (answer) => {
|
|
1715
1688
|
for (const batch of previousQueue) {
|
|
1716
|
-
if (
|
|
1717
|
-
|
|
1689
|
+
if (staying.has(batch.id))
|
|
1690
|
+
continue;
|
|
1691
|
+
pendings.delete(pendingKey(locator, batch.id));
|
|
1692
|
+
landedAhead.delete(pendingKey(locator, batch.id));
|
|
1718
1693
|
}
|
|
1719
1694
|
for (const draft of written.values())
|
|
1720
1695
|
rememberRoomDraft(draft);
|
|
@@ -1729,14 +1704,14 @@ export function composeRuntime(options) {
|
|
|
1729
1704
|
const confirmedChanged = settling.commit?.published !== undefined ||
|
|
1730
1705
|
settling.commit?.refreshed === true ||
|
|
1731
1706
|
overrides !== undefined;
|
|
1732
|
-
|
|
1733
|
-
const { count } = committedRoom(locator, record, answer);
|
|
1707
|
+
const { count, indexed } = committedRoom(locator, answer);
|
|
1734
1708
|
replicas.set(locator, {
|
|
1735
1709
|
record,
|
|
1736
1710
|
queue: kept,
|
|
1737
1711
|
overlaid: touched,
|
|
1738
1712
|
count,
|
|
1739
1713
|
confirmed: (before?.confirmed ?? 0) + (confirmedChanged ? 1 : 0),
|
|
1714
|
+
indexed,
|
|
1740
1715
|
});
|
|
1741
1716
|
return mergeChangeSets([
|
|
1742
1717
|
changeSet(locator, {
|
|
@@ -1751,110 +1726,101 @@ export function composeRuntime(options) {
|
|
|
1751
1726
|
};
|
|
1752
1727
|
}
|
|
1753
1728
|
function adoptModels(locator, record) {
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
if (model.status === 'current')
|
|
1762
|
-
liveModels.set(key, JSON.stringify(model.snapshot));
|
|
1763
|
-
else if (model.status === 'unavailable')
|
|
1764
|
-
liveModels.delete(key);
|
|
1765
|
-
}
|
|
1729
|
+
const model = record.model;
|
|
1730
|
+
models.set(locator, model);
|
|
1731
|
+
invalidModels.delete(locator);
|
|
1732
|
+
if (model.status === 'current')
|
|
1733
|
+
liveModels.set(locator, JSON.stringify(model.snapshot));
|
|
1734
|
+
else if (model.status === 'unavailable')
|
|
1735
|
+
liveModels.delete(locator);
|
|
1766
1736
|
}
|
|
1767
1737
|
function remodel(locator, answers, held) {
|
|
1768
|
-
const
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
journal.record(`model invalid ${locator} ${attached} ${refused.identity} ${refused.rule}${where}`);
|
|
1775
|
-
}
|
|
1776
|
-
models.set(modelKey(locator, attached), state);
|
|
1777
|
-
return state;
|
|
1778
|
-
};
|
|
1779
|
-
return {
|
|
1780
|
-
collection: taken(answers?.collection, held.collection, 'collection'),
|
|
1781
|
-
selection: taken(answers?.selection, held.selection, 'typed-selection'),
|
|
1782
|
-
};
|
|
1738
|
+
const answer = answers?.collection;
|
|
1739
|
+
if (answer === undefined)
|
|
1740
|
+
return held;
|
|
1741
|
+
const state = attachedModel(answer, roomTypes);
|
|
1742
|
+
models.set(locator, state);
|
|
1743
|
+
return { collection: state };
|
|
1783
1744
|
}
|
|
1784
|
-
function publish(local, room,
|
|
1785
|
-
|
|
1745
|
+
function publish(local, room, reached, landing, live) {
|
|
1746
|
+
const landed = exclusive(async () => {
|
|
1786
1747
|
if (!live()) {
|
|
1787
1748
|
caughtUpTo(room.locator);
|
|
1788
1749
|
return true;
|
|
1789
1750
|
}
|
|
1790
1751
|
const prior = replicas.get(room.locator);
|
|
1791
|
-
if (landing.
|
|
1792
|
-
(
|
|
1793
|
-
|
|
1752
|
+
if (!stands(room.locator, landing.from)) {
|
|
1753
|
+
if (landing.kind === 'refreshed') {
|
|
1754
|
+
await local.unrefresh(reached.key);
|
|
1755
|
+
return false;
|
|
1756
|
+
}
|
|
1757
|
+
caughtUpTo(room.locator);
|
|
1758
|
+
await abandoned(local, room.locator);
|
|
1794
1759
|
return false;
|
|
1795
1760
|
}
|
|
1761
|
+
const record = landing.kind === 'refreshed' && prior !== undefined
|
|
1762
|
+
? {
|
|
1763
|
+
...prior.record,
|
|
1764
|
+
roomVersion: reached.roomVersion,
|
|
1765
|
+
...(landing.remodelled ? { model: reached.model } : {}),
|
|
1766
|
+
}
|
|
1767
|
+
: reached;
|
|
1796
1768
|
adoptModels(room.locator, record);
|
|
1797
1769
|
const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
|
|
1798
|
-
const roomVersion = record.roomVersion;
|
|
1799
|
-
const accepted = queue.filter((batch) => {
|
|
1800
|
-
const at = pendings.get(pendingKey(room.locator, batch.id))?.accepted;
|
|
1801
|
-
return at !== undefined && options.adapter.reached(room.locator, roomVersion, at);
|
|
1802
|
-
});
|
|
1803
1770
|
let set;
|
|
1804
1771
|
try {
|
|
1805
|
-
set = await settle(local, room.locator, prior, record, queue
|
|
1772
|
+
set = await settle(local, room.locator, prior, record, queue, landing.kind === 'refreshed'
|
|
1806
1773
|
? {
|
|
1807
1774
|
room: true,
|
|
1808
1775
|
view: 'refreshed',
|
|
1809
|
-
commit: { refreshed: true
|
|
1776
|
+
commit: { refreshed: true },
|
|
1810
1777
|
}
|
|
1811
1778
|
: {
|
|
1812
1779
|
room: true,
|
|
1813
1780
|
view: landing.refreshed ? 'refreshed' : 'confirmed',
|
|
1814
1781
|
commit: {
|
|
1815
1782
|
published: { from: prior?.record.key ?? null },
|
|
1816
|
-
composition: compositionOf(record),
|
|
1817
1783
|
...(landing.refreshed ? { refreshed: true } : {}),
|
|
1818
1784
|
},
|
|
1819
1785
|
});
|
|
1820
1786
|
}
|
|
1821
1787
|
catch (error) {
|
|
1822
|
-
if (!
|
|
1788
|
+
if (!refusedForSpace(error))
|
|
1823
1789
|
throw error;
|
|
1824
|
-
await exceededOn(room.locator, null);
|
|
1825
1790
|
caughtUpTo(room.locator);
|
|
1826
1791
|
return true;
|
|
1827
1792
|
}
|
|
1828
1793
|
if (landing.kind === 'bootstrapped') {
|
|
1829
|
-
|
|
1794
|
+
highest.delete(room.locator);
|
|
1795
|
+
beyondHorizon.delete(room.locator);
|
|
1796
|
+
untransferred(room.locator);
|
|
1797
|
+
startedOver.delete(room.locator);
|
|
1830
1798
|
catching.delete(room.locator);
|
|
1831
|
-
|
|
1799
|
+
if (prior !== undefined && prior.record.key !== record.key)
|
|
1800
|
+
sweep();
|
|
1801
|
+
name(room.locator);
|
|
1832
1802
|
}
|
|
1833
|
-
for (const batch of accepted)
|
|
1834
|
-
pendings.delete(pendingKey(room.locator, batch.id));
|
|
1835
1803
|
stale.delete(room.locator);
|
|
1836
|
-
|
|
1804
|
+
unpreparable.delete(room.locator);
|
|
1837
1805
|
if (unreadable.delete(room.locator))
|
|
1838
1806
|
scopedRefusals.delete(room.locator);
|
|
1839
|
-
sourceRefusals.delete(room.locator);
|
|
1840
|
-
declarationRefusals.delete(room.locator);
|
|
1841
1807
|
list(room, true);
|
|
1842
1808
|
publishChangeSet(set);
|
|
1843
1809
|
await unpair(local);
|
|
1844
1810
|
moveTo({ ...status, ...attention() });
|
|
1845
|
-
|
|
1846
|
-
if (accepted.length > 0 && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
|
|
1811
|
+
if (landing.kind === 'bootstrapped' && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
|
|
1847
1812
|
submitLater(room.locator);
|
|
1848
1813
|
}
|
|
1849
1814
|
return true;
|
|
1850
1815
|
});
|
|
1816
|
+
return landing.kind === 'refreshed' ? landed : landed.finally(() => installed(room.locator));
|
|
1851
1817
|
}
|
|
1852
1818
|
async function savedRoom(locator) {
|
|
1853
1819
|
const local = await openLocal();
|
|
1854
1820
|
const replica = replicas.get(locator);
|
|
1855
1821
|
const room = rooms.get(locator);
|
|
1856
1822
|
if (replica === undefined || room === undefined) {
|
|
1857
|
-
throw runtimeError('room
|
|
1823
|
+
throw runtimeError('unknown-room', `Unknown Room ${locator}`);
|
|
1858
1824
|
}
|
|
1859
1825
|
return { local, replica, room };
|
|
1860
1826
|
}
|
|
@@ -1872,12 +1838,7 @@ export function composeRuntime(options) {
|
|
|
1872
1838
|
return committed(locator, target, saved, new Set(saved.map(({ path }) => path)), writtenBy);
|
|
1873
1839
|
}
|
|
1874
1840
|
async function saveParts(locator, named, writtenBy) {
|
|
1875
|
-
const read = named
|
|
1876
|
-
if (read.some(({ path }, at) => path === '' ||
|
|
1877
|
-
normalize(path) !== path ||
|
|
1878
|
-
read.findIndex((one) => one.path === path) !== at)) {
|
|
1879
|
-
throw runtimeError('room-draft-malformed');
|
|
1880
|
-
}
|
|
1841
|
+
const read = readSavedParts(named);
|
|
1881
1842
|
const target = await savedRoom(locator);
|
|
1882
1843
|
const saved = [];
|
|
1883
1844
|
const files = [];
|
|
@@ -1886,7 +1847,7 @@ export function composeRuntime(options) {
|
|
|
1886
1847
|
for (const { path, parts } of read) {
|
|
1887
1848
|
const file = parts.file === undefined
|
|
1888
1849
|
? undefined
|
|
1889
|
-
: await heldFile(parts.file.blob, binaryBase(visible, path));
|
|
1850
|
+
: await heldFile(parts.file.blob, parts.file.base === undefined ? binaryBase(visible, path) : parts.file.base);
|
|
1890
1851
|
if (parts.file !== undefined && file !== undefined) {
|
|
1891
1852
|
files.push({ key: file.localId, blob: parts.file.blob });
|
|
1892
1853
|
}
|
|
@@ -1920,14 +1881,15 @@ export function composeRuntime(options) {
|
|
|
1920
1881
|
const removed = changes.flatMap((change) => (change.kind === 'remove' ? [change.path] : []));
|
|
1921
1882
|
const referring = removed.length === 0
|
|
1922
1883
|
? []
|
|
1923
|
-
: (await local.referrers(record.key, 'confirmed', removed
|
|
1924
|
-
const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...referring], moved);
|
|
1884
|
+
: (await local.referrers(record.key, 'confirmed', removed)).map(({ path }) => path);
|
|
1885
|
+
const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...paths.flatMap(ancestors), ...referring], moved);
|
|
1925
1886
|
const { visible } = await replayed(record, confirmedLayer, replica.queue);
|
|
1926
|
-
const composition =
|
|
1887
|
+
const composition = composed(record);
|
|
1927
1888
|
return {
|
|
1928
1889
|
record,
|
|
1929
1890
|
confirmed: confirmedLayer,
|
|
1930
1891
|
visible,
|
|
1892
|
+
folders: new Set(await local.folders(record.key, 'visible', paths)),
|
|
1931
1893
|
*visibleOver(queue) {
|
|
1932
1894
|
return (yield* replaying(confirmedLayer, queue, record.locator, composition)).visible;
|
|
1933
1895
|
},
|
|
@@ -1939,13 +1901,7 @@ export function composeRuntime(options) {
|
|
|
1939
1901
|
return false;
|
|
1940
1902
|
const local = await openLocal();
|
|
1941
1903
|
const roots = record.typedSelection?.roots ?? [];
|
|
1942
|
-
|
|
1943
|
-
(await local.typedWithin(record.key, 'visible', roots, false))) {
|
|
1944
|
-
return true;
|
|
1945
|
-
}
|
|
1946
|
-
return (composition.selection !== null &&
|
|
1947
|
-
composition.selection.model.status === 'unavailable' &&
|
|
1948
|
-
(await local.typedWithin(record.key, 'visible', roots, true)));
|
|
1904
|
+
return local.typedWithin(record.key, 'visible', roots, false);
|
|
1949
1905
|
}
|
|
1950
1906
|
function textDiffers(left, right) {
|
|
1951
1907
|
return ((left?.content ?? undefined) !== (right?.content ?? undefined) ||
|
|
@@ -1956,18 +1912,6 @@ export function composeRuntime(options) {
|
|
|
1956
1912
|
return undefined;
|
|
1957
1913
|
return entry.binary !== null ? null : entry.content;
|
|
1958
1914
|
}
|
|
1959
|
-
function walkOf(local, key, view) {
|
|
1960
|
-
return async (after) => {
|
|
1961
|
-
const page = await local.walk(key, view, after, WALK_BYTES);
|
|
1962
|
-
return {
|
|
1963
|
-
entries: page.entries.map(({ path, content, binary }) => ({
|
|
1964
|
-
path,
|
|
1965
|
-
content: binary !== null ? null : content,
|
|
1966
|
-
})),
|
|
1967
|
-
next: page.next,
|
|
1968
|
-
};
|
|
1969
|
-
};
|
|
1970
|
-
}
|
|
1971
1915
|
function lookupOf(local, key, view) {
|
|
1972
1916
|
return async (paths) => {
|
|
1973
1917
|
if (paths.length === 0)
|
|
@@ -1984,37 +1928,6 @@ export function composeRuntime(options) {
|
|
|
1984
1928
|
const read = parseFrontmatter(entry.content ?? '');
|
|
1985
1929
|
return { type: read.ok ? (read.frontmatter.type ?? 'any') : undefined, binary: false };
|
|
1986
1930
|
}
|
|
1987
|
-
function overriddenWalk(walk, overrides) {
|
|
1988
|
-
const sorted = [...overrides.keys()].sort();
|
|
1989
|
-
const beyond = (path) => {
|
|
1990
|
-
if (path === null)
|
|
1991
|
-
return 0;
|
|
1992
|
-
let low = 0;
|
|
1993
|
-
let high = sorted.length;
|
|
1994
|
-
while (low < high) {
|
|
1995
|
-
const middle = (low + high) >>> 1;
|
|
1996
|
-
if (sorted[middle] <= path)
|
|
1997
|
-
low = middle + 1;
|
|
1998
|
-
else
|
|
1999
|
-
high = middle;
|
|
2000
|
-
}
|
|
2001
|
-
return low;
|
|
2002
|
-
};
|
|
2003
|
-
return async (after) => {
|
|
2004
|
-
const page = await walk(after);
|
|
2005
|
-
const end = page.next;
|
|
2006
|
-
const added = sorted
|
|
2007
|
-
.slice(beyond(after), end === null ? sorted.length : beyond(end))
|
|
2008
|
-
.flatMap((path) => {
|
|
2009
|
-
const entry = overrides.get(path);
|
|
2010
|
-
return entry === null || entry === undefined
|
|
2011
|
-
? []
|
|
2012
|
-
: [{ path, content: entry.binary !== null ? null : entry.content }];
|
|
2013
|
-
});
|
|
2014
|
-
const entries = [...page.entries.filter(({ path }) => !overrides.has(path)), ...added].sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0));
|
|
2015
|
-
return { entries, next: end };
|
|
2016
|
-
};
|
|
2017
|
-
}
|
|
2018
1931
|
function overriddenLookup(lookup, overrides) {
|
|
2019
1932
|
return async (paths) => {
|
|
2020
1933
|
const known = new Map(await lookup(paths.filter((path) => !overrides.has(path))));
|
|
@@ -2032,9 +1945,8 @@ export function composeRuntime(options) {
|
|
|
2032
1945
|
}
|
|
2033
1946
|
function validatorOf(record, confirmedLayer) {
|
|
2034
1947
|
return async (resulting, touched) => {
|
|
2035
|
-
const composition = compositionFor(record);
|
|
2036
1948
|
const local = await openLocal();
|
|
2037
|
-
const
|
|
1949
|
+
const changes = [];
|
|
2038
1950
|
const overrides = new Map();
|
|
2039
1951
|
const pace = paced();
|
|
2040
1952
|
for (const path of new Set([...confirmedLayer.held, ...touched])) {
|
|
@@ -2045,19 +1957,13 @@ export function composeRuntime(options) {
|
|
|
2045
1957
|
continue;
|
|
2046
1958
|
overrides.set(path, now);
|
|
2047
1959
|
if (textOf(was) !== textOf(now))
|
|
2048
|
-
|
|
1960
|
+
changes.push({ path, was: textOf(was), now: textOf(now) });
|
|
2049
1961
|
}
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
texts: textsOf(local, record.key, 'confirmed'),
|
|
2056
|
-
referrers: referrersOf(local, record.key, 'confirmed'),
|
|
2057
|
-
}));
|
|
2058
|
-
if (clean)
|
|
2059
|
-
return { verdict: 'accept' };
|
|
2060
|
-
return validatedCollection(overriddenWalk(walkOf(local, record.key, 'confirmed'), overrides), lookup, composition);
|
|
1962
|
+
return validatedNeighbourhood(changes, compositionFor(record), {
|
|
1963
|
+
lookup: overriddenLookup(lookupOf(local, record.key, 'confirmed'), overrides),
|
|
1964
|
+
texts: textsOf(local, record.key, 'confirmed'),
|
|
1965
|
+
referrers: referrersOf(local, record.key, 'confirmed'),
|
|
1966
|
+
});
|
|
2061
1967
|
};
|
|
2062
1968
|
}
|
|
2063
1969
|
function textsOf(local, key, view) {
|
|
@@ -2068,7 +1974,7 @@ export function composeRuntime(options) {
|
|
|
2068
1974
|
}
|
|
2069
1975
|
function referrersOf(local, key, view) {
|
|
2070
1976
|
return async (targets) => [
|
|
2071
|
-
...new Set((await local.referrers(key, view, targets
|
|
1977
|
+
...new Set((await local.referrers(key, view, targets)).map(({ path }) => path)),
|
|
2072
1978
|
];
|
|
2073
1979
|
}
|
|
2074
1980
|
async function sideOf(locator, replica, room, saving) {
|
|
@@ -2081,6 +1987,7 @@ export function composeRuntime(options) {
|
|
|
2081
1987
|
operations: room.operations,
|
|
2082
1988
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
2083
1989
|
visible: saving.visible,
|
|
1990
|
+
folders: saving.folders,
|
|
2084
1991
|
queued: queuedDocuments(replica.queue),
|
|
2085
1992
|
unmodeled: await unmodeledIn(record),
|
|
2086
1993
|
validate: validatorOf(record, saving.confirmed),
|
|
@@ -2101,6 +2008,7 @@ export function composeRuntime(options) {
|
|
|
2101
2008
|
locator,
|
|
2102
2009
|
key: saving.record.key,
|
|
2103
2010
|
roots: saving.record.typedSelection?.roots ?? [],
|
|
2011
|
+
composed: composedOf(composed(saving.record)),
|
|
2104
2012
|
overlay: diff.overlay,
|
|
2105
2013
|
});
|
|
2106
2014
|
}
|
|
@@ -2132,13 +2040,13 @@ export function composeRuntime(options) {
|
|
|
2132
2040
|
}
|
|
2133
2041
|
const moves = saved.map(({ path, parts }) => ({ path, to: moveTarget(parts.move) }));
|
|
2134
2042
|
const sourceSaving = await savingIn(replica, moves.map(({ path }) => ({ kind: 'remove', path })), saved.map(({ path }) => path), moves.map(({ path }) => path));
|
|
2135
|
-
const changed = saved.flatMap(({ path, parts }) => changedParts(path,
|
|
2043
|
+
const changed = saved.flatMap(({ path, parts }) => changedParts(path, parts, sourceSaving.visible));
|
|
2136
2044
|
if (changed.length > 0)
|
|
2137
2045
|
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
2138
2046
|
const target = replicas.get(destination);
|
|
2139
2047
|
const listedRoom = rooms.get(destination);
|
|
2140
2048
|
if (target === undefined || listedRoom === undefined) {
|
|
2141
|
-
throw runtimeError('room
|
|
2049
|
+
throw runtimeError('unknown-room', `Unknown Room ${destination}`);
|
|
2142
2050
|
}
|
|
2143
2051
|
const arriving = await savingIn(target, [], moves.map(({ to }) => to));
|
|
2144
2052
|
const built = await buildMove({
|
|
@@ -2201,10 +2109,10 @@ export function composeRuntime(options) {
|
|
|
2201
2109
|
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
2202
2110
|
const { answer, touched } = committedBoth;
|
|
2203
2111
|
if (answer.fenced !== null) {
|
|
2204
|
-
throw runtimeError('room
|
|
2112
|
+
throw runtimeError('unknown-room', `Unknown Room ${answer.fenced}`);
|
|
2205
2113
|
}
|
|
2206
2114
|
if (fences.has(locator) || fences.has(destination)) {
|
|
2207
|
-
throw runtimeError('room
|
|
2115
|
+
throw runtimeError('unknown-room', `Unknown Room ${locator}`);
|
|
2208
2116
|
}
|
|
2209
2117
|
pendings.set(pendingKey(locator, id), {
|
|
2210
2118
|
drafts: saved,
|
|
@@ -2244,6 +2152,8 @@ export function composeRuntime(options) {
|
|
|
2244
2152
|
submitLater(locator);
|
|
2245
2153
|
}
|
|
2246
2154
|
async function committed(locator, target, saved, consumed, writtenBy, files = []) {
|
|
2155
|
+
if (full)
|
|
2156
|
+
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
2247
2157
|
const into = movedInto(locator, saved);
|
|
2248
2158
|
if (into !== null) {
|
|
2249
2159
|
return committedMove(locator, into, target, saved, consumed, writtenBy, files);
|
|
@@ -2255,7 +2165,7 @@ export function composeRuntime(options) {
|
|
|
2255
2165
|
...saved.map(({ path }) => path),
|
|
2256
2166
|
...parts.flatMap(({ bases }) => bases.map(({ path }) => path)),
|
|
2257
2167
|
]);
|
|
2258
|
-
const changed = saved.flatMap(({ path, parts: held }) => changedParts(path,
|
|
2168
|
+
const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, held, saving.visible));
|
|
2259
2169
|
if (changed.length > 0)
|
|
2260
2170
|
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
2261
2171
|
const requeue = await stepped(requeuing(replica.queue, changes, saving.visible, withdrawable(locator), saving.visibleOver));
|
|
@@ -2283,6 +2193,7 @@ export function composeRuntime(options) {
|
|
|
2283
2193
|
providerLimits: limits,
|
|
2284
2194
|
handle: room.handle,
|
|
2285
2195
|
visible: saving.visible,
|
|
2196
|
+
folders: saving.folders,
|
|
2286
2197
|
changes,
|
|
2287
2198
|
bases: parts.flatMap(({ bases }) => bases),
|
|
2288
2199
|
queued: queuedDocuments(replica.queue),
|
|
@@ -2372,10 +2283,10 @@ export function composeRuntime(options) {
|
|
|
2372
2283
|
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
2373
2284
|
const { answer, touched } = done;
|
|
2374
2285
|
if (answer.fenced !== null) {
|
|
2375
|
-
throw runtimeError('room
|
|
2286
|
+
throw runtimeError('unknown-room', `Unknown Room ${answer.fenced}`);
|
|
2376
2287
|
}
|
|
2377
2288
|
if (fences.has(locator))
|
|
2378
|
-
throw runtimeError('room
|
|
2289
|
+
throw runtimeError('unknown-room', `Unknown Room ${locator}`);
|
|
2379
2290
|
for (const origin of withdrawn.keys())
|
|
2380
2291
|
pendings.delete(pendingKey(locator, origin));
|
|
2381
2292
|
for (const [id, drafts] of drafted) {
|
|
@@ -2424,7 +2335,7 @@ export function composeRuntime(options) {
|
|
|
2424
2335
|
throw new Error('connection refused');
|
|
2425
2336
|
await local.keepUpload(locator, identifier, upload);
|
|
2426
2337
|
if (fences.has(locator))
|
|
2427
|
-
throw runtimeError('room
|
|
2338
|
+
throw runtimeError('unknown-room', `Unknown Room ${locator}`);
|
|
2428
2339
|
const key = pendingKey(locator, identifier);
|
|
2429
2340
|
pendings.set(key, { ...pendings.get(key), upload });
|
|
2430
2341
|
if (!continuing())
|
|
@@ -2433,6 +2344,9 @@ export function composeRuntime(options) {
|
|
|
2433
2344
|
async function readHistory(locator, path, query = {}) {
|
|
2434
2345
|
if (closed || refusal !== null)
|
|
2435
2346
|
return UNCERTAIN;
|
|
2347
|
+
await firstDiscovery;
|
|
2348
|
+
if (!roomsNamed)
|
|
2349
|
+
return UNCERTAIN;
|
|
2436
2350
|
try {
|
|
2437
2351
|
await ensureAuthorization();
|
|
2438
2352
|
}
|
|
@@ -2522,14 +2436,12 @@ export function composeRuntime(options) {
|
|
|
2522
2436
|
return (await openLocal()).file(localId, change?.kind === 'binary' ? change.mediaType : '');
|
|
2523
2437
|
};
|
|
2524
2438
|
}
|
|
2525
|
-
function binaryRoom(request) {
|
|
2526
|
-
if (request.kind === 'path')
|
|
2527
|
-
return request.locator;
|
|
2528
|
-
return options.adapter.referrerScope(request.referrer)?.locator ?? null;
|
|
2529
|
-
}
|
|
2530
2439
|
async function transferBinary(request, transfer, answer) {
|
|
2531
2440
|
if (closed || refusal !== null)
|
|
2532
2441
|
return UNCERTAIN;
|
|
2442
|
+
await firstDiscovery;
|
|
2443
|
+
if (!roomsNamed)
|
|
2444
|
+
return UNCERTAIN;
|
|
2533
2445
|
try {
|
|
2534
2446
|
await ensureAuthorization();
|
|
2535
2447
|
}
|
|
@@ -2539,16 +2451,13 @@ export function composeRuntime(options) {
|
|
|
2539
2451
|
if (closed || authorization === undefined || refusal !== null)
|
|
2540
2452
|
return UNCERTAIN;
|
|
2541
2453
|
const since = fencings;
|
|
2542
|
-
journal.record(request.
|
|
2543
|
-
? `transfer binary ${request.locator} ${request.path}`
|
|
2544
|
-
: `transfer binary ${request.fileRef}`);
|
|
2454
|
+
journal.record(`transfer binary ${request.locator} ${request.path}`);
|
|
2545
2455
|
try {
|
|
2546
2456
|
const result = await transfer(authorization);
|
|
2547
2457
|
const settled = result;
|
|
2548
2458
|
journal.record(`outcome binary ${settled.kind}`);
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
return failure({ kind: 'room-not-readable', locator });
|
|
2459
|
+
if (fencedSince(request.locator, since))
|
|
2460
|
+
return failure({ kind: 'room-not-readable', locator: request.locator });
|
|
2552
2461
|
if (settled.kind === 'scoped-refusal') {
|
|
2553
2462
|
const room = replicas.get(settled.locator)?.record;
|
|
2554
2463
|
if (room !== undefined)
|
|
@@ -2563,23 +2472,27 @@ export function composeRuntime(options) {
|
|
|
2563
2472
|
return UNCERTAIN;
|
|
2564
2473
|
}
|
|
2565
2474
|
}
|
|
2475
|
+
function fetchBinary(request, name) {
|
|
2476
|
+
return transferBinary(request, (authorization) => keptFiles.fetch(name, () => options.adapter.binary(authorization, request)), binaryAnswer);
|
|
2477
|
+
}
|
|
2566
2478
|
async function readBinary(request, name) {
|
|
2567
2479
|
if (closed)
|
|
2568
2480
|
return UNCERTAIN;
|
|
2569
2481
|
const held = await keptFiles.find(name);
|
|
2570
2482
|
if (held !== null)
|
|
2571
2483
|
return held;
|
|
2572
|
-
return
|
|
2484
|
+
return fetchBinary(request, name);
|
|
2573
2485
|
}
|
|
2574
2486
|
async function binaryPath(locator, path, revision) {
|
|
2575
2487
|
if (revision !== undefined)
|
|
2576
2488
|
return { kind: 'path', locator, path, revision };
|
|
2577
2489
|
const replica = replicas.get(locator);
|
|
2578
|
-
if (replica === undefined)
|
|
2579
|
-
return null;
|
|
2580
2490
|
const local = await openLocal();
|
|
2581
|
-
const [held] = await local.entries(replica.record.key, 'visible', [path]);
|
|
2582
|
-
|
|
2491
|
+
const [held] = replica === undefined ? [] : await local.entries(replica.record.key, 'visible', [path]);
|
|
2492
|
+
const attached = held === undefined ? attachedFiles.get(locator)?.get(path) : undefined;
|
|
2493
|
+
if (attached !== undefined)
|
|
2494
|
+
return { kind: 'path', locator, path, revision: attached.revision };
|
|
2495
|
+
if (replica === undefined || held?.binary === undefined || held.binary === null)
|
|
2583
2496
|
return null;
|
|
2584
2497
|
if (held.binary.state === 'pending')
|
|
2585
2498
|
return { kind: 'pending', localId: held.binary.localId };
|
|
@@ -2595,7 +2508,7 @@ export function composeRuntime(options) {
|
|
|
2595
2508
|
const holds = (at) => {
|
|
2596
2509
|
const kept = confirmedLayer.binaries[at];
|
|
2597
2510
|
return (kept?.state === 'accepted' &&
|
|
2598
|
-
kept.
|
|
2511
|
+
kept.sha256 === record.sha256 &&
|
|
2599
2512
|
confirmedLayer.revisions[at] === record.revision);
|
|
2600
2513
|
};
|
|
2601
2514
|
let confirmedAt = path;
|
|
@@ -2609,58 +2522,93 @@ export function composeRuntime(options) {
|
|
|
2609
2522
|
locator,
|
|
2610
2523
|
path: confirmedAt,
|
|
2611
2524
|
revision: record.revision,
|
|
2612
|
-
fileRef: record.fileRef,
|
|
2613
2525
|
};
|
|
2614
2526
|
}
|
|
2615
|
-
async function
|
|
2527
|
+
async function askedBinary(locator, path, revision) {
|
|
2616
2528
|
await ready;
|
|
2617
2529
|
const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
|
|
2618
|
-
|
|
2530
|
+
return drafted !== undefined && drafted.localId === revision
|
|
2619
2531
|
? { kind: 'pending', localId: revision }
|
|
2620
|
-
:
|
|
2532
|
+
: binaryPath(locator, path, revision);
|
|
2533
|
+
}
|
|
2534
|
+
function pendingBinary(locator, path, localId) {
|
|
2535
|
+
const facts = queuedFacts(locator, path, localId);
|
|
2536
|
+
return facts === null
|
|
2537
|
+
? absentBinary(path)
|
|
2538
|
+
: {
|
|
2539
|
+
kind: 'binary',
|
|
2540
|
+
file: localId,
|
|
2541
|
+
byteLength: facts.byteLength,
|
|
2542
|
+
mediaType: facts.mediaType,
|
|
2543
|
+
};
|
|
2544
|
+
}
|
|
2545
|
+
async function readBinaryPath(locator, path, revision) {
|
|
2546
|
+
const asked = await askedBinary(locator, path, revision);
|
|
2621
2547
|
if (asked === null)
|
|
2622
2548
|
return absentBinary(path);
|
|
2623
|
-
if (asked.kind === 'pending')
|
|
2624
|
-
|
|
2625
|
-
return facts === null
|
|
2626
|
-
? absentBinary(path)
|
|
2627
|
-
: {
|
|
2628
|
-
kind: 'binary',
|
|
2629
|
-
file: asked.localId,
|
|
2630
|
-
byteLength: facts.byteLength,
|
|
2631
|
-
mediaType: facts.mediaType,
|
|
2632
|
-
};
|
|
2633
|
-
}
|
|
2549
|
+
if (asked.kind === 'pending')
|
|
2550
|
+
return pendingBinary(locator, path, asked.localId);
|
|
2634
2551
|
return readBinary(asked, { locator, path: asked.path, revision: asked.revision });
|
|
2635
2552
|
}
|
|
2553
|
+
async function heldMediaType(locator, path) {
|
|
2554
|
+
const replica = replicas.get(locator);
|
|
2555
|
+
const [held] = replica === undefined
|
|
2556
|
+
? []
|
|
2557
|
+
: await (await openLocal()).entries(replica.record.key, 'visible', [path]);
|
|
2558
|
+
return held === undefined
|
|
2559
|
+
? attachedFiles.get(locator)?.get(path)?.mediaType
|
|
2560
|
+
: held.binary?.mediaType;
|
|
2561
|
+
}
|
|
2562
|
+
function linkOf(asked, held) {
|
|
2563
|
+
return transferBinary(asked, (authorization) => options.adapter.link(authorization, asked), (result) => binaryLinkAnswer(result, held));
|
|
2564
|
+
}
|
|
2636
2565
|
async function linkBinaryPath(locator, path, revision) {
|
|
2637
2566
|
await ready;
|
|
2638
2567
|
const asked = await binaryPath(locator, path, revision);
|
|
2639
2568
|
if (asked === null || asked.kind === 'pending')
|
|
2640
2569
|
return absentBinary(path);
|
|
2641
|
-
return
|
|
2570
|
+
return linkOf(asked, revision === undefined ? await heldMediaType(locator, path) : undefined);
|
|
2642
2571
|
}
|
|
2643
|
-
async function
|
|
2572
|
+
async function showBinaryPath(locator, path, revision) {
|
|
2573
|
+
const asked = await askedBinary(locator, path, revision);
|
|
2574
|
+
if (asked === null)
|
|
2575
|
+
return absentBinary(path);
|
|
2576
|
+
if (asked.kind === 'pending')
|
|
2577
|
+
return pendingBinary(locator, path, asked.localId);
|
|
2578
|
+
if (closed)
|
|
2579
|
+
return UNCERTAIN;
|
|
2580
|
+
const name = { locator, path: asked.path, revision: asked.revision };
|
|
2581
|
+
const kept = await keptFiles.find(name);
|
|
2582
|
+
if (kept !== null)
|
|
2583
|
+
return kept;
|
|
2584
|
+
const mediaType = await heldMediaType(locator, path);
|
|
2585
|
+
if (mediaType?.startsWith('image/') === true)
|
|
2586
|
+
return fetchBinary(asked, name);
|
|
2587
|
+
return linkOf(asked, revision === undefined ? mediaType : undefined);
|
|
2588
|
+
}
|
|
2589
|
+
async function attachBinary(locator, file) {
|
|
2644
2590
|
await ready;
|
|
2645
|
-
|
|
2646
|
-
if (
|
|
2647
|
-
return
|
|
2648
|
-
|
|
2591
|
+
await firstDiscovery;
|
|
2592
|
+
if (closed || !roomsNamed || connectionOf() === 'unreachable' || refusal !== null)
|
|
2593
|
+
return OFFLINE;
|
|
2594
|
+
const request = await attachedFile(locator, file);
|
|
2595
|
+
return transferBinary({ locator, path: file.name }, async (held) => {
|
|
2596
|
+
const result = await options.adapter.attach(held, request);
|
|
2597
|
+
if (result.kind === 'attached') {
|
|
2598
|
+
const landed = attachedFiles.get(locator) ?? new Map();
|
|
2599
|
+
landed.set(result.path, { revision: result.revision, mediaType: request.mediaType });
|
|
2600
|
+
attachedFiles.set(locator, landed);
|
|
2601
|
+
}
|
|
2602
|
+
return result;
|
|
2603
|
+
}, attachAnswer);
|
|
2649
2604
|
}
|
|
2650
2605
|
function readPart(file, part) {
|
|
2651
|
-
return
|
|
2606
|
+
return database.ask({ kind: 'file.part', key: file, part });
|
|
2652
2607
|
}
|
|
2653
2608
|
async function cleanUp() {
|
|
2654
|
-
const freed = await
|
|
2609
|
+
const freed = await database.ask({ kind: 'opened.clean' });
|
|
2655
2610
|
return freed + ((await options.shell.cleanUp?.()) ?? 0);
|
|
2656
2611
|
}
|
|
2657
|
-
function declaredAgents() {
|
|
2658
|
-
for (const capability of capabilities) {
|
|
2659
|
-
if (capability.conversationAgents !== undefined)
|
|
2660
|
-
return capability.conversationAgents;
|
|
2661
|
-
}
|
|
2662
|
-
return undefined;
|
|
2663
|
-
}
|
|
2664
2612
|
function declaredAction(name) {
|
|
2665
2613
|
for (const capability of capabilities) {
|
|
2666
2614
|
for (const action of capability.actions ?? []) {
|
|
@@ -2712,6 +2660,24 @@ export function composeRuntime(options) {
|
|
|
2712
2660
|
return UNCERTAIN;
|
|
2713
2661
|
}
|
|
2714
2662
|
}
|
|
2663
|
+
async function readConversationHistory(request) {
|
|
2664
|
+
await ready;
|
|
2665
|
+
const stream = options.adapter.conversations;
|
|
2666
|
+
if (closed || refusal !== null || stream?.history === undefined)
|
|
2667
|
+
return UNCERTAIN;
|
|
2668
|
+
journal.record('transfer conversation history');
|
|
2669
|
+
try {
|
|
2670
|
+
const result = await stream.history(request);
|
|
2671
|
+
if (result.kind === 'revoked-authorization')
|
|
2672
|
+
await refuse('revoked-authorization');
|
|
2673
|
+
journal.record(`outcome conversation history ${result.kind}`);
|
|
2674
|
+
return calledResult(result);
|
|
2675
|
+
}
|
|
2676
|
+
catch {
|
|
2677
|
+
journal.record('outcome conversation history transport-uncertainty');
|
|
2678
|
+
return UNCERTAIN;
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2715
2681
|
function unsettled(result) {
|
|
2716
2682
|
return result.kind !== 'action' && result.retryable;
|
|
2717
2683
|
}
|
|
@@ -2750,10 +2716,6 @@ export function composeRuntime(options) {
|
|
|
2750
2716
|
continue;
|
|
2751
2717
|
if (closed || signal.aborted)
|
|
2752
2718
|
return 'done';
|
|
2753
|
-
if (pastHorizon(held.operation)) {
|
|
2754
|
-
await local.dropCall(held.operation);
|
|
2755
|
-
continue;
|
|
2756
|
-
}
|
|
2757
2719
|
calling.add(held.operation);
|
|
2758
2720
|
try {
|
|
2759
2721
|
if (unsettled(await invokeAction(held.name, held.input)))
|
|
@@ -2773,6 +2735,33 @@ export function composeRuntime(options) {
|
|
|
2773
2735
|
const held = replicas.get(locator);
|
|
2774
2736
|
return (held !== undefined && options.adapter.reached(locator, held.record.roomVersion, roomVersion));
|
|
2775
2737
|
}
|
|
2738
|
+
function shownAt(locator) {
|
|
2739
|
+
const held = replicas.get(locator)?.record.roomVersion;
|
|
2740
|
+
const raised = highest.get(locator);
|
|
2741
|
+
if (held === undefined || raised === undefined)
|
|
2742
|
+
return held ?? raised;
|
|
2743
|
+
return options.adapter.reached(locator, held, raised) ? held : raised;
|
|
2744
|
+
}
|
|
2745
|
+
function showing(locator, roomVersion) {
|
|
2746
|
+
const at = shownAt(locator);
|
|
2747
|
+
if (at === undefined || !options.adapter.reached(locator, at, roomVersion)) {
|
|
2748
|
+
highest.set(locator, roomVersion);
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
function older(locator, roomVersion) {
|
|
2752
|
+
if (beyondHorizon.has(locator))
|
|
2753
|
+
return false;
|
|
2754
|
+
const at = shownAt(locator);
|
|
2755
|
+
return (at !== undefined &&
|
|
2756
|
+
options.adapter.reached(locator, at, roomVersion) &&
|
|
2757
|
+
!options.adapter.reached(locator, roomVersion, at));
|
|
2758
|
+
}
|
|
2759
|
+
function replaced(locators) {
|
|
2760
|
+
for (const locator of locators) {
|
|
2761
|
+
highest.delete(locator);
|
|
2762
|
+
beyondHorizon.add(locator);
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2776
2765
|
async function writtenAfter(locator, roomVersion, path) {
|
|
2777
2766
|
const held = replicas.get(locator);
|
|
2778
2767
|
if (held === undefined)
|
|
@@ -2783,169 +2772,233 @@ export function composeRuntime(options) {
|
|
|
2783
2772
|
options.adapter.reached(locator, written, roomVersion) &&
|
|
2784
2773
|
!options.adapter.reached(locator, roomVersion, written));
|
|
2785
2774
|
}
|
|
2786
|
-
function
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
else
|
|
2796
|
-
known.set(path, entry.binary !== null ? null : entry.content);
|
|
2797
|
-
}
|
|
2798
|
-
return known;
|
|
2799
|
-
};
|
|
2800
|
-
}
|
|
2801
|
-
async function unreadableRoom(texts, walk, paths) {
|
|
2802
|
-
if (paths !== null) {
|
|
2803
|
-
const text = {};
|
|
2804
|
-
for (const [path, content] of await texts(paths))
|
|
2805
|
-
if (content !== null)
|
|
2806
|
-
text[path] = content;
|
|
2807
|
-
return stepped(unreadableIn(text, Object.keys(text)));
|
|
2808
|
-
}
|
|
2809
|
-
for (let after = null;;) {
|
|
2810
|
-
const page = await walk(after);
|
|
2811
|
-
const text = {};
|
|
2812
|
-
for (const { path, content } of page.entries)
|
|
2813
|
-
if (content !== null)
|
|
2814
|
-
text[path] = content;
|
|
2815
|
-
const refused = await stepped(unreadableIn(text, Object.keys(text)));
|
|
2816
|
-
if (refused !== null)
|
|
2817
|
-
return refused;
|
|
2818
|
-
if (page.next === null)
|
|
2819
|
-
return null;
|
|
2820
|
-
after = page.next;
|
|
2821
|
-
}
|
|
2822
|
-
}
|
|
2823
|
-
async function validatedRoom(local, record, view, previous, overrides = null) {
|
|
2824
|
-
const composition = compositionOf(record);
|
|
2825
|
-
const modeled = record.model.status !== 'unavailable' || record.selectionModel.status !== 'unavailable';
|
|
2826
|
-
const lookup = overrides === null
|
|
2827
|
-
? lookupOf(local, record.key, view)
|
|
2828
|
-
: overriddenLookup(lookupOf(local, record.key, view), overrides);
|
|
2829
|
-
const texts = overrides === null
|
|
2830
|
-
? textsOf(local, record.key, view)
|
|
2831
|
-
: overriddenTexts(textsOf(local, record.key, view), overrides);
|
|
2832
|
-
const walk = overrides === null
|
|
2833
|
-
? walkOf(local, record.key, view)
|
|
2834
|
-
: overriddenWalk(walkOf(local, record.key, view), overrides);
|
|
2835
|
-
let changes = null;
|
|
2836
|
-
let reshaped = false;
|
|
2837
|
-
if (previous !== null && previous.changed.length <= CLEAN_CHANGES) {
|
|
2838
|
-
const [was, now] = await Promise.all([
|
|
2839
|
-
local.entries(record.key, 'confirmed', previous.changed),
|
|
2840
|
-
overrides === null
|
|
2841
|
-
? local.entries(record.key, view, previous.changed)
|
|
2842
|
-
: Promise.resolve([]),
|
|
2843
|
-
]);
|
|
2844
|
-
const before = new Map(was.map((entry) => [entry.path, entry]));
|
|
2845
|
-
const after = overrides === null
|
|
2846
|
-
? new Map(now.map((entry) => [entry.path, entry]))
|
|
2847
|
-
: overrides;
|
|
2848
|
-
changes = previous.changed.flatMap((path) => {
|
|
2849
|
-
const one = before.get(path) ?? null;
|
|
2850
|
-
const other = after.get(path) ?? null;
|
|
2851
|
-
reshaped ||= textDiffers(one, other);
|
|
2852
|
-
const held = textOf(one);
|
|
2853
|
-
const next = textOf(other);
|
|
2854
|
-
return held === next ? [] : [{ path, was: held, now: next }];
|
|
2855
|
-
});
|
|
2856
|
-
}
|
|
2857
|
-
const named = changes === null ? null : changes.map(({ path }) => path);
|
|
2858
|
-
const textChanged = changes === null || changes.length > 0 || reshaped;
|
|
2859
|
-
const modelsChanged = previous === null || !sameComposition(compositionOf(previous.record), composition);
|
|
2860
|
-
if (modeled && (textChanged || modelsChanged)) {
|
|
2861
|
-
const clean = previous !== null &&
|
|
2862
|
-
changes !== null &&
|
|
2863
|
-
(await changedCleanly(changes, compositionOf(previous.record), composition, {
|
|
2864
|
-
lookup,
|
|
2865
|
-
texts,
|
|
2866
|
-
referrers: referrersOf(local, record.key, 'confirmed'),
|
|
2867
|
-
}));
|
|
2868
|
-
if (clean)
|
|
2869
|
-
return null;
|
|
2870
|
-
const verdict = await validatedCollection(walk, lookup, composition);
|
|
2871
|
-
if (verdict.verdict === 'accept')
|
|
2872
|
-
return null;
|
|
2873
|
-
if (verdict.rule !== 'model-unavailable')
|
|
2874
|
-
return { kind: 'refused', refusal: verdict };
|
|
2875
|
-
return unreadableRoom(texts, walk, named);
|
|
2876
|
-
}
|
|
2877
|
-
if (textChanged)
|
|
2878
|
-
return unreadableRoom(texts, walk, named);
|
|
2879
|
-
return null;
|
|
2775
|
+
async function writtenLater(local, locator, key, paths, roomVersion) {
|
|
2776
|
+
if (paths.length === 0 || !older(locator, roomVersion))
|
|
2777
|
+
return new Set();
|
|
2778
|
+
const entries = await local.entries(key, 'confirmed', paths);
|
|
2779
|
+
return new Set(entries.flatMap(({ path, written }) => written !== null &&
|
|
2780
|
+
options.adapter.reached(locator, written, roomVersion) &&
|
|
2781
|
+
!options.adapter.reached(locator, roomVersion, written)
|
|
2782
|
+
? [path]
|
|
2783
|
+
: []));
|
|
2880
2784
|
}
|
|
2881
|
-
function refreshingOf(local, key, liveness) {
|
|
2785
|
+
function refreshingOf(local, locator, key, composition, liveness, replaying = false) {
|
|
2786
|
+
const write = replaying ? finishingWrite : exclusive;
|
|
2882
2787
|
return {
|
|
2788
|
+
turn() {
|
|
2789
|
+
if (replaying)
|
|
2790
|
+
installing(locator);
|
|
2791
|
+
},
|
|
2883
2792
|
prior: async (paths) => new Map((await local.entries(key, 'refreshed', paths)).map((entry) => [entry.path, entry])),
|
|
2793
|
+
later: (paths, roomVersion) => writtenLater(local, locator, key, paths, roomVersion),
|
|
2884
2794
|
async stage(page) {
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
const last =
|
|
2888
|
-
const
|
|
2795
|
+
for (let written = 0;;) {
|
|
2796
|
+
const end = batchEnd(page.entries, written);
|
|
2797
|
+
const last = end === page.entries.length;
|
|
2798
|
+
const entries = page.entries.slice(written, end);
|
|
2799
|
+
const staged = await write(async () => {
|
|
2889
2800
|
if (!liveness())
|
|
2890
2801
|
return false;
|
|
2891
|
-
await local.refresh(key, last
|
|
2802
|
+
await timed(stageBudget, () => local.refresh(key, composition, last
|
|
2892
2803
|
? { ...page, entries }
|
|
2893
|
-
: { entries, removals: [], folders: [], folderRemovals: [] });
|
|
2804
|
+
: { entries, removals: [], folders: [], folderRemovals: [] }));
|
|
2894
2805
|
return true;
|
|
2895
2806
|
});
|
|
2896
2807
|
if (!staged)
|
|
2897
2808
|
return false;
|
|
2809
|
+
if (last)
|
|
2810
|
+
return true;
|
|
2811
|
+
written = end;
|
|
2898
2812
|
}
|
|
2899
|
-
return true;
|
|
2900
2813
|
},
|
|
2901
2814
|
};
|
|
2902
2815
|
}
|
|
2903
|
-
function
|
|
2904
|
-
|
|
2816
|
+
function batchEnd(entries, from) {
|
|
2817
|
+
let end = from;
|
|
2905
2818
|
let spent = 0;
|
|
2906
|
-
|
|
2907
|
-
const size = (
|
|
2908
|
-
if (spent + size >
|
|
2909
|
-
|
|
2910
|
-
spent = 0;
|
|
2911
|
-
}
|
|
2912
|
-
batches.at(-1).push(entry);
|
|
2819
|
+
while (end < entries.length) {
|
|
2820
|
+
const size = (entries[end].content?.length ?? 0) + ROW_WEIGHT;
|
|
2821
|
+
if (spent + size > stageBudget.bytes && end > from)
|
|
2822
|
+
break;
|
|
2913
2823
|
spent += size;
|
|
2914
|
-
|
|
2915
|
-
|
|
2824
|
+
end += 1;
|
|
2825
|
+
}
|
|
2826
|
+
return end;
|
|
2827
|
+
}
|
|
2828
|
+
async function timed(budget, work) {
|
|
2829
|
+
const started = performance.now();
|
|
2830
|
+
const done = await work();
|
|
2831
|
+
const spent = performance.now() - started;
|
|
2832
|
+
if (spent < budget.aim / 2)
|
|
2833
|
+
budget.bytes = Math.min(budget.ceiling, Math.round(budget.bytes * 1.5));
|
|
2834
|
+
else if (spent > budget.aim * 2) {
|
|
2835
|
+
budget.bytes = Math.max(budget.floor, Math.round(budget.bytes / 1.5));
|
|
2836
|
+
}
|
|
2837
|
+
return done;
|
|
2838
|
+
}
|
|
2839
|
+
function installWrite(work) {
|
|
2840
|
+
return installTurn(installTurns.starting, work);
|
|
2841
|
+
}
|
|
2842
|
+
function finishingWrite(work) {
|
|
2843
|
+
return installTurn(installTurns.finishing, work);
|
|
2844
|
+
}
|
|
2845
|
+
function installTurn(queue, work) {
|
|
2846
|
+
return new Promise((resolve, reject) => {
|
|
2847
|
+
queue.push(() => void exclusive(work)
|
|
2848
|
+
.then(resolve, reject)
|
|
2849
|
+
.finally(() => {
|
|
2850
|
+
installTurns.busy = false;
|
|
2851
|
+
nextInstallTurn();
|
|
2852
|
+
}));
|
|
2853
|
+
nextInstallTurn();
|
|
2854
|
+
});
|
|
2916
2855
|
}
|
|
2917
|
-
|
|
2918
|
-
if (
|
|
2919
|
-
return
|
|
2920
|
-
const turn =
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2856
|
+
function nextInstallTurn() {
|
|
2857
|
+
if (installTurns.busy || installTurns.ahead > 0)
|
|
2858
|
+
return;
|
|
2859
|
+
const turn = installTurns.finishing.shift() ?? installTurns.starting.shift();
|
|
2860
|
+
if (turn === undefined)
|
|
2861
|
+
return;
|
|
2862
|
+
installTurns.busy = true;
|
|
2863
|
+
turn();
|
|
2864
|
+
}
|
|
2865
|
+
async function ahead(work) {
|
|
2866
|
+
let taken = false;
|
|
2867
|
+
const hold = {
|
|
2868
|
+
take() {
|
|
2869
|
+
if (taken)
|
|
2870
|
+
return;
|
|
2871
|
+
taken = true;
|
|
2872
|
+
installTurns.ahead += 1;
|
|
2873
|
+
},
|
|
2874
|
+
release() {
|
|
2875
|
+
if (!taken)
|
|
2876
|
+
return;
|
|
2877
|
+
taken = false;
|
|
2878
|
+
installTurns.ahead -= 1;
|
|
2879
|
+
resumeInstall();
|
|
2880
|
+
},
|
|
2881
|
+
};
|
|
2882
|
+
hold.take();
|
|
2924
2883
|
try {
|
|
2925
|
-
await
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2884
|
+
return await work(hold);
|
|
2885
|
+
}
|
|
2886
|
+
finally {
|
|
2887
|
+
hold.release();
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
function resumeInstall() {
|
|
2891
|
+
if (installTurns.ahead > 0)
|
|
2892
|
+
return;
|
|
2893
|
+
void yielded().then(nextInstallTurn);
|
|
2894
|
+
}
|
|
2895
|
+
async function heldStream() {
|
|
2896
|
+
while (streamsFree === 0)
|
|
2897
|
+
await new Promise((admit) => installWaiters.push(admit));
|
|
2898
|
+
streamsFree -= 1;
|
|
2899
|
+
return once(() => {
|
|
2900
|
+
streamsFree += 1;
|
|
2901
|
+
});
|
|
2902
|
+
}
|
|
2903
|
+
async function unqueued() {
|
|
2904
|
+
while (queuedText >= QUEUED_TEXT) {
|
|
2905
|
+
await new Promise((admit) => installWaiters.push(admit));
|
|
2906
|
+
}
|
|
2907
|
+
}
|
|
2908
|
+
function once(release) {
|
|
2909
|
+
let released = false;
|
|
2910
|
+
return () => {
|
|
2911
|
+
if (released)
|
|
2912
|
+
return;
|
|
2913
|
+
released = true;
|
|
2914
|
+
release();
|
|
2915
|
+
for (const admit of installWaiters.splice(0))
|
|
2916
|
+
admit();
|
|
2917
|
+
};
|
|
2918
|
+
}
|
|
2919
|
+
function installing(locator) {
|
|
2920
|
+
installs.add(locator);
|
|
2921
|
+
queueReadiness();
|
|
2922
|
+
}
|
|
2923
|
+
function untransferred(locator) {
|
|
2924
|
+
transfers.delete(locator);
|
|
2925
|
+
arrivals.delete(locator);
|
|
2926
|
+
streaming.delete(locator);
|
|
2927
|
+
underived.delete(locator);
|
|
2928
|
+
unindexed.delete(locator);
|
|
2929
|
+
}
|
|
2930
|
+
function setBack(locator) {
|
|
2931
|
+
const now = bytesOf(locator, true);
|
|
2932
|
+
const earlier = startedOver.get(locator);
|
|
2933
|
+
startedOver.set(locator, {
|
|
2934
|
+
done: Math.max(now.done, earlier?.done ?? 0),
|
|
2935
|
+
total: Math.max(now.total, earlier?.total ?? 0),
|
|
2936
|
+
});
|
|
2937
|
+
}
|
|
2938
|
+
function owedIndexes(locator, bytes, text) {
|
|
2939
|
+
if (bytes <= 0 || text <= 0)
|
|
2940
|
+
return;
|
|
2941
|
+
unindexed.set(locator, [...(unindexed.get(locator) ?? []), { bytes, text }]);
|
|
2942
|
+
}
|
|
2943
|
+
function built(locator, { done, text }) {
|
|
2944
|
+
const owed = unindexed.get(locator) ?? [];
|
|
2945
|
+
let left = text;
|
|
2946
|
+
while (!done && left > 0 && owed.length > 0) {
|
|
2947
|
+
const [first] = owed;
|
|
2948
|
+
if (left >= first.text) {
|
|
2949
|
+
left -= first.text;
|
|
2950
|
+
owed.shift();
|
|
2951
|
+
continue;
|
|
2952
|
+
}
|
|
2953
|
+
owed[0] = {
|
|
2954
|
+
bytes: (first.bytes * (first.text - left)) / first.text,
|
|
2955
|
+
text: first.text - left,
|
|
2956
|
+
};
|
|
2957
|
+
left = 0;
|
|
2958
|
+
}
|
|
2959
|
+
if (done || owed.length === 0)
|
|
2960
|
+
unindexed.delete(locator);
|
|
2961
|
+
queueReadiness();
|
|
2962
|
+
}
|
|
2963
|
+
function installed(locator) {
|
|
2964
|
+
if (installs.delete(locator))
|
|
2965
|
+
queueReadiness();
|
|
2966
|
+
}
|
|
2967
|
+
async function pullRefresh(authorized, held, liveness, hold, refresh) {
|
|
2968
|
+
if (!liveness())
|
|
2969
|
+
return unconfirmed;
|
|
2970
|
+
const { record } = held;
|
|
2971
|
+
const from = standingOf(record.locator);
|
|
2972
|
+
const local = await openLocal();
|
|
2973
|
+
try {
|
|
2974
|
+
await exclusive(() => local.unrefresh(record.key));
|
|
2975
|
+
const result = await advancement(async (authorization, request) => {
|
|
2976
|
+
hold.release();
|
|
2977
|
+
try {
|
|
2978
|
+
return await refresh(authorization, request);
|
|
2979
|
+
}
|
|
2980
|
+
finally {
|
|
2981
|
+
hold.take();
|
|
2982
|
+
}
|
|
2983
|
+
}, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.locator, record.key, composed(record), liveness));
|
|
2984
|
+
if (result.kind !== 'advanced') {
|
|
2985
|
+
journal.record(`transfer refresh ${record.locator} ${result.kind}`);
|
|
2986
|
+
return result;
|
|
2938
2987
|
}
|
|
2939
2988
|
journal.record(`transfer refresh ${record.locator} ${result.record.roomVersion}`);
|
|
2940
|
-
return {
|
|
2989
|
+
return {
|
|
2990
|
+
kind: 'advanced',
|
|
2991
|
+
record: result.record,
|
|
2992
|
+
from,
|
|
2993
|
+
remodelled: result.record.model !== record.model,
|
|
2994
|
+
};
|
|
2941
2995
|
}
|
|
2942
2996
|
catch (error) {
|
|
2997
|
+
if (refusedForSpace(error))
|
|
2998
|
+
return unconfirmed;
|
|
2943
2999
|
journal.record(`transfer refresh ${record.locator} transport-uncertainty`);
|
|
2944
3000
|
throw error;
|
|
2945
3001
|
}
|
|
2946
|
-
finally {
|
|
2947
|
-
reads.finished(record.locator, turn);
|
|
2948
|
-
}
|
|
2949
3002
|
}
|
|
2950
3003
|
const heldContents = new Map();
|
|
2951
3004
|
function heldContentOf(local, key) {
|
|
@@ -2963,63 +3016,115 @@ export function composeRuntime(options) {
|
|
|
2963
3016
|
heldContents.set(key, made);
|
|
2964
3017
|
return made;
|
|
2965
3018
|
}
|
|
3019
|
+
async function abandoned(local, locator) {
|
|
3020
|
+
await local.abandon(locator);
|
|
3021
|
+
setBack(locator);
|
|
3022
|
+
untransferred(locator);
|
|
3023
|
+
liveQueries.recover();
|
|
3024
|
+
sweep();
|
|
3025
|
+
}
|
|
2966
3026
|
function staging(local, liveness) {
|
|
3027
|
+
const derived = async (locator, key, composition) => {
|
|
3028
|
+
underived.delete(locator);
|
|
3029
|
+
for (;;) {
|
|
3030
|
+
if (!liveness())
|
|
3031
|
+
return false;
|
|
3032
|
+
const done = await finishingWrite(async () => liveness()
|
|
3033
|
+
? timed(deriveBudget, () => local.rederive(key, composition, deriveBudget.bytes, DERIVE_LONGEST_MS, DERIVE_FLOOR))
|
|
3034
|
+
: null);
|
|
3035
|
+
if (done === null)
|
|
3036
|
+
return false;
|
|
3037
|
+
built(locator, done);
|
|
3038
|
+
if (done.done)
|
|
3039
|
+
return true;
|
|
3040
|
+
}
|
|
3041
|
+
};
|
|
2967
3042
|
return {
|
|
2968
3043
|
held: (locator) => transfers.get(locator) ?? null,
|
|
3044
|
+
async turn(locator, step) {
|
|
3045
|
+
installing(locator);
|
|
3046
|
+
return step === 'downloading' ? heldStream() : () => { };
|
|
3047
|
+
},
|
|
3048
|
+
unqueued,
|
|
3049
|
+
queued(text) {
|
|
3050
|
+
queuedText += text;
|
|
3051
|
+
return once(() => {
|
|
3052
|
+
queuedText -= text;
|
|
3053
|
+
});
|
|
3054
|
+
},
|
|
3055
|
+
arriving(locator) {
|
|
3056
|
+
let ended = false;
|
|
3057
|
+
return {
|
|
3058
|
+
at(bytes) {
|
|
3059
|
+
if (ended)
|
|
3060
|
+
return;
|
|
3061
|
+
arrivals.set(locator, bytes);
|
|
3062
|
+
streaming.add(locator);
|
|
3063
|
+
const now = performance.now();
|
|
3064
|
+
if (now - arrivalReported < ARRIVAL_REPORT_MS)
|
|
3065
|
+
return;
|
|
3066
|
+
arrivalReported = now;
|
|
3067
|
+
queueReadiness();
|
|
3068
|
+
},
|
|
3069
|
+
end() {
|
|
3070
|
+
if (ended)
|
|
3071
|
+
return;
|
|
3072
|
+
ended = true;
|
|
3073
|
+
if (streaming.delete(locator))
|
|
3074
|
+
queueReadiness();
|
|
3075
|
+
},
|
|
3076
|
+
};
|
|
3077
|
+
},
|
|
3078
|
+
streamed: options.adapter.roomStream !== undefined,
|
|
2969
3079
|
async stage(record, page, composition) {
|
|
2970
|
-
const
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
if (!liveness())
|
|
2977
|
-
return null;
|
|
2978
|
-
if (index === 0 && !(await admitted(local, record.locator, bytes)))
|
|
2979
|
-
return null;
|
|
2980
|
-
try {
|
|
2981
|
-
return await local.stage(record, composition, { entries, folders: last ? page.folders : [] }, key, last);
|
|
2982
|
-
}
|
|
2983
|
-
catch (error) {
|
|
2984
|
-
if (!exceeded(error))
|
|
2985
|
-
throw error;
|
|
2986
|
-
await exceededOn(record.locator, bytes);
|
|
2987
|
-
return null;
|
|
2988
|
-
}
|
|
2989
|
-
});
|
|
2990
|
-
if (staged === null)
|
|
3080
|
+
const { locator } = record;
|
|
3081
|
+
installing(locator);
|
|
3082
|
+
if (!liveness())
|
|
3083
|
+
return null;
|
|
3084
|
+
const key = await installWrite(async () => {
|
|
3085
|
+
if (!liveness())
|
|
2991
3086
|
return null;
|
|
2992
|
-
|
|
2993
|
-
}
|
|
2994
|
-
|
|
3087
|
+
return local.stage(record, composition, page);
|
|
3088
|
+
});
|
|
3089
|
+
if (key === null || !liveness())
|
|
3090
|
+
return null;
|
|
3091
|
+
const arrived = !transfers.has(locator);
|
|
3092
|
+
const prior = transfers.get(locator)?.done.bytes ?? 0;
|
|
3093
|
+
const held = { ...record, key };
|
|
3094
|
+
transfers.set(locator, held);
|
|
3095
|
+
if (arrived)
|
|
3096
|
+
liveQueries.recover();
|
|
3097
|
+
installing(locator);
|
|
2995
3098
|
liveQueries.recover();
|
|
2996
|
-
|
|
3099
|
+
const text = page.entries.reduce((sum, entry) => sum + (entry.content?.length ?? 0), 0);
|
|
3100
|
+
owedIndexes(locator, record.done.bytes - prior, text);
|
|
3101
|
+
const owed = text + (underived.get(locator) ?? 0);
|
|
3102
|
+
underived.set(locator, owed);
|
|
3103
|
+
if (owed < deriveBudget.bytes)
|
|
3104
|
+
return held;
|
|
3105
|
+
return (await derived(locator, key, composition)) ? held : null;
|
|
2997
3106
|
},
|
|
2998
|
-
|
|
3107
|
+
derive: derived,
|
|
3108
|
+
async refreshing(locator, key, composition) {
|
|
2999
3109
|
await exclusive(() => local.unrefresh(key));
|
|
3000
|
-
return refreshingOf(local, key, liveness);
|
|
3110
|
+
return refreshingOf(local, locator, key, composition, liveness, true);
|
|
3001
3111
|
},
|
|
3002
3112
|
abandon: (locator) => exclusive(async () => {
|
|
3003
|
-
if (
|
|
3004
|
-
|
|
3005
|
-
await local.abandon(locator);
|
|
3006
|
-
transfers.delete(locator);
|
|
3007
|
-
liveQueries.recover();
|
|
3008
|
-
sweep();
|
|
3113
|
+
if (liveness())
|
|
3114
|
+
await abandoned(local, locator);
|
|
3009
3115
|
}),
|
|
3010
|
-
setAside:
|
|
3116
|
+
setAside: async (locator) => {
|
|
3011
3117
|
if (!liveness())
|
|
3012
3118
|
return;
|
|
3013
|
-
|
|
3014
|
-
|
|
3119
|
+
setBack(locator);
|
|
3120
|
+
untransferred(locator);
|
|
3015
3121
|
liveQueries.recover();
|
|
3016
|
-
}
|
|
3122
|
+
},
|
|
3017
3123
|
release: (key) => exclusive(async () => {
|
|
3018
3124
|
await local.release(key);
|
|
3019
3125
|
sweep();
|
|
3020
3126
|
}),
|
|
3021
3127
|
content: (key) => heldContentOf(local, key),
|
|
3022
|
-
validate: (record, view) => validatedRoom(local, record, view, null),
|
|
3023
3128
|
};
|
|
3024
3129
|
}
|
|
3025
3130
|
function reusable(room, local) {
|
|
@@ -3031,30 +3136,35 @@ export function composeRuntime(options) {
|
|
|
3031
3136
|
async function pullBootstrap(authorized, room, local, liveness) {
|
|
3032
3137
|
if (!liveness())
|
|
3033
3138
|
return unconfirmed;
|
|
3034
|
-
const turn = generation;
|
|
3035
3139
|
if (replicas.has(room.locator)) {
|
|
3036
3140
|
catching.add(room.locator);
|
|
3037
3141
|
liveQueries.recover();
|
|
3038
3142
|
}
|
|
3039
3143
|
try {
|
|
3040
|
-
const result = await bootstrapRoom(reads, authorized, room, {
|
|
3041
|
-
collection: modelOf(room.locator),
|
|
3042
|
-
selection: modelOf(room.locator, 'typed-selection'),
|
|
3043
|
-
}, (models, held) => (liveness() ? remodel(room.locator, models, held) : held), liveness, staging(local, liveness), reusable(room, local));
|
|
3144
|
+
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));
|
|
3044
3145
|
const detail = result.kind === 'staged' ? result.record.roomVersion : result.kind;
|
|
3045
3146
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
3046
|
-
if (result.kind
|
|
3147
|
+
if (result.kind === 'staged' && older(room.locator, result.record.roomVersion)) {
|
|
3047
3148
|
caughtUpTo(room.locator);
|
|
3149
|
+
installed(room.locator);
|
|
3150
|
+
await abandoned(local, room.locator);
|
|
3151
|
+
journal.record(`transfer bootstrap ${room.locator} older than shown`);
|
|
3152
|
+
return { kind: 'transport-uncertainty' };
|
|
3153
|
+
}
|
|
3154
|
+
if (result.kind !== 'staged') {
|
|
3155
|
+
caughtUpTo(room.locator);
|
|
3156
|
+
installed(room.locator);
|
|
3157
|
+
}
|
|
3048
3158
|
return result;
|
|
3049
3159
|
}
|
|
3050
3160
|
catch (error) {
|
|
3051
|
-
journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
|
|
3052
3161
|
caughtUpTo(room.locator);
|
|
3162
|
+
installed(room.locator);
|
|
3163
|
+
if (refusedForSpace(error))
|
|
3164
|
+
return unconfirmed;
|
|
3165
|
+
journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
|
|
3053
3166
|
throw error;
|
|
3054
3167
|
}
|
|
3055
|
-
finally {
|
|
3056
|
-
reads.finished(room.locator, turn);
|
|
3057
|
-
}
|
|
3058
3168
|
}
|
|
3059
3169
|
function caughtUpTo(locator) {
|
|
3060
3170
|
if (catching.delete(locator))
|
|
@@ -3062,72 +3172,83 @@ export function composeRuntime(options) {
|
|
|
3062
3172
|
}
|
|
3063
3173
|
function behind(room) {
|
|
3064
3174
|
const held = replicas.get(room.locator)?.record.roomVersion;
|
|
3065
|
-
return
|
|
3066
|
-
[announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
|
|
3175
|
+
return held !== undefined && !options.adapter.reached(room.locator, held, room.roomVersion);
|
|
3067
3176
|
}
|
|
3068
|
-
|
|
3069
|
-
const
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
return
|
|
3177
|
+
function standingOf(locator) {
|
|
3178
|
+
const held = replicas.get(locator);
|
|
3179
|
+
return { key: held?.record.key ?? null, confirmed: held?.confirmed ?? 0 };
|
|
3180
|
+
}
|
|
3181
|
+
function stands(locator, from) {
|
|
3182
|
+
const now = standingOf(locator);
|
|
3183
|
+
return now.key === from.key && now.confirmed === from.confirmed;
|
|
3075
3184
|
}
|
|
3076
|
-
async function
|
|
3077
|
-
|
|
3185
|
+
async function advance(room, local, authorized, running) {
|
|
3186
|
+
const live = () => running() && !full;
|
|
3187
|
+
if (!live())
|
|
3078
3188
|
return unconfirmed;
|
|
3079
3189
|
const held = replicas.get(room.locator);
|
|
3080
|
-
if (held !== undefined) {
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
emitRooms();
|
|
3092
|
-
}
|
|
3093
|
-
return null;
|
|
3094
|
-
}
|
|
3095
|
-
if (refreshed.kind === 'advanced') {
|
|
3096
|
-
const landing = { kind: 'refreshed', base: refreshed.base };
|
|
3097
|
-
if (!(await publish(local, room, refreshed.record, landing, live))) {
|
|
3098
|
-
const now = replicas.get(room.locator)?.record.roomVersion;
|
|
3099
|
-
const reached = now !== undefined &&
|
|
3100
|
-
options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
|
|
3101
|
-
return reached ? null : advance(room, local, authorized, live);
|
|
3102
|
-
}
|
|
3103
|
-
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
3104
|
-
return null;
|
|
3105
|
-
}
|
|
3106
|
-
if (refreshed.kind !== 'beyond-horizon')
|
|
3190
|
+
if (held !== undefined && options.adapter.roomStream !== undefined) {
|
|
3191
|
+
stale.delete(room.locator);
|
|
3192
|
+
name(room.locator);
|
|
3193
|
+
return null;
|
|
3194
|
+
}
|
|
3195
|
+
const refresh = reads.refresh;
|
|
3196
|
+
if (held !== undefined && refresh !== undefined) {
|
|
3197
|
+
const refreshed = await ahead((hold) => refreshedRoom(room, local, authorized, held, live, hold, refresh));
|
|
3198
|
+
if (refreshed === 'behind')
|
|
3199
|
+
return advance(room, local, authorized, live);
|
|
3200
|
+
if (refreshed !== 'beyond-horizon')
|
|
3107
3201
|
return refreshed;
|
|
3108
|
-
if (
|
|
3109
|
-
owed.add(room.locator);
|
|
3110
|
-
liveQueries.recover();
|
|
3111
|
-
return unconfirmed;
|
|
3112
|
-
}
|
|
3113
|
-
if (reloadsDue.get(room.locator) === generation)
|
|
3202
|
+
if (reloadsDue.has(room.locator))
|
|
3114
3203
|
return unconfirmed;
|
|
3204
|
+
replaced([room.locator]);
|
|
3115
3205
|
}
|
|
3206
|
+
const from = standingOf(room.locator);
|
|
3116
3207
|
const bootstrapped = await pullBootstrap(authorized, room, local, live);
|
|
3208
|
+
if (bootstrapped.kind !== 'staged')
|
|
3209
|
+
return live() ? bootstrapped : unconfirmed;
|
|
3210
|
+
const landing = { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from };
|
|
3211
|
+
if (await publish(local, room, bootstrapped.record, landing, live)) {
|
|
3212
|
+
return live() ? null : unconfirmed;
|
|
3213
|
+
}
|
|
3214
|
+
return reachedBy(room.locator, bootstrapped.record.roomVersion)
|
|
3215
|
+
? null
|
|
3216
|
+
: advance(room, local, authorized, live);
|
|
3217
|
+
}
|
|
3218
|
+
async function refreshedRoom(room, local, authorized, held, live, hold, refresh) {
|
|
3219
|
+
const refreshed = await pullRefresh(authorized, held, live, hold, refresh);
|
|
3117
3220
|
if (!live())
|
|
3118
3221
|
return unconfirmed;
|
|
3119
|
-
if (
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3222
|
+
if (refreshed.kind === 'still-current') {
|
|
3223
|
+
stale.delete(room.locator);
|
|
3224
|
+
if (unreadable.delete(room.locator)) {
|
|
3225
|
+
scopedRefusals.delete(room.locator);
|
|
3226
|
+
list(room, true);
|
|
3227
|
+
emitRooms();
|
|
3228
|
+
}
|
|
3229
|
+
return null;
|
|
3230
|
+
}
|
|
3231
|
+
if (refreshed.kind === 'advanced') {
|
|
3232
|
+
const landing = {
|
|
3233
|
+
kind: 'refreshed',
|
|
3234
|
+
from: refreshed.from,
|
|
3235
|
+
remodelled: refreshed.remodelled,
|
|
3236
|
+
};
|
|
3237
|
+
if (!(await publish(local, room, refreshed.record, landing, live))) {
|
|
3238
|
+
const now = replicas.get(room.locator)?.record.roomVersion;
|
|
3239
|
+
const reached = now !== undefined &&
|
|
3240
|
+
options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
|
|
3241
|
+
return reached ? null : 'behind';
|
|
3242
|
+
}
|
|
3243
|
+
await retainModel(local, room, modelOf(room.locator));
|
|
3244
|
+
return null;
|
|
3245
|
+
}
|
|
3246
|
+
return refreshed.kind === 'beyond-horizon' ? 'beyond-horizon' : refreshed;
|
|
3123
3247
|
}
|
|
3124
3248
|
async function withhold(withheld, room, local, progress) {
|
|
3125
|
-
if (withheld.kind === 'unconfirmed')
|
|
3249
|
+
if (withheld.kind === 'unconfirmed' || isFenced(room.locator))
|
|
3126
3250
|
return;
|
|
3127
|
-
|
|
3128
|
-
stale.delete(room.locator);
|
|
3129
|
-
else
|
|
3130
|
-
stale.add(room.locator);
|
|
3251
|
+
stale.add(room.locator);
|
|
3131
3252
|
if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
|
|
3132
3253
|
const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
|
|
3133
3254
|
repairable.set(room.locator, reset);
|
|
@@ -3149,33 +3270,11 @@ export function composeRuntime(options) {
|
|
|
3149
3270
|
list(room, false);
|
|
3150
3271
|
emitRooms();
|
|
3151
3272
|
}
|
|
3152
|
-
else if (withheld.kind === 'refused') {
|
|
3153
|
-
refusedSource(room, withheld);
|
|
3154
|
-
}
|
|
3155
3273
|
else if (withheld.kind === 'revoked-authorization') {
|
|
3156
3274
|
await refuse('revoked-authorization');
|
|
3157
3275
|
}
|
|
3158
3276
|
}
|
|
3159
|
-
function refusedSource(room, { refusal }) {
|
|
3160
|
-
sourceRefusals.set(room.locator, {
|
|
3161
|
-
code: refusal.rule,
|
|
3162
|
-
retryable: false,
|
|
3163
|
-
fields: {
|
|
3164
|
-
file: refusal.file,
|
|
3165
|
-
...(refusal.location === undefined ? {} : { location: refusal.location }),
|
|
3166
|
-
...(refusal.referrers === undefined ? {} : { referrers: refusal.referrers }),
|
|
3167
|
-
},
|
|
3168
|
-
});
|
|
3169
|
-
declarationRefusals.delete(room.locator);
|
|
3170
|
-
unreadable.add(room.locator);
|
|
3171
|
-
list(room, false);
|
|
3172
|
-
emitRooms();
|
|
3173
|
-
moveTo({ ...status, ...attention() });
|
|
3174
|
-
journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
|
|
3175
|
-
}
|
|
3176
3277
|
function leaving(batch, result) {
|
|
3177
|
-
if (result.kind === 'expired')
|
|
3178
|
-
return { batch, returned: returnedBy('expired') };
|
|
3179
3278
|
return {
|
|
3180
3279
|
batch,
|
|
3181
3280
|
returned: {
|
|
@@ -3194,8 +3293,8 @@ export function composeRuntime(options) {
|
|
|
3194
3293
|
function settling(acceptance) {
|
|
3195
3294
|
return acceptance !== null && 'apply' in acceptance;
|
|
3196
3295
|
}
|
|
3197
|
-
function
|
|
3198
|
-
return settling(acceptance) ?
|
|
3296
|
+
function failureOf(acceptance) {
|
|
3297
|
+
return settling(acceptance) ? null : acceptance;
|
|
3199
3298
|
}
|
|
3200
3299
|
async function acceptance(room, local, submitted, accepted) {
|
|
3201
3300
|
if (!continuing())
|
|
@@ -3208,44 +3307,17 @@ export function composeRuntime(options) {
|
|
|
3208
3307
|
logAccepted(room.locator, submitted, accepted);
|
|
3209
3308
|
let record = current.record;
|
|
3210
3309
|
let changes;
|
|
3211
|
-
if (!options.adapter.reached(room.locator, held, accepted.roomVersion)
|
|
3310
|
+
if (!options.adapter.reached(room.locator, held, accepted.roomVersion) &&
|
|
3311
|
+
!older(room.locator, accepted.roomVersion)) {
|
|
3212
3312
|
const paths = [...returnedPaths(submitted, accepted)];
|
|
3213
3313
|
const confirmedLayer = await stepped(slicing(await local.entries(record.key, 'confirmed', paths), paths));
|
|
3214
|
-
const back = await stepped(returning(confirmedLayer, submitted, accepted, accepted.roomVersion));
|
|
3314
|
+
const back = await stepped(returning(confirmedLayer, submitted, accepted, accepted.roomVersion, composed(record)));
|
|
3215
3315
|
record = {
|
|
3216
3316
|
...record,
|
|
3217
3317
|
roomVersion: options.adapter.follows(room.locator, held, accepted.roomVersion)
|
|
3218
3318
|
? accepted.roomVersion
|
|
3219
3319
|
: held,
|
|
3220
3320
|
};
|
|
3221
|
-
const overrides = new Map([
|
|
3222
|
-
...back.set.map((entry) => [entry.path, entry]),
|
|
3223
|
-
...back.removed.map((path) => [path, null]),
|
|
3224
|
-
]);
|
|
3225
|
-
const refused = await validatedRoom(local, record, 'confirmed', { record: current.record, changed: [...overrides.keys()] }, overrides);
|
|
3226
|
-
if (refused !== null) {
|
|
3227
|
-
const key = pendingKey(room.locator, submitted.id);
|
|
3228
|
-
return {
|
|
3229
|
-
locator: room.locator,
|
|
3230
|
-
left: [],
|
|
3231
|
-
commit: {
|
|
3232
|
-
rooms: [],
|
|
3233
|
-
writes: [
|
|
3234
|
-
{
|
|
3235
|
-
locator: room.locator,
|
|
3236
|
-
accepted: [{ id: submitted.id, roomVersion: accepted.roomVersion }],
|
|
3237
|
-
},
|
|
3238
|
-
],
|
|
3239
|
-
},
|
|
3240
|
-
refused,
|
|
3241
|
-
apply() {
|
|
3242
|
-
const state = pendings.get(key);
|
|
3243
|
-
if (state !== undefined && !fences.has(room.locator))
|
|
3244
|
-
pendings.set(key, { ...state, accepted: accepted.roomVersion });
|
|
3245
|
-
return changeSet(room.locator, { queue: true });
|
|
3246
|
-
},
|
|
3247
|
-
};
|
|
3248
|
-
}
|
|
3249
3321
|
changes = back;
|
|
3250
3322
|
}
|
|
3251
3323
|
const settled = await settlement(local, room.locator, current, record, current.queue.filter((batch) => batch.id !== submitted.id), { room: true, ...(changes === undefined ? {} : { changes }) });
|
|
@@ -3253,10 +3325,9 @@ export function composeRuntime(options) {
|
|
|
3253
3325
|
...settled,
|
|
3254
3326
|
apply(answer) {
|
|
3255
3327
|
const set = settled.apply(answer);
|
|
3328
|
+
showing(room.locator, accepted.roomVersion);
|
|
3256
3329
|
pendings.delete(pendingKey(room.locator, submitted.id));
|
|
3257
3330
|
unreadable.delete(room.locator);
|
|
3258
|
-
sourceRefusals.delete(room.locator);
|
|
3259
|
-
declarationRefusals.delete(room.locator);
|
|
3260
3331
|
list(room, true);
|
|
3261
3332
|
return set;
|
|
3262
3333
|
},
|
|
@@ -3274,13 +3345,9 @@ export function composeRuntime(options) {
|
|
|
3274
3345
|
const failed = await exclusive(async () => {
|
|
3275
3346
|
const adopting = await acceptance(room, local, submitted, accepted);
|
|
3276
3347
|
await adoptedTogether(local, [adopting]);
|
|
3277
|
-
return
|
|
3348
|
+
return failureOf(adopting);
|
|
3278
3349
|
});
|
|
3279
|
-
|
|
3280
|
-
return true;
|
|
3281
|
-
if (failed.kind === 'refused' && continuing())
|
|
3282
|
-
refusedSource(room, failed);
|
|
3283
|
-
return false;
|
|
3350
|
+
return failed === null;
|
|
3284
3351
|
}
|
|
3285
3352
|
async function adoptedMove(room, partner, local, submitted, accepted) {
|
|
3286
3353
|
const other = discovered.get(partner);
|
|
@@ -3294,18 +3361,10 @@ export function composeRuntime(options) {
|
|
|
3294
3361
|
...accepted.destination,
|
|
3295
3362
|
});
|
|
3296
3363
|
await adoptedTogether(local, [left, arrived]);
|
|
3297
|
-
|
|
3298
|
-
if (other !== undefined && refusedArrival?.kind === 'refused' && continuing()) {
|
|
3299
|
-
refusedSource(other, refusedArrival);
|
|
3300
|
-
}
|
|
3301
|
-
return refusalOf(left);
|
|
3364
|
+
return failureOf(left);
|
|
3302
3365
|
});
|
|
3303
3366
|
submitLater(partner);
|
|
3304
|
-
|
|
3305
|
-
return true;
|
|
3306
|
-
if (failed.kind === 'refused' && continuing())
|
|
3307
|
-
refusedSource(room, failed);
|
|
3308
|
-
return false;
|
|
3367
|
+
return failed === null;
|
|
3309
3368
|
}
|
|
3310
3369
|
function queueIn(locator) {
|
|
3311
3370
|
return replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
@@ -3416,20 +3475,13 @@ export function composeRuntime(options) {
|
|
|
3416
3475
|
return;
|
|
3417
3476
|
moveTo({ ...status, ...attention() });
|
|
3418
3477
|
}
|
|
3419
|
-
async function discardHeld(head, room, local, signal) {
|
|
3420
|
-
const partner = partnerOf(head, room.locator);
|
|
3421
|
-
const other = partner === null ? undefined : discovered.get(partner);
|
|
3422
|
-
await discardPending(other === undefined ? [room] : [room, other], local, signal);
|
|
3423
|
-
}
|
|
3424
3478
|
async function submitRoom(room, local, authorized, progress, signal) {
|
|
3425
3479
|
while (continuing() && authorization !== undefined && !signal.aborted) {
|
|
3426
3480
|
const replica = replicas.get(room.locator);
|
|
3427
|
-
const head = replica?.queue
|
|
3481
|
+
const head = replica?.queue.find((batch) => !landedAhead.has(pendingKey(room.locator, batch.id)));
|
|
3428
3482
|
if (replica === undefined || head === undefined)
|
|
3429
3483
|
return 'done';
|
|
3430
3484
|
const state = pendings.get(pendingKey(room.locator, head.id));
|
|
3431
|
-
if (state?.accepted !== undefined)
|
|
3432
|
-
return 'done';
|
|
3433
3485
|
const partner = partnerOf(head, room.locator);
|
|
3434
3486
|
if (partner !== null && !leavingIn(head, room.locator)) {
|
|
3435
3487
|
submitLater(partner);
|
|
@@ -3453,10 +3505,6 @@ export function composeRuntime(options) {
|
|
|
3453
3505
|
return 'done';
|
|
3454
3506
|
}
|
|
3455
3507
|
const lost = earlier || submission.state === 'submitted';
|
|
3456
|
-
if (lost && pastHorizon(head.id)) {
|
|
3457
|
-
await discardHeld(head, room, local, signal);
|
|
3458
|
-
return 'done';
|
|
3459
|
-
}
|
|
3460
3508
|
let result;
|
|
3461
3509
|
journal.record(`submission ${room.locator} ${head.id}`);
|
|
3462
3510
|
await exclusive(() => mark(local, room.locator, head, { state: 'submitted' }, lost));
|
|
@@ -3534,32 +3582,12 @@ export function composeRuntime(options) {
|
|
|
3534
3582
|
const other = partner === null ? undefined : discovered.get(partner);
|
|
3535
3583
|
if (result.kind === 'conflicted' ||
|
|
3536
3584
|
result.kind === 'rejected' ||
|
|
3537
|
-
result.kind === 'expired' ||
|
|
3538
3585
|
result.kind === 'retryable-refusal') {
|
|
3539
|
-
if (result.kind === 'expired' && lost && result.restored !== true) {
|
|
3540
|
-
journal.record(`outcome ${room.locator} ${head.id} expired`);
|
|
3541
|
-
await discardHeld(head, room, local, signal);
|
|
3542
|
-
return 'done';
|
|
3543
|
-
}
|
|
3544
|
-
if (result.kind === 'expired') {
|
|
3545
|
-
await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, partner !== null && lost));
|
|
3546
|
-
const there = other === undefined ? null : await advance(other, local, authorized, continuing);
|
|
3547
|
-
if (other !== undefined && there !== null) {
|
|
3548
|
-
await withhold(there, other, local, progress);
|
|
3549
|
-
return 'done';
|
|
3550
|
-
}
|
|
3551
|
-
const withheld = await advance(room, local, authorized, continuing);
|
|
3552
|
-
if (withheld !== null) {
|
|
3553
|
-
await withhold(withheld, room, local, progress);
|
|
3554
|
-
return 'done';
|
|
3555
|
-
}
|
|
3556
|
-
}
|
|
3557
3586
|
journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` +
|
|
3558
3587
|
(result.kind === 'conflicted' && result.roomVersion !== undefined
|
|
3559
3588
|
? ` ${result.roomVersion}`
|
|
3560
3589
|
: ''));
|
|
3561
|
-
|
|
3562
|
-
journal.record(`refusal ${result.kind} ${result.code}`);
|
|
3590
|
+
journal.record(`refusal ${result.kind} ${result.code}`);
|
|
3563
3591
|
}
|
|
3564
3592
|
if (result.kind === 'retryable-refusal') {
|
|
3565
3593
|
const reset = result.reset;
|
|
@@ -3567,11 +3595,11 @@ export function composeRuntime(options) {
|
|
|
3567
3595
|
await exclusive(() => mark(local, room.locator, head, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
|
|
3568
3596
|
return reset === undefined ? 'again' : { at: Date.parse(reset) };
|
|
3569
3597
|
}
|
|
3570
|
-
if (result.kind === 'conflicted' || result.kind === 'rejected'
|
|
3598
|
+
if (result.kind === 'conflicted' || result.kind === 'rejected') {
|
|
3571
3599
|
const left = leaving(head, result);
|
|
3572
3600
|
const refreshed = async () => ({
|
|
3573
|
-
there: other === undefined ? null : await
|
|
3574
|
-
here: await
|
|
3601
|
+
there: other === undefined ? null : await advanced(other, local, authorized),
|
|
3602
|
+
here: await advanced(room, local, authorized),
|
|
3575
3603
|
});
|
|
3576
3604
|
const judged = result.kind === 'conflicted' && other !== undefined ? await refreshed() : null;
|
|
3577
3605
|
if (judged !== null && (judged.there !== null || judged.here !== null)) {
|
|
@@ -3591,16 +3619,19 @@ export function composeRuntime(options) {
|
|
|
3591
3619
|
return;
|
|
3592
3620
|
}
|
|
3593
3621
|
const confirmedLayer = await sliceAt(current.record, 'confirmed', [head]);
|
|
3594
|
-
const held =
|
|
3595
|
-
(await landedWhole(room.locator, head, confirmedLayer))
|
|
3596
|
-
|
|
3597
|
-
|
|
3622
|
+
const held = result.kind === 'conflicted' &&
|
|
3623
|
+
(await landedWhole(room.locator, head, confirmedLayer, result.affected));
|
|
3624
|
+
if (held &&
|
|
3625
|
+
result.kind === 'conflicted' &&
|
|
3626
|
+
partner === null &&
|
|
3627
|
+
!landed(head, confirmedLayer)) {
|
|
3628
|
+
landedAhead.set(pendingKey(room.locator, head.id), Object.fromEntries([...touchedBy([head])].map((path) => [path, confirmedLayer.revisions[path]])));
|
|
3629
|
+
return;
|
|
3630
|
+
}
|
|
3598
3631
|
publishChangeSet(await settle(local, room.locator, current, current.record, current.queue.filter((batch) => batch.id !== head.id), { leaving: [held ? { ...left, returned: null } : left] }));
|
|
3599
3632
|
await unpair(local);
|
|
3600
3633
|
moveTo({ ...status, ...attention() });
|
|
3601
3634
|
});
|
|
3602
|
-
if (result.kind === 'expired')
|
|
3603
|
-
continue;
|
|
3604
3635
|
const { there, here } = judged ?? (await refreshed());
|
|
3605
3636
|
if (other !== undefined && there !== null)
|
|
3606
3637
|
await withhold(there, other, local, progress);
|
|
@@ -3620,93 +3651,55 @@ export function composeRuntime(options) {
|
|
|
3620
3651
|
}
|
|
3621
3652
|
return 'done';
|
|
3622
3653
|
}
|
|
3623
|
-
function pastHorizon(identity) {
|
|
3624
|
-
return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
|
|
3625
|
-
}
|
|
3626
|
-
async function discardPending(rooms, local, signal) {
|
|
3627
|
-
const discarded = await exclusive(async () => {
|
|
3628
|
-
const held = rooms
|
|
3629
|
-
.filter(({ locator }) => !fences.has(locator))
|
|
3630
|
-
.map((room) => ({
|
|
3631
|
-
room,
|
|
3632
|
-
before: replicas.get(room.locator),
|
|
3633
|
-
queue: queueIn(room.locator),
|
|
3634
|
-
}));
|
|
3635
|
-
if (held.length === 0)
|
|
3636
|
-
return [];
|
|
3637
|
-
const answer = await local.commit({
|
|
3638
|
-
rooms: held.flatMap(({ room, before }) => before === undefined
|
|
3639
|
-
? []
|
|
3640
|
-
: [
|
|
3641
|
-
{
|
|
3642
|
-
locator: room.locator,
|
|
3643
|
-
key: before.record.key,
|
|
3644
|
-
roots: before.record.typedSelection?.roots ?? [],
|
|
3645
|
-
overlay: { set: [], cleared: [...before.overlaid] },
|
|
3646
|
-
},
|
|
3647
|
-
]),
|
|
3648
|
-
discards: held.map(({ room }) => room.locator),
|
|
3649
|
-
});
|
|
3650
|
-
for (const { room, before, queue } of held) {
|
|
3651
|
-
const { locator } = room;
|
|
3652
|
-
lastFenced.set(locator, ++fencings);
|
|
3653
|
-
for (const batch of queue)
|
|
3654
|
-
pendings.delete(pendingKey(locator, batch.id));
|
|
3655
|
-
queuesWithoutReplica.delete(locator);
|
|
3656
|
-
if (before !== undefined) {
|
|
3657
|
-
replicas.set(locator, {
|
|
3658
|
-
...before,
|
|
3659
|
-
queue: [],
|
|
3660
|
-
overlaid: new Set(),
|
|
3661
|
-
count: answer.counts[locator] ?? before.count,
|
|
3662
|
-
});
|
|
3663
|
-
}
|
|
3664
|
-
journal.record(`room discarded ${locator}`);
|
|
3665
|
-
}
|
|
3666
|
-
publishChangeSet(mergeChangeSets(held.map(({ room }) => changeSet(room.locator, {
|
|
3667
|
-
documents: answer.changed[room.locator] ?? [],
|
|
3668
|
-
queue: true,
|
|
3669
|
-
}))));
|
|
3670
|
-
moveTo({ ...status, ...attention() });
|
|
3671
|
-
return held.map(({ room }) => room);
|
|
3672
|
-
});
|
|
3673
|
-
for (const room of discarded) {
|
|
3674
|
-
if (signal.aborted)
|
|
3675
|
-
return;
|
|
3676
|
-
const progress = { complete: true, reachable: true };
|
|
3677
|
-
await roomTask(room.locator, async (live) => {
|
|
3678
|
-
await Promise.race([
|
|
3679
|
-
reload(room, local, progress, () => live() && !signal.aborted),
|
|
3680
|
-
abandonedOn(signal),
|
|
3681
|
-
]);
|
|
3682
|
-
});
|
|
3683
|
-
if (signal.aborted)
|
|
3684
|
-
return;
|
|
3685
|
-
await rest(progress);
|
|
3686
|
-
}
|
|
3687
|
-
}
|
|
3688
3654
|
function awaitingAnswer(locator) {
|
|
3655
|
+
answering.get(locator)?.release();
|
|
3689
3656
|
let release = () => { };
|
|
3690
|
-
const
|
|
3657
|
+
const answered = new Promise((resolve) => {
|
|
3691
3658
|
release = resolve;
|
|
3692
3659
|
});
|
|
3693
|
-
|
|
3660
|
+
const awaited = { answered, release };
|
|
3661
|
+
answering.set(locator, awaited);
|
|
3694
3662
|
return () => {
|
|
3695
|
-
if (answering.get(locator)
|
|
3663
|
+
if (answering.get(locator) === awaited)
|
|
3696
3664
|
answering.delete(locator);
|
|
3697
3665
|
release();
|
|
3698
3666
|
};
|
|
3699
3667
|
}
|
|
3700
|
-
|
|
3701
|
-
const
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3668
|
+
function declaredAsHeld(room) {
|
|
3669
|
+
const held = replicas.get(room.locator)?.record;
|
|
3670
|
+
return (held === undefined ||
|
|
3671
|
+
(JSON.stringify(held.operations) === JSON.stringify(room.operations) &&
|
|
3672
|
+
JSON.stringify(held.replica) === JSON.stringify(room.replica) &&
|
|
3673
|
+
JSON.stringify(held.typedSelection) === JSON.stringify(room.typedSelection ?? null)));
|
|
3674
|
+
}
|
|
3675
|
+
async function keepNaming(local, room) {
|
|
3676
|
+
const held = replicas.get(room.locator)?.record;
|
|
3677
|
+
if (held === undefined || (held.name === room.name && held.handle === room.handle))
|
|
3678
|
+
return;
|
|
3679
|
+
await exclusive(async () => {
|
|
3680
|
+
if (!continuing())
|
|
3681
|
+
return;
|
|
3682
|
+
const replica = replicas.get(room.locator);
|
|
3683
|
+
if (replica === undefined)
|
|
3684
|
+
return;
|
|
3685
|
+
const record = { ...replica.record, name: room.name, handle: room.handle };
|
|
3686
|
+
await local.commit({
|
|
3687
|
+
rooms: [
|
|
3688
|
+
{
|
|
3689
|
+
locator: room.locator,
|
|
3690
|
+
key: record.key,
|
|
3691
|
+
record: storedRecord(record),
|
|
3692
|
+
composed: composedOf(composed(record)),
|
|
3693
|
+
},
|
|
3694
|
+
],
|
|
3695
|
+
});
|
|
3696
|
+
replicas.set(room.locator, { ...replica, record });
|
|
3697
|
+
});
|
|
3698
|
+
}
|
|
3699
|
+
async function retainModel(local, room, next) {
|
|
3700
|
+
const key = room.locator;
|
|
3701
|
+
const previous = modelOf(room.locator);
|
|
3702
|
+
if (JSON.stringify(previous) === JSON.stringify(next) && declaredAsHeld(room))
|
|
3710
3703
|
return;
|
|
3711
3704
|
if (next.status === 'unavailable') {
|
|
3712
3705
|
invalidModels.add(key);
|
|
@@ -3718,26 +3711,15 @@ export function composeRuntime(options) {
|
|
|
3718
3711
|
const held = replicas.get(room.locator);
|
|
3719
3712
|
let changed = null;
|
|
3720
3713
|
if (held !== undefined) {
|
|
3721
|
-
const
|
|
3714
|
+
const record = {
|
|
3722
3715
|
...held.record,
|
|
3716
|
+
name: room.name,
|
|
3717
|
+
handle: room.handle,
|
|
3723
3718
|
operations: room.operations,
|
|
3724
3719
|
replica: room.replica,
|
|
3725
3720
|
typedSelection: room.typedSelection ?? null,
|
|
3726
|
-
|
|
3721
|
+
model: next,
|
|
3727
3722
|
};
|
|
3728
|
-
let read = 'staged';
|
|
3729
|
-
if (!sameMembership(declared, held.record)) {
|
|
3730
|
-
const verdict = await validatedCollection(walkOf(local, held.record.key, 'confirmed'), lookupOf(local, held.record.key, 'confirmed'), compositionOf(declared));
|
|
3731
|
-
read =
|
|
3732
|
-
verdict.verdict !== 'refuse'
|
|
3733
|
-
? 'staged'
|
|
3734
|
-
: verdict.rule === 'model-unavailable'
|
|
3735
|
-
? 'deferred'
|
|
3736
|
-
: { kind: 'refused', refusal: verdict };
|
|
3737
|
-
}
|
|
3738
|
-
const record = read === 'staged'
|
|
3739
|
-
? declared
|
|
3740
|
-
: { ...declared, typedSelection: held.record.typedSelection };
|
|
3741
3723
|
const confirmedLayer = await sliceAt(record, 'confirmed', held.queue, held.overlaid);
|
|
3742
3724
|
const replayedLayer = await replayed(record, confirmedLayer, held.queue);
|
|
3743
3725
|
const diff = await overlayDiff(record, held, replayedLayer.visible, replayedLayer.touched);
|
|
@@ -3748,41 +3730,33 @@ export function composeRuntime(options) {
|
|
|
3748
3730
|
key: record.key,
|
|
3749
3731
|
record: storedRecord(record),
|
|
3750
3732
|
roots: record.typedSelection?.roots ?? [],
|
|
3751
|
-
|
|
3733
|
+
composed: composedOf(composed(record)),
|
|
3752
3734
|
overlay: diff.overlay,
|
|
3753
3735
|
},
|
|
3754
3736
|
],
|
|
3755
3737
|
});
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
replicas.set(room.locator, { ...held, record, overlaid: diff.touched, count });
|
|
3738
|
+
const { count, indexed } = committedRoom(room.locator, answer);
|
|
3739
|
+
replicas.set(room.locator, { ...held, record, overlaid: diff.touched, count, indexed });
|
|
3759
3740
|
changed = changeSet(room.locator, {
|
|
3760
3741
|
documents: answer.changed[room.locator] ?? [],
|
|
3761
3742
|
room: true,
|
|
3762
3743
|
});
|
|
3763
|
-
if (read === 'staged' || read === 'deferred') {
|
|
3764
|
-
if (declarationRefusals.delete(room.locator)) {
|
|
3765
|
-
sourceRefusals.delete(room.locator);
|
|
3766
|
-
unreadable.delete(room.locator);
|
|
3767
|
-
}
|
|
3768
|
-
}
|
|
3769
|
-
else {
|
|
3770
|
-
refusedSource(room, read);
|
|
3771
|
-
declarationRefusals.add(room.locator);
|
|
3772
|
-
}
|
|
3773
|
-
}
|
|
3774
|
-
models.set(key, next);
|
|
3775
|
-
if (next.status === 'current') {
|
|
3776
|
-
liveModels.set(key, JSON.stringify(next.snapshot));
|
|
3777
|
-
invalidModels.delete(key);
|
|
3778
3744
|
}
|
|
3779
|
-
|
|
3745
|
+
heldModel(room, next);
|
|
3780
3746
|
if (changed === null)
|
|
3781
3747
|
emitRooms();
|
|
3782
3748
|
else
|
|
3783
3749
|
publishChangeSet(changed);
|
|
3784
3750
|
});
|
|
3785
3751
|
}
|
|
3752
|
+
function heldModel(room, next) {
|
|
3753
|
+
models.set(room.locator, next);
|
|
3754
|
+
if (next.status === 'current') {
|
|
3755
|
+
liveModels.set(room.locator, JSON.stringify(next.snapshot));
|
|
3756
|
+
invalidModels.delete(room.locator);
|
|
3757
|
+
}
|
|
3758
|
+
list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
3759
|
+
}
|
|
3786
3760
|
async function scoped(result, room, local) {
|
|
3787
3761
|
if (result.scope === 'content' && !result.retryable) {
|
|
3788
3762
|
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
@@ -3796,14 +3770,14 @@ export function composeRuntime(options) {
|
|
|
3796
3770
|
});
|
|
3797
3771
|
let scopedAway = room;
|
|
3798
3772
|
if (result.scope === 'model')
|
|
3799
|
-
await retainModel(local, room,
|
|
3773
|
+
await retainModel(local, room, {
|
|
3800
3774
|
status: 'unavailable',
|
|
3801
3775
|
reason: 'not-readable',
|
|
3802
3776
|
});
|
|
3803
3777
|
else if (result.scope !== 'operation') {
|
|
3804
3778
|
unreadable.add(room.locator);
|
|
3805
3779
|
scopedAway = { ...room, replica: { ...room.replica, admission: 'unavailable' } };
|
|
3806
|
-
await retainModel(local, scopedAway,
|
|
3780
|
+
await retainModel(local, scopedAway, modelOf(room.locator));
|
|
3807
3781
|
}
|
|
3808
3782
|
if (!continuing())
|
|
3809
3783
|
return;
|
|
@@ -3811,6 +3785,17 @@ export function composeRuntime(options) {
|
|
|
3811
3785
|
emitRooms();
|
|
3812
3786
|
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
3813
3787
|
}
|
|
3788
|
+
function name(locator) {
|
|
3789
|
+
const stream = options.adapter.roomStream;
|
|
3790
|
+
const record = replicas.get(locator)?.record;
|
|
3791
|
+
const room = discovered.get(locator);
|
|
3792
|
+
if (stream === undefined || record === undefined || room === undefined)
|
|
3793
|
+
return;
|
|
3794
|
+
if (!socketOpen || !handed || record.replica.selection !== room.replica.selection)
|
|
3795
|
+
return;
|
|
3796
|
+
naming += 1;
|
|
3797
|
+
stream.hold([{ locator, roomVersion: record.roomVersion }]);
|
|
3798
|
+
}
|
|
3814
3799
|
function heldRooms(listed) {
|
|
3815
3800
|
return listed.flatMap((room) => {
|
|
3816
3801
|
const record = replicas.get(room.locator)?.record;
|
|
@@ -3828,14 +3813,19 @@ export function composeRuntime(options) {
|
|
|
3828
3813
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
3829
3814
|
});
|
|
3830
3815
|
if (attempt === 'opened') {
|
|
3816
|
+
naming += 1;
|
|
3831
3817
|
options.adapter.roomStream?.hold(heldRooms(ready));
|
|
3832
3818
|
handed = true;
|
|
3833
3819
|
}
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3820
|
+
for (const room of ready) {
|
|
3821
|
+
if (attempt !== 'unstreamed' && replicas.has(room.locator)) {
|
|
3822
|
+
if (attempt === 'authorized')
|
|
3823
|
+
name(room.locator);
|
|
3824
|
+
continue;
|
|
3825
|
+
}
|
|
3826
|
+
void roomTask(room.locator, (live) => loadRoom(room, local, progress, live)).then(() => rest(progress));
|
|
3827
|
+
}
|
|
3828
|
+
const failed = prepared.find((room) => room.status === 'rejected');
|
|
3839
3829
|
if (failed !== undefined)
|
|
3840
3830
|
throw failed.reason;
|
|
3841
3831
|
}
|
|
@@ -3853,27 +3843,31 @@ export function composeRuntime(options) {
|
|
|
3853
3843
|
return false;
|
|
3854
3844
|
}
|
|
3855
3845
|
if (prior !== undefined)
|
|
3856
|
-
await retainModel(local, room,
|
|
3846
|
+
await retainModel(local, room, modelOf(room.locator));
|
|
3847
|
+
if (prior !== undefined)
|
|
3848
|
+
await keepNaming(local, room);
|
|
3857
3849
|
if (discoveryPaused || authorization === undefined)
|
|
3858
3850
|
return false;
|
|
3859
3851
|
return room.replica.admission === 'admitted';
|
|
3860
3852
|
}
|
|
3861
3853
|
async function caughtUp(room, local, progress, live) {
|
|
3862
|
-
if (
|
|
3854
|
+
if (options.adapter.roomStream !== undefined || authorization === undefined)
|
|
3855
|
+
return;
|
|
3856
|
+
if (!live() || !behind(room))
|
|
3863
3857
|
return;
|
|
3864
3858
|
const withheld = await advance(room, local, authorization, live);
|
|
3865
3859
|
if (withheld !== null)
|
|
3866
3860
|
await withhold(withheld, room, local, progress);
|
|
3867
3861
|
}
|
|
3868
3862
|
function revoked(locator) {
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
3872
|
-
});
|
|
3863
|
+
const model = modelOf(locator);
|
|
3864
|
+
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
3873
3865
|
}
|
|
3874
3866
|
async function loadRoom(room, local, progress, live) {
|
|
3875
3867
|
if (!live() || authorization === undefined)
|
|
3876
3868
|
return;
|
|
3869
|
+
if (replicas.has(room.locator) && !(await prepareRoom(room, local, progress)))
|
|
3870
|
+
return;
|
|
3877
3871
|
if (replicas.has(room.locator) && revoked(room.locator)) {
|
|
3878
3872
|
await reload(room, local, progress, live);
|
|
3879
3873
|
return;
|
|
@@ -3891,21 +3885,53 @@ export function composeRuntime(options) {
|
|
|
3891
3885
|
reloadsDue.delete(room.locator);
|
|
3892
3886
|
if (!live() || authorization === undefined)
|
|
3893
3887
|
return;
|
|
3888
|
+
const from = standingOf(room.locator);
|
|
3894
3889
|
const bootstrapped = await pullBootstrap(authorization, room, local, live);
|
|
3895
|
-
if (!live())
|
|
3896
|
-
return;
|
|
3897
3890
|
if (bootstrapped.kind !== 'staged') {
|
|
3898
|
-
|
|
3891
|
+
if (live())
|
|
3892
|
+
await withhold(bootstrapped, room, local, progress);
|
|
3899
3893
|
return;
|
|
3900
3894
|
}
|
|
3901
|
-
await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
|
|
3895
|
+
await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from }, live);
|
|
3902
3896
|
await caughtUp(room, local, progress, live);
|
|
3903
3897
|
}
|
|
3904
3898
|
finally {
|
|
3905
3899
|
caughtUpTo(room.locator);
|
|
3906
3900
|
}
|
|
3907
3901
|
}
|
|
3908
|
-
async function
|
|
3902
|
+
async function asked(authorized) {
|
|
3903
|
+
try {
|
|
3904
|
+
return (await options.adapter.discover?.(authorized)) ?? { kind: 'transport-uncertainty' };
|
|
3905
|
+
}
|
|
3906
|
+
catch {
|
|
3907
|
+
return { kind: 'transport-uncertainty' };
|
|
3908
|
+
}
|
|
3909
|
+
}
|
|
3910
|
+
function malformed(listed) {
|
|
3911
|
+
for (const room of listed) {
|
|
3912
|
+
if (room.operations === undefined ||
|
|
3913
|
+
!['write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
|
|
3914
|
+
room.operations[operation].every((scope) => typeof scope === 'string')) ||
|
|
3915
|
+
room.replica === undefined ||
|
|
3916
|
+
typeof room.replica.selection !== 'string' ||
|
|
3917
|
+
room.replica.selection.length === 0 ||
|
|
3918
|
+
!['admitted', 'unavailable'].includes(room.replica.admission) ||
|
|
3919
|
+
!['locked', 'open'].includes(room.replica.offline)) {
|
|
3920
|
+
return `invalid model or replica declaration ${room.locator}`;
|
|
3921
|
+
}
|
|
3922
|
+
if (!declaredSelection(room))
|
|
3923
|
+
return `invalid typed selection declaration ${room.locator}`;
|
|
3924
|
+
}
|
|
3925
|
+
return null;
|
|
3926
|
+
}
|
|
3927
|
+
async function signIn(local, signedIn) {
|
|
3928
|
+
if (JSON.stringify(signedIn) === JSON.stringify(member))
|
|
3929
|
+
return;
|
|
3930
|
+
await local.fact('member', signedIn);
|
|
3931
|
+
member = signedIn;
|
|
3932
|
+
emitMember();
|
|
3933
|
+
}
|
|
3934
|
+
async function discoverRooms(local, progress, attempt, live, found) {
|
|
3909
3935
|
if (closed || authorization === undefined) {
|
|
3910
3936
|
notifyReady();
|
|
3911
3937
|
return;
|
|
@@ -3913,13 +3939,9 @@ export function composeRuntime(options) {
|
|
|
3913
3939
|
moveTo(transferring(status));
|
|
3914
3940
|
discoveries += 1;
|
|
3915
3941
|
const started = discoveries;
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
}
|
|
3920
|
-
catch {
|
|
3921
|
-
result = { kind: 'transport-uncertainty' };
|
|
3922
|
-
}
|
|
3942
|
+
const result = found ?? (await asked(authorization));
|
|
3943
|
+
if (result.kind === 'discovery')
|
|
3944
|
+
roomsNamed = true;
|
|
3923
3945
|
if (!live()) {
|
|
3924
3946
|
progress.complete = false;
|
|
3925
3947
|
notifyReady();
|
|
@@ -3927,12 +3949,7 @@ export function composeRuntime(options) {
|
|
|
3927
3949
|
}
|
|
3928
3950
|
journal.record(`connection discover ${result.kind}`);
|
|
3929
3951
|
if (result.kind === 'transport-uncertainty') {
|
|
3930
|
-
|
|
3931
|
-
await retainModel(local, room, 'collection', cached(modelOf(room.locator)));
|
|
3932
|
-
if (room.typedSelection !== null) {
|
|
3933
|
-
await retainModel(local, room, 'typed-selection', cached(modelOf(room.locator, 'typed-selection')));
|
|
3934
|
-
}
|
|
3935
|
-
}
|
|
3952
|
+
await cachedModels(local);
|
|
3936
3953
|
progress.reachable = false;
|
|
3937
3954
|
progress.complete = false;
|
|
3938
3955
|
notifyReady();
|
|
@@ -3953,26 +3970,12 @@ export function composeRuntime(options) {
|
|
|
3953
3970
|
notifyReady();
|
|
3954
3971
|
return;
|
|
3955
3972
|
}
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
room.replica.selection.length === 0 ||
|
|
3963
|
-
!['admitted', 'unavailable'].includes(room.replica.admission) ||
|
|
3964
|
-
!['locked', 'open'].includes(room.replica.offline)) {
|
|
3965
|
-
journal.record(`invalid model or replica declaration ${room.locator}`);
|
|
3966
|
-
await refuse('missing-required-guarantee');
|
|
3967
|
-
notifyReady();
|
|
3968
|
-
return;
|
|
3969
|
-
}
|
|
3970
|
-
if (!declaredSelection(room)) {
|
|
3971
|
-
journal.record(`invalid typed selection declaration ${room.locator}`);
|
|
3972
|
-
await refuse('missing-required-guarantee');
|
|
3973
|
-
notifyReady();
|
|
3974
|
-
return;
|
|
3975
|
-
}
|
|
3973
|
+
const invalid = malformed(result.rooms);
|
|
3974
|
+
if (invalid !== null) {
|
|
3975
|
+
journal.record(invalid);
|
|
3976
|
+
await refuse('missing-required-guarantee');
|
|
3977
|
+
notifyReady();
|
|
3978
|
+
return;
|
|
3976
3979
|
}
|
|
3977
3980
|
if (refusal !== null) {
|
|
3978
3981
|
refusal = null;
|
|
@@ -3980,7 +3983,6 @@ export function composeRuntime(options) {
|
|
|
3980
3983
|
}
|
|
3981
3984
|
const previousCapabilities = capabilities;
|
|
3982
3985
|
capabilities = result.capabilities;
|
|
3983
|
-
outcomeRetention = result.retention.outcome;
|
|
3984
3986
|
notifyReady();
|
|
3985
3987
|
if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
|
|
3986
3988
|
JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
|
|
@@ -3988,13 +3990,8 @@ export function composeRuntime(options) {
|
|
|
3988
3990
|
await local.fact('providerLimits', limits);
|
|
3989
3991
|
await local.fact('capabilities', capabilities);
|
|
3990
3992
|
}
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
await local.fact('member', signedIn);
|
|
3994
|
-
member = signedIn;
|
|
3995
|
-
emitMember();
|
|
3996
|
-
}
|
|
3997
|
-
await synchronize(await applyDiscovery(result.rooms, started, result.covered), local, progress, attempt, live);
|
|
3993
|
+
await signIn(local, result.member ?? null);
|
|
3994
|
+
await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
|
|
3998
3995
|
}
|
|
3999
3996
|
async function ensureAuthorization() {
|
|
4000
3997
|
if (authorization !== undefined)
|
|
@@ -4009,23 +4006,45 @@ export function composeRuntime(options) {
|
|
|
4009
4006
|
}
|
|
4010
4007
|
}
|
|
4011
4008
|
async function admittedRead(work) {
|
|
4012
|
-
|
|
4013
|
-
|
|
4009
|
+
while (roomReads.free === 0) {
|
|
4010
|
+
await new Promise((admit) => roomReads.waiting.push(admit));
|
|
4014
4011
|
}
|
|
4015
|
-
|
|
4016
|
-
while (reads.free === 0)
|
|
4017
|
-
await new Promise((admit) => reads.waiting.push(admit));
|
|
4018
|
-
reads.free -= 1;
|
|
4012
|
+
roomReads.free -= 1;
|
|
4019
4013
|
try {
|
|
4020
4014
|
await work();
|
|
4021
4015
|
}
|
|
4022
4016
|
finally {
|
|
4023
|
-
|
|
4024
|
-
|
|
4017
|
+
roomReads.free += 1;
|
|
4018
|
+
roomReads.waiting.shift()?.();
|
|
4025
4019
|
}
|
|
4026
4020
|
}
|
|
4021
|
+
function inRoom(locator, work) {
|
|
4022
|
+
roomWork += 1;
|
|
4023
|
+
const ran = (roomLanes.get(locator) ?? Promise.resolve()).then(async () => {
|
|
4024
|
+
try {
|
|
4025
|
+
return await work(continuing);
|
|
4026
|
+
}
|
|
4027
|
+
finally {
|
|
4028
|
+
roomWork -= 1;
|
|
4029
|
+
}
|
|
4030
|
+
});
|
|
4031
|
+
const chain = ran.then(() => undefined, () => undefined);
|
|
4032
|
+
roomLanes.set(locator, chain);
|
|
4033
|
+
working.add(chain);
|
|
4034
|
+
void chain.finally(() => {
|
|
4035
|
+
working.delete(chain);
|
|
4036
|
+
if (roomLanes.get(locator) === chain)
|
|
4037
|
+
roomLanes.delete(locator);
|
|
4038
|
+
});
|
|
4039
|
+
return ran;
|
|
4040
|
+
}
|
|
4027
4041
|
function roomTask(locator, work) {
|
|
4028
|
-
|
|
4042
|
+
if (stopping)
|
|
4043
|
+
return Promise.resolve();
|
|
4044
|
+
return inRoom(locator, (live) => admittedRead(() => work(live))).catch(() => undefined);
|
|
4045
|
+
}
|
|
4046
|
+
function advanced(room, local, authorized) {
|
|
4047
|
+
return inRoom(room.locator, (live) => advance(room, local, authorized, live));
|
|
4029
4048
|
}
|
|
4030
4049
|
function task(lane, work) {
|
|
4031
4050
|
if (stopping)
|
|
@@ -4041,23 +4060,19 @@ export function composeRuntime(options) {
|
|
|
4041
4060
|
void run.finally(() => working.delete(run));
|
|
4042
4061
|
return run;
|
|
4043
4062
|
}
|
|
4044
|
-
function sync(attempt) {
|
|
4063
|
+
function sync(attempt, found) {
|
|
4045
4064
|
return task(OPENING_LANE, async (live) => {
|
|
4046
4065
|
if (!live()) {
|
|
4047
|
-
|
|
4066
|
+
readyFromStore();
|
|
4048
4067
|
return;
|
|
4049
4068
|
}
|
|
4050
4069
|
const progress = { complete: true, reachable: true };
|
|
4051
|
-
|
|
4052
|
-
if (attempt === 'opened')
|
|
4053
|
-
opened = generation;
|
|
4054
|
-
underway = attempt === 'opened' ? { generation, progress } : null;
|
|
4070
|
+
underway = attempt === 'opened' ? progress : null;
|
|
4055
4071
|
if (attempt === 'opened')
|
|
4056
4072
|
streamed = progress;
|
|
4057
4073
|
complete = false;
|
|
4058
4074
|
recorded = false;
|
|
4059
4075
|
moveTo(transferring(status));
|
|
4060
|
-
handed = false;
|
|
4061
4076
|
try {
|
|
4062
4077
|
const local = await openLocal();
|
|
4063
4078
|
if (!live())
|
|
@@ -4065,7 +4080,7 @@ export function composeRuntime(options) {
|
|
|
4065
4080
|
await ensureAuthorization();
|
|
4066
4081
|
if (!live())
|
|
4067
4082
|
return;
|
|
4068
|
-
await discoverRooms(local, progress, attempt, live);
|
|
4083
|
+
await discoverRooms(local, progress, attempt, live, found);
|
|
4069
4084
|
}
|
|
4070
4085
|
catch {
|
|
4071
4086
|
progress.complete = false;
|
|
@@ -4073,7 +4088,6 @@ export function composeRuntime(options) {
|
|
|
4073
4088
|
progress.reachable = false;
|
|
4074
4089
|
}
|
|
4075
4090
|
finally {
|
|
4076
|
-
gate.release();
|
|
4077
4091
|
notifyReady();
|
|
4078
4092
|
}
|
|
4079
4093
|
if (!live())
|
|
@@ -4085,35 +4099,59 @@ export function composeRuntime(options) {
|
|
|
4085
4099
|
if (handed || closed || progress.reachable)
|
|
4086
4100
|
return;
|
|
4087
4101
|
underway = null;
|
|
4088
|
-
opened = null;
|
|
4089
|
-
journal.record('connection reopen');
|
|
4090
4102
|
await rest(progress, live);
|
|
4091
|
-
options.adapter.roomStream?.reopen();
|
|
4092
4103
|
});
|
|
4093
4104
|
}
|
|
4094
|
-
function
|
|
4105
|
+
function resynchronize() {
|
|
4095
4106
|
return task(OPENING_LANE, async (live) => {
|
|
4096
|
-
if (!live() || !
|
|
4107
|
+
if (!live() || !announced)
|
|
4097
4108
|
return;
|
|
4098
4109
|
const progress = { complete: true, reachable: true };
|
|
4099
|
-
const gate = discover();
|
|
4100
4110
|
moveTo(transferring(status));
|
|
4101
4111
|
try {
|
|
4102
4112
|
const local = await openLocal();
|
|
4103
4113
|
if (!live())
|
|
4104
4114
|
return;
|
|
4105
|
-
await
|
|
4115
|
+
await synchronize([...discovered.values()], local, progress, 'unstreamed', live);
|
|
4116
|
+
}
|
|
4117
|
+
catch {
|
|
4118
|
+
progress.complete = false;
|
|
4119
|
+
}
|
|
4120
|
+
if (live())
|
|
4121
|
+
await finishOpening(progress, live);
|
|
4122
|
+
});
|
|
4123
|
+
}
|
|
4124
|
+
function countAuthorityChange() {
|
|
4125
|
+
authorityChanges += 1;
|
|
4126
|
+
for (const listener of authorityChangeListeners)
|
|
4127
|
+
listener(authorityChanges);
|
|
4128
|
+
}
|
|
4129
|
+
function changeAuthority(change) {
|
|
4130
|
+
return task(OPENING_LANE, async (live) => {
|
|
4131
|
+
if (!live() || !announced)
|
|
4132
|
+
return;
|
|
4133
|
+
countAuthorityChange();
|
|
4134
|
+
const progress = { complete: true, reachable: true };
|
|
4135
|
+
moveTo(transferring(status));
|
|
4136
|
+
try {
|
|
4137
|
+
const local = await openLocal();
|
|
4106
4138
|
if (!live())
|
|
4107
4139
|
return;
|
|
4108
|
-
|
|
4140
|
+
const invalid = malformed(change.rooms);
|
|
4141
|
+
if (invalid !== null) {
|
|
4142
|
+
journal.record(invalid);
|
|
4143
|
+
await refuse('missing-required-guarantee');
|
|
4144
|
+
return;
|
|
4145
|
+
}
|
|
4146
|
+
if (change.member !== undefined)
|
|
4147
|
+
await signIn(local, change.member);
|
|
4148
|
+
discoveries += 1;
|
|
4149
|
+
const listed = await applyDiscovery(change.rooms, discoveries, change.left);
|
|
4150
|
+
await synchronize(listed, local, progress, 'authorized', live);
|
|
4109
4151
|
}
|
|
4110
4152
|
catch {
|
|
4111
4153
|
progress.complete = false;
|
|
4112
4154
|
}
|
|
4113
|
-
finally {
|
|
4114
|
-
gate.release();
|
|
4115
|
-
notifyReady();
|
|
4116
|
-
}
|
|
4117
4155
|
if (!live())
|
|
4118
4156
|
return;
|
|
4119
4157
|
if (!progress.complete)
|
|
@@ -4121,14 +4159,17 @@ export function composeRuntime(options) {
|
|
|
4121
4159
|
await rest(progress, live);
|
|
4122
4160
|
});
|
|
4123
4161
|
}
|
|
4124
|
-
function closedSocket(revoked, lost) {
|
|
4125
|
-
|
|
4162
|
+
function closedSocket(revoked, lost, authorityChanged) {
|
|
4163
|
+
notifyReady();
|
|
4164
|
+
const opened = announced;
|
|
4126
4165
|
socketOpen = false;
|
|
4166
|
+
announced = false;
|
|
4167
|
+
missedChange ||= authorityChanged;
|
|
4168
|
+
handed = false;
|
|
4169
|
+
naming = 0;
|
|
4127
4170
|
connectionLost();
|
|
4128
|
-
if (lost)
|
|
4129
|
-
reads.lost();
|
|
4171
|
+
if (lost)
|
|
4130
4172
|
conversations.lost();
|
|
4131
|
-
}
|
|
4132
4173
|
if (revoked) {
|
|
4133
4174
|
void refuse('revoked-authorization');
|
|
4134
4175
|
return;
|
|
@@ -4139,18 +4180,32 @@ export function composeRuntime(options) {
|
|
|
4139
4180
|
moveTo({
|
|
4140
4181
|
...status,
|
|
4141
4182
|
state: status.state === 'synced' ? 'syncing' : status.state,
|
|
4142
|
-
providerUnreachable: status.providerUnreachable || !opened,
|
|
4183
|
+
providerUnreachable: status.providerUnreachable || (!opened && lost),
|
|
4184
|
+
});
|
|
4185
|
+
void closing(opened);
|
|
4186
|
+
}
|
|
4187
|
+
function closing(opened) {
|
|
4188
|
+
return task(OPENING_LANE, async (live) => {
|
|
4189
|
+
if (!live())
|
|
4190
|
+
return;
|
|
4191
|
+
const local = await openLocal();
|
|
4192
|
+
if (!opened && !socketOpen)
|
|
4193
|
+
await cachedModels(local);
|
|
4194
|
+
await rest(null, live);
|
|
4143
4195
|
});
|
|
4144
4196
|
}
|
|
4145
|
-
function
|
|
4146
|
-
|
|
4197
|
+
function cachedModels(local) {
|
|
4198
|
+
return settleModels([...rooms.values()].map((room) => discovered.get(room.locator) ?? room), 'cached', local);
|
|
4199
|
+
}
|
|
4200
|
+
function caughtUpRooms(expired) {
|
|
4201
|
+
const progress = underway;
|
|
4202
|
+
underway = null;
|
|
4203
|
+
naming = Math.max(0, naming - 1);
|
|
4147
4204
|
const cycle = progress ?? { complete: true, reachable: true };
|
|
4148
|
-
|
|
4149
|
-
opened = null;
|
|
4205
|
+
replaced(expired);
|
|
4150
4206
|
const reloading = progress === null ? [] : owedReloads(expired);
|
|
4151
4207
|
const replacing = reloadNow((room) => expired.includes(room.locator) || reloading.includes(room.locator), cycle);
|
|
4152
4208
|
return task(OPENING_LANE, async (live) => {
|
|
4153
|
-
underway = null;
|
|
4154
4209
|
if (!live())
|
|
4155
4210
|
return;
|
|
4156
4211
|
if (progress === null) {
|
|
@@ -4158,24 +4213,14 @@ export function composeRuntime(options) {
|
|
|
4158
4213
|
await rest(cycle, live);
|
|
4159
4214
|
return;
|
|
4160
4215
|
}
|
|
4161
|
-
await roomsCarried([...expired, ...reloading]);
|
|
4162
|
-
if (!live())
|
|
4163
|
-
return;
|
|
4164
4216
|
try {
|
|
4165
4217
|
const local = await openLocal();
|
|
4166
|
-
|
|
4218
|
+
await replacing;
|
|
4167
4219
|
if (progress.reachable) {
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
!reloading.includes(room.locator)) {
|
|
4172
|
-
await confirmModels(room, local);
|
|
4173
|
-
}
|
|
4174
|
-
}
|
|
4220
|
+
await settleModels([...discovered.values()].filter((room) => replicas.has(room.locator) &&
|
|
4221
|
+
!expired.includes(room.locator) &&
|
|
4222
|
+
!reloading.includes(room.locator)), 'current', local);
|
|
4175
4223
|
}
|
|
4176
|
-
const failed = reloaded.find((outcome) => outcome.status === 'rejected');
|
|
4177
|
-
if (failed !== undefined)
|
|
4178
|
-
throw failed.reason;
|
|
4179
4224
|
}
|
|
4180
4225
|
catch {
|
|
4181
4226
|
progress.complete = false;
|
|
@@ -4187,11 +4232,8 @@ export function composeRuntime(options) {
|
|
|
4187
4232
|
}
|
|
4188
4233
|
function owedReloads(expired) {
|
|
4189
4234
|
const reloading = [...discovered.values()]
|
|
4190
|
-
.filter((room) => replicas.has(room.locator) &&
|
|
4191
|
-
!expired.includes(room.locator) &&
|
|
4192
|
-
(owed.has(room.locator) || revoked(room.locator)))
|
|
4235
|
+
.filter((room) => replicas.has(room.locator) && !expired.includes(room.locator) && revoked(room.locator))
|
|
4193
4236
|
.map((room) => room.locator);
|
|
4194
|
-
owed.clear();
|
|
4195
4237
|
for (const locator of reloading)
|
|
4196
4238
|
catching.add(locator);
|
|
4197
4239
|
liveQueries.recover();
|
|
@@ -4200,24 +4242,43 @@ export function composeRuntime(options) {
|
|
|
4200
4242
|
async function reloadNow(replaced, progress) {
|
|
4201
4243
|
const reloaded = [...discovered.values()].filter((room) => replicas.has(room.locator) && replaced(room));
|
|
4202
4244
|
for (const room of reloaded)
|
|
4203
|
-
reloadsDue.
|
|
4245
|
+
reloadsDue.add(room.locator);
|
|
4204
4246
|
try {
|
|
4205
4247
|
const local = await openLocal();
|
|
4206
|
-
|
|
4248
|
+
for (const room of reloaded) {
|
|
4249
|
+
void roomTask(room.locator, (live) => reload(room, local, progress, live)).then(() => rest(progress));
|
|
4250
|
+
}
|
|
4207
4251
|
}
|
|
4208
4252
|
catch {
|
|
4209
4253
|
for (const room of reloaded)
|
|
4210
4254
|
reloadsDue.delete(room.locator);
|
|
4211
4255
|
progress.reachable = false;
|
|
4212
|
-
return [];
|
|
4213
4256
|
}
|
|
4214
4257
|
}
|
|
4215
|
-
async function
|
|
4216
|
-
|
|
4217
|
-
const model = modelOf(room.locator
|
|
4218
|
-
|
|
4219
|
-
|
|
4258
|
+
async function settleModels(held, status, local) {
|
|
4259
|
+
const owed = (room) => {
|
|
4260
|
+
const model = modelOf(room.locator);
|
|
4261
|
+
return model.status !== 'unavailable' && model.status !== status;
|
|
4262
|
+
};
|
|
4263
|
+
const settled = await exclusive(async () => {
|
|
4264
|
+
if (!continuing())
|
|
4265
|
+
return [];
|
|
4266
|
+
const unchanged = held.filter((room) => owed(room) && declaredAsHeld(room));
|
|
4267
|
+
for (const room of unchanged) {
|
|
4268
|
+
const next = { ...modelOf(room.locator), status };
|
|
4269
|
+
const replica = replicas.get(room.locator);
|
|
4270
|
+
if (replica !== undefined)
|
|
4271
|
+
replicas.set(room.locator, { ...replica, record: { ...replica.record, model: next } });
|
|
4272
|
+
heldModel(room, next);
|
|
4220
4273
|
}
|
|
4274
|
+
if (unchanged.length > 0)
|
|
4275
|
+
emitRooms();
|
|
4276
|
+
return unchanged;
|
|
4277
|
+
});
|
|
4278
|
+
for (const room of held) {
|
|
4279
|
+
if (settled.includes(room) || !owed(room))
|
|
4280
|
+
continue;
|
|
4281
|
+
await retainModel(local, room, { ...modelOf(room.locator), status });
|
|
4221
4282
|
}
|
|
4222
4283
|
}
|
|
4223
4284
|
function submitLater(locator) {
|
|
@@ -4262,14 +4323,10 @@ export function composeRuntime(options) {
|
|
|
4262
4323
|
try {
|
|
4263
4324
|
const local = await openLocal();
|
|
4264
4325
|
await roomTask(locator, async (live) => {
|
|
4265
|
-
|
|
4266
|
-
if (!current() || authorization === undefined)
|
|
4326
|
+
if (!live() || authorization === undefined)
|
|
4267
4327
|
return;
|
|
4268
|
-
const withheld = await
|
|
4269
|
-
|
|
4270
|
-
abandonedOn(signal),
|
|
4271
|
-
]);
|
|
4272
|
-
if (withheld !== null && current())
|
|
4328
|
+
const withheld = await advance(room, local, authorization, live);
|
|
4329
|
+
if (withheld !== null && live())
|
|
4273
4330
|
await withhold(withheld, room, local, progress);
|
|
4274
4331
|
});
|
|
4275
4332
|
}
|
|
@@ -4324,14 +4381,11 @@ export function composeRuntime(options) {
|
|
|
4324
4381
|
progress.complete = false;
|
|
4325
4382
|
return;
|
|
4326
4383
|
}
|
|
4327
|
-
reads.applied();
|
|
4328
4384
|
submitAll();
|
|
4329
4385
|
for (const locator of stale)
|
|
4330
4386
|
if (repairable.has(locator))
|
|
4331
4387
|
repairLater(locator, {});
|
|
4332
4388
|
await conversations.reconcile();
|
|
4333
|
-
if (status.providerUnreachable)
|
|
4334
|
-
conversations.reconnected();
|
|
4335
4389
|
const reading = { complete: true, reachable: true };
|
|
4336
4390
|
await conversations.catchUp(reading);
|
|
4337
4391
|
if (progress.reachable && reading.reachable)
|
|
@@ -4357,7 +4411,7 @@ export function composeRuntime(options) {
|
|
|
4357
4411
|
if (!live())
|
|
4358
4412
|
return;
|
|
4359
4413
|
let lastCompleted = status.lastCompleted;
|
|
4360
|
-
let synced = complete && stale.size === 0 && !pending();
|
|
4414
|
+
let synced = complete && roomWork === 0 && naming === 0 && stale.size === 0 && !pending();
|
|
4361
4415
|
if (synced && !recorded) {
|
|
4362
4416
|
const completedAt = new Date().toISOString();
|
|
4363
4417
|
try {
|
|
@@ -4372,74 +4426,69 @@ export function composeRuntime(options) {
|
|
|
4372
4426
|
}
|
|
4373
4427
|
}
|
|
4374
4428
|
moveTo({
|
|
4375
|
-
state: pending()
|
|
4429
|
+
state: pending()
|
|
4430
|
+
? 'pending'
|
|
4431
|
+
: synced && complete && roomWork === 0 && naming === 0
|
|
4432
|
+
? 'synced'
|
|
4433
|
+
: 'syncing',
|
|
4376
4434
|
lastCompleted,
|
|
4377
|
-
providerUnreachable: !progress.reachable,
|
|
4435
|
+
providerUnreachable: progress === null ? status.providerUnreachable : !progress.reachable,
|
|
4378
4436
|
...attention(),
|
|
4379
4437
|
});
|
|
4380
4438
|
}
|
|
4381
|
-
function
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
}
|
|
4389
|
-
function applyRoom(locator, roomVersion) {
|
|
4390
|
-
announced.set(locator, roomVersion);
|
|
4391
|
-
return task(roomLane(locator), async (live) => {
|
|
4392
|
-
if (discovering?.generation === generation)
|
|
4393
|
-
await discovering.done;
|
|
4394
|
-
const saving = answering.get(locator);
|
|
4395
|
-
if (saving?.generation === generation)
|
|
4396
|
-
await saving.answered;
|
|
4397
|
-
if (!live() || authorization === undefined)
|
|
4398
|
-
return;
|
|
4399
|
-
const room = discovered.get(locator);
|
|
4400
|
-
const held = replicas.get(locator);
|
|
4401
|
-
if (room === undefined || room.replica.admission !== 'admitted')
|
|
4402
|
-
return;
|
|
4403
|
-
if (held !== undefined &&
|
|
4404
|
-
(held.record.replica.selection !== room.replica.selection ||
|
|
4405
|
-
options.adapter.reached(locator, held.record.roomVersion, roomVersion))) {
|
|
4406
|
-
return;
|
|
4407
|
-
}
|
|
4408
|
-
const during = underway?.generation === generation ? underway.progress : null;
|
|
4409
|
-
const progress = during ?? { complete: true, reachable: true };
|
|
4410
|
-
if (during === null)
|
|
4411
|
-
moveTo(transferring(status));
|
|
4439
|
+
function applyRoom(change) {
|
|
4440
|
+
const { locator } = change;
|
|
4441
|
+
if (stopping)
|
|
4442
|
+
return Promise.resolve();
|
|
4443
|
+
return inRoom(locator, async (live) => {
|
|
4444
|
+
await answering.get(locator)?.answered;
|
|
4445
|
+
const progress = underway ?? { complete: true, reachable: true };
|
|
4412
4446
|
try {
|
|
4413
4447
|
const local = await openLocal();
|
|
4414
|
-
|
|
4415
|
-
if (!live()
|
|
4416
|
-
return;
|
|
4417
|
-
const
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4448
|
+
for (;;) {
|
|
4449
|
+
if (!live())
|
|
4450
|
+
return null;
|
|
4451
|
+
const room = discovered.get(locator);
|
|
4452
|
+
const held = replicas.get(locator);
|
|
4453
|
+
if (room === undefined ||
|
|
4454
|
+
held === undefined ||
|
|
4455
|
+
room.replica.admission !== 'admitted' ||
|
|
4456
|
+
held.record.replica.selection !== room.replica.selection) {
|
|
4457
|
+
return null;
|
|
4458
|
+
}
|
|
4459
|
+
const at = held.record.roomVersion;
|
|
4460
|
+
if (options.adapter.reached(locator, at, change.roomVersion))
|
|
4461
|
+
return progress;
|
|
4462
|
+
if (!options.adapter.reached(locator, at, change.from)) {
|
|
4463
|
+
name(locator);
|
|
4464
|
+
return progress;
|
|
4465
|
+
}
|
|
4466
|
+
if (underway === null)
|
|
4467
|
+
moveTo(transferring(status));
|
|
4468
|
+
const writing = () => live() && !full;
|
|
4469
|
+
const refreshed = await ahead((hold) => pullRefresh(authorization ?? {}, held, writing, hold, () => change.read()));
|
|
4470
|
+
if (refreshed.kind !== 'advanced')
|
|
4471
|
+
return progress;
|
|
4472
|
+
const landing = {
|
|
4473
|
+
kind: 'refreshed',
|
|
4474
|
+
from: refreshed.from,
|
|
4475
|
+
remodelled: refreshed.remodelled,
|
|
4476
|
+
};
|
|
4477
|
+
if (await publish(local, room, refreshed.record, landing, writing)) {
|
|
4478
|
+
stale.delete(locator);
|
|
4479
|
+
await retainModel(local, room, modelOf(locator));
|
|
4480
|
+
return progress;
|
|
4481
|
+
}
|
|
4482
|
+
}
|
|
4421
4483
|
}
|
|
4422
4484
|
catch {
|
|
4485
|
+
if (!stale.has(locator))
|
|
4486
|
+
name(locator);
|
|
4423
4487
|
stale.add(locator);
|
|
4488
|
+
return progress;
|
|
4424
4489
|
}
|
|
4425
|
-
|
|
4426
|
-
if (during === null)
|
|
4427
|
-
await rest(progress, live);
|
|
4428
|
-
}
|
|
4429
|
-
});
|
|
4490
|
+
}).then((progress) => rest(progress), () => undefined);
|
|
4430
4491
|
}
|
|
4431
|
-
function roomsCarried(replacing) {
|
|
4432
|
-
const replaced = new Set(replacing.map(roomLane));
|
|
4433
|
-
return Promise.all([...lanes]
|
|
4434
|
-
.filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
|
|
4435
|
-
.map(([, held]) => held.chain));
|
|
4436
|
-
}
|
|
4437
|
-
const receipts = {
|
|
4438
|
-
heads: async () => (await openLocal()).receivedHeads(),
|
|
4439
|
-
parts: async (key, count) => (await openLocal()).receivedParts(key, count),
|
|
4440
|
-
keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
|
|
4441
|
-
drop: async (keys) => (await openLocal()).dropReceived(keys),
|
|
4442
|
-
};
|
|
4443
4492
|
function observeStream() {
|
|
4444
4493
|
const stream = options.adapter.roomStream;
|
|
4445
4494
|
if (stream === undefined || withdrawStream !== undefined)
|
|
@@ -4449,29 +4498,33 @@ export function composeRuntime(options) {
|
|
|
4449
4498
|
return;
|
|
4450
4499
|
if (event.kind === 'opened') {
|
|
4451
4500
|
socketOpen = true;
|
|
4501
|
+
announced = false;
|
|
4502
|
+
handed = false;
|
|
4503
|
+
naming = 0;
|
|
4452
4504
|
connectionLive();
|
|
4453
4505
|
generation += 1;
|
|
4454
|
-
reads.opened();
|
|
4455
4506
|
conversations.reopened();
|
|
4456
4507
|
recovery.resume();
|
|
4457
|
-
|
|
4508
|
+
}
|
|
4509
|
+
else if (event.kind === 'discovered') {
|
|
4510
|
+
if (!socketOpen)
|
|
4511
|
+
return;
|
|
4512
|
+
announced = true;
|
|
4513
|
+
if (missedChange) {
|
|
4514
|
+
missedChange = false;
|
|
4515
|
+
countAuthorityChange();
|
|
4516
|
+
}
|
|
4517
|
+
void sync('opened', event.discovery);
|
|
4458
4518
|
}
|
|
4459
4519
|
else if (event.kind === 'closed')
|
|
4460
|
-
closedSocket(event.revoked, event.lost);
|
|
4461
|
-
else if (event.kind === '
|
|
4462
|
-
void
|
|
4520
|
+
closedSocket(event.revoked, event.lost, event.authorityChanged);
|
|
4521
|
+
else if (event.kind === 'caught-up')
|
|
4522
|
+
void caughtUpRooms(event.expired);
|
|
4463
4523
|
else if (event.kind === 'authority-changed')
|
|
4464
|
-
void
|
|
4524
|
+
void changeAuthority(event);
|
|
4465
4525
|
else
|
|
4466
|
-
void applyRoom(event
|
|
4467
|
-
}
|
|
4468
|
-
}
|
|
4469
|
-
async function heldRevisions(locator, paths) {
|
|
4470
|
-
const held = replicas.get(locator);
|
|
4471
|
-
if (held === undefined || paths.length === 0)
|
|
4472
|
-
return new Map();
|
|
4473
|
-
const entries = await (await openLocal()).entries(held.record.key, 'confirmed', paths);
|
|
4474
|
-
return new Map(entries.flatMap(({ path, revision }) => revision === null ? [] : [[path, revision]]));
|
|
4526
|
+
void applyRoom(event);
|
|
4527
|
+
});
|
|
4475
4528
|
}
|
|
4476
4529
|
function writesHeld() {
|
|
4477
4530
|
return serviceMode === 'write-halted' || haltedRefusal;
|
|
@@ -4541,8 +4594,9 @@ export function composeRuntime(options) {
|
|
|
4541
4594
|
if (closed)
|
|
4542
4595
|
return;
|
|
4543
4596
|
closed = true;
|
|
4544
|
-
|
|
4545
|
-
|
|
4597
|
+
notifyDiscovered();
|
|
4598
|
+
if (probing !== null)
|
|
4599
|
+
clearInterval(probing);
|
|
4546
4600
|
connectionLive();
|
|
4547
4601
|
lockListeners.clear();
|
|
4548
4602
|
readinessWatches.clear();
|
|
@@ -4553,16 +4607,14 @@ export function composeRuntime(options) {
|
|
|
4553
4607
|
withdrawServiceMode?.();
|
|
4554
4608
|
conversations.close();
|
|
4555
4609
|
void recovery.retire();
|
|
4556
|
-
store?.close();
|
|
4557
|
-
void opening?.then((opened) => {
|
|
4558
|
-
opened.close();
|
|
4559
|
-
}, () => { });
|
|
4560
4610
|
roomListeners.clear();
|
|
4611
|
+
unlistedRooms.clear();
|
|
4561
4612
|
refusalListeners.clear();
|
|
4562
4613
|
storageLossListeners.clear();
|
|
4563
4614
|
statusListeners.clear();
|
|
4564
4615
|
serviceModeListeners.clear();
|
|
4565
4616
|
memberListeners.clear();
|
|
4617
|
+
authorityChangeListeners.clear();
|
|
4566
4618
|
liveQueries.close();
|
|
4567
4619
|
queueSubscriptions.clear();
|
|
4568
4620
|
roomDraftListeners.clear();
|
|
@@ -4570,6 +4622,7 @@ export function composeRuntime(options) {
|
|
|
4570
4622
|
}
|
|
4571
4623
|
async function stop() {
|
|
4572
4624
|
stopping = true;
|
|
4625
|
+
inFlight.abort();
|
|
4573
4626
|
await recovery.retire();
|
|
4574
4627
|
await Promise.all(working);
|
|
4575
4628
|
await mutations;
|
|
@@ -4583,20 +4636,20 @@ export function composeRuntime(options) {
|
|
|
4583
4636
|
observeStream();
|
|
4584
4637
|
void openLocal().then(() => {
|
|
4585
4638
|
if (capabilities.length > 0)
|
|
4586
|
-
|
|
4639
|
+
readyFromStore();
|
|
4587
4640
|
}, () => {
|
|
4588
|
-
|
|
4641
|
+
readyFromStore();
|
|
4589
4642
|
});
|
|
4590
4643
|
if (options.adapter.roomStream === undefined)
|
|
4591
|
-
void sync('unstreamed');
|
|
4644
|
+
void sync('unstreamed', null);
|
|
4592
4645
|
function planned(input, reach) {
|
|
4593
|
-
const plan =
|
|
4646
|
+
const plan = planWithin(input, roomFacts(reach), planReach(reach));
|
|
4594
4647
|
const named = plan.paths !== null &&
|
|
4595
4648
|
plan.rooms !== null &&
|
|
4596
4649
|
plan.paths.length * plan.rooms.length <= MAXIMUM_WINDOW;
|
|
4597
4650
|
return plan.limit === null && named ? plan : { ...plan, limit: capped(plan.limit) };
|
|
4598
4651
|
}
|
|
4599
|
-
function watchPlan(plan, deliver) {
|
|
4652
|
+
function watchPlan(plan, deliver, fail) {
|
|
4600
4653
|
let seeded = false;
|
|
4601
4654
|
let pending = true;
|
|
4602
4655
|
const shown = () => {
|
|
@@ -4622,7 +4675,7 @@ export function composeRuntime(options) {
|
|
|
4622
4675
|
}
|
|
4623
4676
|
seeded = false;
|
|
4624
4677
|
deliver(message);
|
|
4625
|
-
});
|
|
4678
|
+
}, fail);
|
|
4626
4679
|
return () => {
|
|
4627
4680
|
watchers.delete(shown);
|
|
4628
4681
|
if (watchers.size === 0)
|
|
@@ -4630,86 +4683,59 @@ export function composeRuntime(options) {
|
|
|
4630
4683
|
stop();
|
|
4631
4684
|
};
|
|
4632
4685
|
}
|
|
4633
|
-
async function searched(input, reach
|
|
4686
|
+
async function searched(input, reach) {
|
|
4634
4687
|
const asked = searchPlanOf(input);
|
|
4635
|
-
|
|
4636
|
-
const
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
};
|
|
4679
|
-
const answered = covered(answering);
|
|
4680
|
-
if ('pending' in answered)
|
|
4681
|
-
continue;
|
|
4682
|
-
const searchedConversations = named.filter((one) => searchIndex.conversations().has(one));
|
|
4683
|
-
const seen = searchIndex.changes();
|
|
4684
|
-
await searchIndex.caughtUp();
|
|
4685
|
-
const selected = roomsAlone(answering) ? null : await everyRow(answering);
|
|
4686
|
-
if (selected !== null && 'pending' in selected)
|
|
4687
|
-
continue;
|
|
4688
|
-
if (searchIndex.changes() !== seen ||
|
|
4689
|
-
JSON.stringify(covered(answering)) !== JSON.stringify(answered))
|
|
4690
|
-
continue;
|
|
4691
|
-
const hits = searchIndex.search(plan, selected === null ? null : selected.documents, searchedConversations, answering.rooms !== null
|
|
4692
|
-
? new Set(answering.rooms)
|
|
4693
|
-
: locked.length === 0
|
|
4694
|
-
? null
|
|
4695
|
-
: new Set(reachedRooms(answering)), plan.collection
|
|
4696
|
-
? new Map(answered.coverage.rooms.map(({ room }) => [
|
|
4697
|
-
room,
|
|
4698
|
-
replicas.get(room)?.record.typedSelection?.roots ?? [],
|
|
4699
|
-
]))
|
|
4700
|
-
: null);
|
|
4701
|
-
const coverage = {
|
|
4702
|
-
rooms: answered.coverage.rooms,
|
|
4703
|
-
conversations: searchedConversations,
|
|
4704
|
-
index: searchIndex.generation(),
|
|
4705
|
-
overlay: answered.coverage.overlay,
|
|
4688
|
+
await ready;
|
|
4689
|
+
const search = options.adapter.search;
|
|
4690
|
+
if (search === undefined) {
|
|
4691
|
+
return {
|
|
4692
|
+
kind: 'undeclared',
|
|
4693
|
+
code: 'action-undeclared',
|
|
4694
|
+
fields: { name: 'search' },
|
|
4695
|
+
retryable: false,
|
|
4696
|
+
};
|
|
4697
|
+
}
|
|
4698
|
+
if (closed || refusal !== null || connectionOf() !== 'connected')
|
|
4699
|
+
return OFFLINE;
|
|
4700
|
+
const reached = reach === null ? null : new Set(reach.rooms);
|
|
4701
|
+
const named = asked.rooms === null
|
|
4702
|
+
? reached === null
|
|
4703
|
+
? null
|
|
4704
|
+
: [...reached]
|
|
4705
|
+
: asked.rooms.filter((room) => reached === null || reached.has(room));
|
|
4706
|
+
const rooms = named === null ? null : named.filter((room) => replicas.has(room)).sort();
|
|
4707
|
+
if (rooms !== null && rooms.length === 0)
|
|
4708
|
+
return { kind: 'found', documents: [] };
|
|
4709
|
+
try {
|
|
4710
|
+
await ensureAuthorization();
|
|
4711
|
+
}
|
|
4712
|
+
catch {
|
|
4713
|
+
return UNCERTAIN;
|
|
4714
|
+
}
|
|
4715
|
+
if (closed || authorization === undefined || refusal !== null)
|
|
4716
|
+
return UNCERTAIN;
|
|
4717
|
+
journal.record('transfer search');
|
|
4718
|
+
try {
|
|
4719
|
+
const result = await search(authorization, { text: asked.text, rooms });
|
|
4720
|
+
if (result.kind === 'revoked-authorization')
|
|
4721
|
+
await refuse('revoked-authorization');
|
|
4722
|
+
journal.record(`outcome search ${result.kind}`);
|
|
4723
|
+
if (result.kind === 'rejected')
|
|
4724
|
+
return result;
|
|
4725
|
+
if (result.kind !== 'found')
|
|
4726
|
+
return failure(result);
|
|
4727
|
+
const kept = rooms === null ? null : new Set(rooms);
|
|
4728
|
+
return {
|
|
4729
|
+
kind: 'found',
|
|
4730
|
+
documents: result.documents.filter((hit) => kept === null || kept.has(hit.room)),
|
|
4706
4731
|
};
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4732
|
+
}
|
|
4733
|
+
catch {
|
|
4734
|
+
journal.record('outcome search transport-uncertainty');
|
|
4735
|
+
return UNCERTAIN;
|
|
4710
4736
|
}
|
|
4711
4737
|
}
|
|
4712
|
-
const watch = (topic, input, deliver) => {
|
|
4738
|
+
const watch = (topic, input, deliver, fail) => {
|
|
4713
4739
|
if (topic === 'room-drafts') {
|
|
4714
4740
|
const listener = { notify: deliver, ready: false };
|
|
4715
4741
|
roomDraftListeners.add(listener);
|
|
@@ -4758,12 +4784,13 @@ export function composeRuntime(options) {
|
|
|
4758
4784
|
};
|
|
4759
4785
|
}
|
|
4760
4786
|
if (topic === 'readiness') {
|
|
4761
|
-
return watchReadiness({ target: '', code: 'stored'
|
|
4787
|
+
return watchReadiness({ target: '', code: 'stored' }, null, deliver);
|
|
4762
4788
|
}
|
|
4763
4789
|
if (topic === 'rooms') {
|
|
4764
|
-
|
|
4765
|
-
|
|
4790
|
+
unlistedRooms.add(deliver);
|
|
4791
|
+
emitKnownRooms();
|
|
4766
4792
|
return () => {
|
|
4793
|
+
unlistedRooms.delete(deliver);
|
|
4767
4794
|
roomListeners.delete(deliver);
|
|
4768
4795
|
};
|
|
4769
4796
|
}
|
|
@@ -4802,6 +4829,13 @@ export function composeRuntime(options) {
|
|
|
4802
4829
|
memberListeners.delete(deliver);
|
|
4803
4830
|
};
|
|
4804
4831
|
}
|
|
4832
|
+
if (topic === 'authority-changes') {
|
|
4833
|
+
authorityChangeListeners.add(deliver);
|
|
4834
|
+
deliver(authorityChanges);
|
|
4835
|
+
return () => {
|
|
4836
|
+
authorityChangeListeners.delete(deliver);
|
|
4837
|
+
};
|
|
4838
|
+
}
|
|
4805
4839
|
if (topic === 'queue') {
|
|
4806
4840
|
const queue = { locator: input[0], notify: deliver };
|
|
4807
4841
|
queueSubscriptions.add(queue);
|
|
@@ -4810,24 +4844,34 @@ export function composeRuntime(options) {
|
|
|
4810
4844
|
queueSubscriptions.delete(queue);
|
|
4811
4845
|
};
|
|
4812
4846
|
}
|
|
4813
|
-
return watchPlan(planned(input[0], null), deliver);
|
|
4847
|
+
return watchPlan(planned(input[0], null), deliver, fail);
|
|
4814
4848
|
};
|
|
4815
4849
|
const md = {
|
|
4816
4850
|
loaded() {
|
|
4817
4851
|
return openLocal().then(() => undefined);
|
|
4818
4852
|
},
|
|
4819
4853
|
rooms() {
|
|
4820
|
-
|
|
4854
|
+
if (closed)
|
|
4855
|
+
return Promise.resolve([]);
|
|
4856
|
+
return new Promise((settle) => {
|
|
4857
|
+
const answer = (value) => {
|
|
4858
|
+
unlistedRooms.delete(answer);
|
|
4859
|
+
roomListeners.delete(answer);
|
|
4860
|
+
settle(value);
|
|
4861
|
+
};
|
|
4862
|
+
unlistedRooms.add(answer);
|
|
4863
|
+
emitKnownRooms();
|
|
4864
|
+
});
|
|
4821
4865
|
},
|
|
4822
4866
|
query: ((input) => rowsQuery(planned(input, null))),
|
|
4823
|
-
search: (input) => searched(input, null
|
|
4867
|
+
search: (input) => searched(input, null),
|
|
4824
4868
|
types(locator) {
|
|
4825
4869
|
return {
|
|
4826
4870
|
collection: modelTypes(modelOf(locator)),
|
|
4827
4871
|
typedSelection: rooms.get(locator)?.typedSelection === undefined ||
|
|
4828
4872
|
rooms.get(locator)?.typedSelection === null
|
|
4829
4873
|
? null
|
|
4830
|
-
: modelTypes(
|
|
4874
|
+
: modelTypes(builtIn),
|
|
4831
4875
|
};
|
|
4832
4876
|
},
|
|
4833
4877
|
roomDraft: {
|
|
@@ -4844,33 +4888,17 @@ export function composeRuntime(options) {
|
|
|
4844
4888
|
const record = roomDrafts.get(room)?.get(path);
|
|
4845
4889
|
return record === undefined ? null : ((await roomDraftsOf([record]))[0] ?? null);
|
|
4846
4890
|
},
|
|
4847
|
-
write(room, path, revision, input) {
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
const
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
? undefined
|
|
4859
|
-
: await heldFile(parts.file.blob, binaryBase(await visibleOf(room, [path]), path));
|
|
4860
|
-
const record = {
|
|
4861
|
-
locator: room,
|
|
4862
|
-
path,
|
|
4863
|
-
revision: roomDraftRevision(),
|
|
4864
|
-
parts: held(current?.parts, parts, file),
|
|
4865
|
-
returned: current?.returned ?? null,
|
|
4866
|
-
};
|
|
4867
|
-
await local.keepRoomDraft(record, parts.file === undefined || file === undefined
|
|
4868
|
-
? []
|
|
4869
|
-
: [{ key: file.localId, blob: parts.file.blob }]);
|
|
4870
|
-
rememberRoomDraft(record);
|
|
4871
|
-
publishChangeSet(changeSet(room, { drafts: [path] }));
|
|
4872
|
-
return { outcome: 'written', roomDraft: (await roomDraftsOf([record]))[0] ?? null };
|
|
4873
|
-
});
|
|
4891
|
+
write(room, path, revision, input, next = roomDraftRevision()) {
|
|
4892
|
+
if (typeof next !== 'string' || next === '')
|
|
4893
|
+
return Promise.reject(runtimeError('room-draft-malformed'));
|
|
4894
|
+
return gathered(`room-draft\u0000${room}\u0000${path}`, { revision, input, next }, (earlier, later) => {
|
|
4895
|
+
if (later.revision !== earlier.next)
|
|
4896
|
+
return null;
|
|
4897
|
+
const parts = gatheredParts(earlier.input, later.input);
|
|
4898
|
+
return parts === null
|
|
4899
|
+
? null
|
|
4900
|
+
: { revision: earlier.revision, input: parts, next: later.next };
|
|
4901
|
+
}, (written) => writtenRoomDraft(room, path, written.revision, written.input, written.next));
|
|
4874
4902
|
},
|
|
4875
4903
|
discard(room, path, revision) {
|
|
4876
4904
|
return exclusive(async () => {
|
|
@@ -4897,13 +4925,13 @@ export function composeRuntime(options) {
|
|
|
4897
4925
|
});
|
|
4898
4926
|
},
|
|
4899
4927
|
write(app, value) {
|
|
4900
|
-
return
|
|
4928
|
+
return exclusive(() => keptPresentation(app, () => structuredClone(value)));
|
|
4901
4929
|
},
|
|
4902
4930
|
edit(app, members) {
|
|
4903
|
-
return
|
|
4904
|
-
...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(
|
|
4905
|
-
...Object.entries(
|
|
4906
|
-
]));
|
|
4931
|
+
return gathered(`presentation\u0000${app}`, structuredClone(members), (earlier, later) => ({ ...earlier, ...later }), (edited) => keptPresentation(app, (held) => Object.fromEntries([
|
|
4932
|
+
...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(edited, member)),
|
|
4933
|
+
...Object.entries(edited).filter(([, value]) => value !== null),
|
|
4934
|
+
])));
|
|
4907
4935
|
},
|
|
4908
4936
|
},
|
|
4909
4937
|
save(room, named, writtenBy) {
|
|
@@ -4922,7 +4950,8 @@ export function composeRuntime(options) {
|
|
|
4922
4950
|
part: readPart,
|
|
4923
4951
|
read: readBinaryPath,
|
|
4924
4952
|
link: linkBinaryPath,
|
|
4925
|
-
|
|
4953
|
+
show: showBinaryPath,
|
|
4954
|
+
attach: attachBinary,
|
|
4926
4955
|
},
|
|
4927
4956
|
async call(name, input) {
|
|
4928
4957
|
await ready;
|
|
@@ -4938,14 +4967,17 @@ export function composeRuntime(options) {
|
|
|
4938
4967
|
return journal.export();
|
|
4939
4968
|
},
|
|
4940
4969
|
cleanUp,
|
|
4970
|
+
unsent: () => database.ask({ kind: 'authored.unsent' }),
|
|
4971
|
+
reset: () => options.shell.reset(),
|
|
4941
4972
|
persistence() {
|
|
4942
4973
|
return persistence === 'granted';
|
|
4943
4974
|
},
|
|
4944
4975
|
async retry() {
|
|
4945
4976
|
journal.record('retry');
|
|
4946
|
-
|
|
4977
|
+
unpreparable.clear();
|
|
4947
4978
|
queueReadiness();
|
|
4948
|
-
|
|
4979
|
+
if (full)
|
|
4980
|
+
await probed();
|
|
4949
4981
|
},
|
|
4950
4982
|
observe: observing(watch),
|
|
4951
4983
|
async reauthorize() {
|
|
@@ -4959,7 +4991,7 @@ export function composeRuntime(options) {
|
|
|
4959
4991
|
authorization = undefined;
|
|
4960
4992
|
discoveryPaused = false;
|
|
4961
4993
|
if (options.adapter.roomStream === undefined)
|
|
4962
|
-
await sync('unstreamed');
|
|
4994
|
+
await sync('unstreamed', null);
|
|
4963
4995
|
else
|
|
4964
4996
|
await task(OPENING_LANE, ensureAuthorization);
|
|
4965
4997
|
},
|
|
@@ -4967,7 +4999,6 @@ export function composeRuntime(options) {
|
|
|
4967
4999
|
const reachable = {
|
|
4968
5000
|
md,
|
|
4969
5001
|
watch,
|
|
4970
|
-
referrerScope: (referrer) => options.adapter.referrerScope(referrer),
|
|
4971
5002
|
plan: planned,
|
|
4972
5003
|
query: (plan) => rowsQuery(plan),
|
|
4973
5004
|
watchPlan,
|
|
@@ -4977,12 +5008,12 @@ export function composeRuntime(options) {
|
|
|
4977
5008
|
lockedRooms,
|
|
4978
5009
|
watchLocks,
|
|
4979
5010
|
located,
|
|
5011
|
+
room: (locator) => listed().find((one) => one.locator === locator),
|
|
4980
5012
|
};
|
|
4981
5013
|
return {
|
|
4982
5014
|
md,
|
|
4983
5015
|
seed,
|
|
4984
5016
|
stop,
|
|
4985
|
-
lose: loseRooms,
|
|
4986
5017
|
watch,
|
|
4987
5018
|
persisted(answer) {
|
|
4988
5019
|
if (answer === persistence)
|
|
@@ -4991,6 +5022,10 @@ export function composeRuntime(options) {
|
|
|
4991
5022
|
journal.record(`persistence ${answer}`);
|
|
4992
5023
|
queueReadiness();
|
|
4993
5024
|
},
|
|
4994
|
-
within: (reach, port) =>
|
|
5025
|
+
within: (reach, port) => {
|
|
5026
|
+
if (full)
|
|
5027
|
+
void probed();
|
|
5028
|
+
return narrowed(reachable, reach, port);
|
|
5029
|
+
},
|
|
4995
5030
|
};
|
|
4996
5031
|
}
|