@agentshouse/mdruntime 0.1.0-cc5745cd9af96 → 0.1.0-ccbb89b0ca151
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 +18 -15
- package/dist/adapter.d.ts +56 -32
- package/dist/adapter.d.ts.map +1 -1
- package/dist/attached-model.d.ts +5 -1
- package/dist/attached-model.d.ts.map +1 -1
- package/dist/attached-model.js +21 -4
- package/dist/binary.d.ts +0 -5
- package/dist/binary.d.ts.map +1 -1
- package/dist/binary.js +1 -1
- package/dist/bootstrap.d.ts +30 -8
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +186 -85
- package/dist/chain.d.ts +11 -0
- package/dist/chain.d.ts.map +1 -0
- package/dist/chain.js +64 -0
- package/dist/change-set.d.ts +6 -16
- package/dist/change-set.d.ts.map +1 -1
- package/dist/change-set.js +11 -94
- package/dist/change.d.ts +31 -1
- package/dist/change.d.ts.map +1 -1
- package/dist/change.js +10 -1
- package/dist/client.d.ts +9 -5
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +80 -6
- package/dist/collection.d.ts +6 -20
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +31 -354
- package/dist/compile.d.ts +16 -0
- package/dist/compile.d.ts.map +1 -0
- package/dist/compile.js +765 -0
- package/dist/connection.d.ts +16 -9
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +2399 -952
- package/dist/conversation.d.ts +3 -3
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +80 -68
- package/dist/digest.d.ts +7 -0
- package/dist/digest.d.ts.map +1 -0
- package/dist/digest.js +113 -0
- package/dist/durability.d.ts +3 -5
- package/dist/durability.d.ts.map +1 -1
- package/dist/durability.js +5 -0
- package/dist/entry.d.ts +3 -1
- package/dist/entry.d.ts.map +1 -1
- package/dist/entry.js +18 -24
- package/dist/file.d.ts +2 -19
- package/dist/file.d.ts.map +1 -1
- package/dist/file.js +7 -122
- package/dist/fulltext.d.ts +27 -0
- package/dist/fulltext.d.ts.map +1 -0
- package/dist/fulltext.js +368 -0
- package/dist/host.d.ts +4 -4
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +45 -15
- package/dist/identity.d.ts +0 -1
- package/dist/identity.d.ts.map +1 -1
- package/dist/identity.js +0 -3
- package/dist/index.d.ts +11 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/live.d.ts +22 -0
- package/dist/live.d.ts.map +1 -0
- package/dist/live.js +124 -0
- package/dist/media.d.ts +8 -0
- package/dist/media.d.ts.map +1 -0
- package/dist/media.js +195 -0
- package/dist/memory.d.ts +2 -1
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +63 -22
- package/dist/messages.d.ts +28 -0
- package/dist/messages.d.ts.map +1 -0
- package/dist/messages.js +90 -0
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +1 -1
- package/dist/name.d.ts +4 -0
- package/dist/name.d.ts.map +1 -0
- package/dist/name.js +122 -0
- package/dist/observation.d.ts +12 -0
- package/dist/observation.d.ts.map +1 -1
- package/dist/observation.js +84 -0
- package/dist/opened.d.ts +22 -0
- package/dist/opened.d.ts.map +1 -0
- package/dist/opened.js +107 -0
- package/dist/outcome.d.ts +40 -5
- package/dist/outcome.d.ts.map +1 -1
- package/dist/outcome.js +36 -2
- package/dist/overlay.d.ts +1 -4
- package/dist/overlay.d.ts.map +1 -1
- package/dist/overlay.js +22 -148
- package/dist/query.d.ts +11 -1
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +13 -1
- package/dist/reach.d.ts +11 -3
- package/dist/reach.d.ts.map +1 -1
- package/dist/reach.js +198 -69
- package/dist/readiness.d.ts +20 -9
- package/dist/readiness.d.ts.map +1 -1
- package/dist/readiness.js +6 -6
- package/dist/reads.d.ts.map +1 -1
- package/dist/reads.js +4 -2
- package/dist/recovery.d.ts +2 -2
- package/dist/recovery.d.ts.map +1 -1
- package/dist/recovery.js +8 -7
- package/dist/refresh.d.ts +15 -14
- package/dist/refresh.d.ts.map +1 -1
- package/dist/refresh.js +113 -75
- package/dist/replica.d.ts +56 -39
- package/dist/replica.d.ts.map +1 -1
- package/dist/replica.js +277 -197
- package/dist/room-draft.d.ts +4 -1
- package/dist/room-draft.d.ts.map +1 -1
- package/dist/room-draft.js +54 -13
- package/dist/rows.d.ts +38 -31
- package/dist/rows.d.ts.map +1 -1
- package/dist/rows.js +173 -101
- package/dist/save.d.ts +49 -14
- package/dist/save.d.ts.map +1 -1
- package/dist/save.js +352 -98
- package/dist/search.d.ts +11 -43
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +0 -213
- package/dist/shared.d.ts +4 -3
- package/dist/shared.d.ts.map +1 -1
- package/dist/shared.js +67 -34
- package/dist/store.d.ts +83 -95
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +317 -720
- package/dist/stored.d.ts +152 -0
- package/dist/stored.d.ts.map +1 -0
- package/dist/stored.js +607 -0
- package/dist/tables.d.ts +352 -0
- package/dist/tables.d.ts.map +1 -0
- package/dist/tables.js +624 -0
- package/dist/turn.d.ts +1 -1
- package/dist/turn.d.ts.map +1 -1
- package/dist/turn.js +31 -20
- package/dist/validation.d.ts +30 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +162 -0
- package/dist/windows.d.ts +2 -6
- package/dist/windows.d.ts.map +1 -1
- package/dist/windows.js +30 -34
- package/dist/wire.d.ts +17 -1
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +5 -1
- package/dist/words.d.ts +19 -0
- package/dist/words.d.ts.map +1 -0
- package/dist/words.js +150 -0
- package/package.json +18 -19
- package/AGENTS.md +0 -299
- package/dist/chat/acts.d.ts +0 -33
- package/dist/chat/acts.d.ts.map +0 -1
- package/dist/chat/acts.js +0 -36
- package/dist/chat/context.d.ts +0 -3
- package/dist/chat/context.d.ts.map +0 -1
- package/dist/chat/context.js +0 -10
- package/dist/chat/earlier.d.ts +0 -10
- package/dist/chat/earlier.d.ts.map +0 -1
- package/dist/chat/earlier.js +0 -54
- package/dist/chat/input.d.ts +0 -9
- package/dist/chat/input.d.ts.map +0 -1
- package/dist/chat/input.js +0 -99
- 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 -202
- package/dist/chat/messages.d.ts +0 -24
- package/dist/chat/messages.d.ts.map +0 -1
- package/dist/chat/messages.js +0 -187
- package/dist/chat/name.d.ts +0 -6
- package/dist/chat/name.d.ts.map +0 -1
- package/dist/chat/name.js +0 -37
- package/dist/chat/opened.d.ts +0 -11
- package/dist/chat/opened.d.ts.map +0 -1
- package/dist/chat/opened.js +0 -17
- package/dist/chat/overlay.d.ts +0 -32
- package/dist/chat/overlay.d.ts.map +0 -1
- package/dist/chat/overlay.js +0 -107
- package/dist/chat/parts.d.ts +0 -57
- package/dist/chat/parts.d.ts.map +0 -1
- package/dist/chat/parts.js +0 -119
- 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 -64
- package/dist/chat/provider.d.ts.map +0 -1
- package/dist/chat/provider.js +0 -638
- 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 -34
- package/dist/chat/ready.d.ts.map +0 -1
- package/dist/chat/ready.js +0 -91
- 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 -302
- package/dist/chat.d.ts +0 -14
- package/dist/chat.d.ts.map +0 -1
- package/dist/chat.js +0 -8
- package/dist/read-side.d.ts +0 -27
- package/dist/read-side.d.ts.map +0 -1
- package/dist/read-side.js +0 -826
package/dist/connection.js
CHANGED
|
@@ -1,47 +1,42 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { escapePointer, parseFrontmatter, roomTarget, target } from '@agentshouse/mdmodel';
|
|
2
|
+
import { attachedModel, builtInModel, roomTypesOf } from './attached-model.js';
|
|
2
3
|
import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
|
|
3
4
|
import { observing, whole } from './observation.js';
|
|
4
5
|
import { filterOf, identityOf, planOf, plain, queryKey, } from './query.js';
|
|
5
|
-
import {
|
|
6
|
+
import { createLiveQueries } from './live.js';
|
|
6
7
|
import { seededResult } from './seed.js';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
8
|
+
import { documentKey, roomOfKey } from './rows.js';
|
|
9
|
+
import { searchPlanOf } from './search.js';
|
|
9
10
|
import { capped, MAXIMUM_WINDOW } from './windows.js';
|
|
10
|
-
import { bootstrapRoom } from './bootstrap.js';
|
|
11
|
-
import { changeSet,
|
|
11
|
+
import { bootstrapRoom, NOTHING_DONE } from './bootstrap.js';
|
|
12
|
+
import { changeSet, mergeChangeSets } from './change-set.js';
|
|
12
13
|
import { createConversationStore, } from './conversation.js';
|
|
13
|
-
import { queuedFile } from './change.js';
|
|
14
|
-
import {
|
|
14
|
+
import { leaving as leavingIn, partnerOf, queuedFile, } from './change.js';
|
|
15
|
+
import { available, collectionOf, unreadable as unreadableIn, validRoot, } from './collection.js';
|
|
15
16
|
import { runtimeError } from './failure.js';
|
|
16
17
|
import { createLog } from './log.js';
|
|
17
18
|
import { binaryRecord } from './binary.js';
|
|
18
|
-
import {
|
|
19
|
+
import { attachedFile } from './file.js';
|
|
20
|
+
import { openedFiles } from './opened.js';
|
|
21
|
+
import { ABSENT_TARGET, absentBinary, attachAnswer, binaryAnswer, binaryLinkAnswer, calledResult, failure, historyAnswer, LOCKED_UNREADABLE, lockedRoom, OFFLINE, undeclared, UNCERTAIN, UNREADABLE, } from './outcome.js';
|
|
19
22
|
import { createRecovery } from './recovery.js';
|
|
20
23
|
import { doorReads } from './reads.js';
|
|
21
|
-
import {
|
|
22
|
-
import { buildBatch, joined,
|
|
23
|
-
import {
|
|
24
|
-
import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, moveTarget, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
|
|
24
|
+
import { advancement } from './refresh.js';
|
|
25
|
+
import { buildBatch, buildMove, joined, limiting, queuedDocuments, requeuing, } from './save.js';
|
|
26
|
+
import { compositionOf, entryAt, landed, movedBy, overlayOf, replaying, returning, returnedPaths, sameMembership, slicing, slicingWith, standing, storedRecord, touchedBy, } from './replica.js';
|
|
27
|
+
import { binaryBase, changedParts, changesOf, gatheredParts, held, heldFile, joinedDraft, moveTarget, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
|
|
25
28
|
import { same, saving, STARTING, transferring, } from './status.js';
|
|
26
|
-
import { detectStorageLoss, nextGeneration } from './durability.js';
|
|
27
|
-
import { readinessOf,
|
|
29
|
+
import { detectStorageLoss, nextGeneration, refusedForSpace, } from './durability.js';
|
|
30
|
+
import { readinessOf, } from './readiness.js';
|
|
28
31
|
import { FencedMap } from './fence.js';
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
32
|
+
import { sameBinary } from './overlay.js';
|
|
33
|
+
import { paced, ROW_WEIGHT, stepped, yielded } from './turn.js';
|
|
31
34
|
import { identityTime, roomDraftRevision } from './identity.js';
|
|
32
35
|
import { beneath, normalize } from './paths.js';
|
|
33
36
|
import { openRuntimeStore, } from './store.js';
|
|
37
|
+
import { changedCleanly, sameComposition, validatedCollection, } from './validation.js';
|
|
34
38
|
import { unconfirmed } from './transfer.js';
|
|
35
39
|
import { governs, holds, narrowed, planReach, } from './reach.js';
|
|
36
|
-
function roomsAlone(plan) {
|
|
37
|
-
return (plan.reach === null &&
|
|
38
|
-
plan.root.types === null &&
|
|
39
|
-
plan.root.where === null &&
|
|
40
|
-
plan.root.pathPrefix === null &&
|
|
41
|
-
plan.paths === null &&
|
|
42
|
-
plan.hop === null &&
|
|
43
|
-
plan.backlinks === null);
|
|
44
|
-
}
|
|
45
40
|
function within(root, path) {
|
|
46
41
|
return root === '' || beneath(root, path);
|
|
47
42
|
}
|
|
@@ -59,24 +54,28 @@ const OPENING_LANE = 'opening';
|
|
|
59
54
|
const CONVERSATION_LANE = 'conversations';
|
|
60
55
|
const ROOM_READS = 256;
|
|
61
56
|
const ROOM_ATTEMPTS = 16;
|
|
57
|
+
const ERASE_BATCH = 512;
|
|
58
|
+
const DERIVE_BYTES = 1 << 23;
|
|
59
|
+
const DERIVE_FLOOR = 1 << 18;
|
|
60
|
+
// 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.
|
|
61
|
+
const DERIVE_AIM_MS = 250;
|
|
62
|
+
const WALK_BYTES = 1 << 20;
|
|
63
|
+
const QUERY_BYTES = 1 << 20;
|
|
64
|
+
const STAGE_BYTES = 1 << 20;
|
|
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 PARTS_HELD = 4;
|
|
69
|
+
const QUEUED_BYTES = 1 << 23;
|
|
70
|
+
// Bytes still arriving are reported at most this often, well inside the 2 s progress bound, so readiness is not rebuilt for every chunk the network hands over.
|
|
71
|
+
const ARRIVAL_REPORT_MS = 250;
|
|
72
|
+
const CLEAN_CHANGES = 2_000;
|
|
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
|
+
const LOCK_GRACE_MS = 120_000;
|
|
62
75
|
function operationOf(input) {
|
|
63
76
|
const operation = input?.operation_id;
|
|
64
77
|
return typeof operation === 'string' ? operation : null;
|
|
65
78
|
}
|
|
66
|
-
const outside = new Map();
|
|
67
|
-
function outsideOf(roots) {
|
|
68
|
-
const key = JSON.stringify(roots);
|
|
69
|
-
const held = outside.get(key);
|
|
70
|
-
if (held !== undefined)
|
|
71
|
-
return held;
|
|
72
|
-
const counted = (path) => (roots.some((root) => beneath(root, path)) ? 0 : 1);
|
|
73
|
-
const made = {
|
|
74
|
-
documents: (path) => (markdownPath(path) ? counted(path) : 0),
|
|
75
|
-
binaries: counted,
|
|
76
|
-
};
|
|
77
|
-
outside.set(key, made);
|
|
78
|
-
return made;
|
|
79
|
-
}
|
|
80
79
|
function abandonedOn(signal) {
|
|
81
80
|
return new Promise((resolve) => {
|
|
82
81
|
if (signal.aborted)
|
|
@@ -88,11 +87,14 @@ function abandonedOn(signal) {
|
|
|
88
87
|
function roomLane(locator) {
|
|
89
88
|
return `room ${locator}`;
|
|
90
89
|
}
|
|
90
|
+
function pendingKey(locator, id) {
|
|
91
|
+
return `${locator}\u0000${id}`;
|
|
92
|
+
}
|
|
91
93
|
export function composeRuntime(options) {
|
|
92
94
|
const localSaves = options.adapter.localSaves === true;
|
|
95
|
+
const roomTypes = roomTypesOf(options.adapter.roomTypes, options.adapter.nameableRoomTypes);
|
|
93
96
|
let authorization = options.authorization;
|
|
94
97
|
let opening;
|
|
95
|
-
let store;
|
|
96
98
|
const fences = new Map();
|
|
97
99
|
const isFenced = (locator) => fences.has(locator);
|
|
98
100
|
let fencings = 0;
|
|
@@ -111,9 +113,9 @@ export function composeRuntime(options) {
|
|
|
111
113
|
const rooms = new FencedMap(isFenced);
|
|
112
114
|
const counted = new Map();
|
|
113
115
|
const unreadable = new Set();
|
|
114
|
-
const
|
|
115
|
-
const models = new FencedMap(
|
|
116
|
-
const liveModels = new FencedMap(
|
|
116
|
+
const builtIn = builtInModel(roomTypes);
|
|
117
|
+
const models = new FencedMap(isFenced);
|
|
118
|
+
const liveModels = new FencedMap(isFenced);
|
|
117
119
|
const invalidModels = new Set();
|
|
118
120
|
const discovered = new Map();
|
|
119
121
|
const reads = doorReads(options.adapter, (locator) => discovered.get(locator), () => generation);
|
|
@@ -121,23 +123,19 @@ export function composeRuntime(options) {
|
|
|
121
123
|
const scopedRefusals = new Map();
|
|
122
124
|
const sourceRefusals = new Map();
|
|
123
125
|
const declarationRefusals = new Set();
|
|
124
|
-
function
|
|
125
|
-
return
|
|
126
|
+
function modelOf(locator) {
|
|
127
|
+
return models.get(locator) ?? NO_MODEL;
|
|
126
128
|
}
|
|
127
|
-
function
|
|
128
|
-
return
|
|
129
|
-
}
|
|
130
|
-
function savedModel(locator, attached) {
|
|
131
|
-
return invalidModels.has(modelKey(locator, attached))
|
|
129
|
+
function savedModel(locator) {
|
|
130
|
+
return invalidModels.has(locator)
|
|
132
131
|
? { status: 'unavailable', reason: 'revoked' }
|
|
133
|
-
: modelOf(locator
|
|
132
|
+
: modelOf(locator);
|
|
133
|
+
}
|
|
134
|
+
function composed(record) {
|
|
135
|
+
return compositionOf(record, builtIn);
|
|
134
136
|
}
|
|
135
137
|
function compositionFor(record) {
|
|
136
|
-
return
|
|
137
|
-
...record,
|
|
138
|
-
model: savedModel(record.locator, 'collection'),
|
|
139
|
-
selectionModel: savedModel(record.locator, 'typed-selection'),
|
|
140
|
-
});
|
|
138
|
+
return composed({ ...record, model: savedModel(record.locator) });
|
|
141
139
|
}
|
|
142
140
|
let refusal = null;
|
|
143
141
|
let storageLoss = null;
|
|
@@ -169,6 +167,7 @@ export function composeRuntime(options) {
|
|
|
169
167
|
let roomReads = { generation, free: ROOM_READS, waiting: [] };
|
|
170
168
|
const working = new Set();
|
|
171
169
|
let mutations = Promise.resolve();
|
|
170
|
+
let gathering = null;
|
|
172
171
|
const roomListeners = new Set();
|
|
173
172
|
const refusalListeners = new Set();
|
|
174
173
|
const storageLossListeners = new Set();
|
|
@@ -176,50 +175,93 @@ export function composeRuntime(options) {
|
|
|
176
175
|
const queueSubscriptions = new Set();
|
|
177
176
|
const roomDraftListeners = new Set();
|
|
178
177
|
const presentationListeners = new Set();
|
|
179
|
-
const
|
|
178
|
+
const liveQueries = createLiveQueries((plan, cover) => answeredPlan(plan, cover));
|
|
180
179
|
let withdrawConversations;
|
|
181
180
|
let currencyQueued = null;
|
|
182
181
|
let socketOpen = false;
|
|
182
|
+
const confirmed = new Set();
|
|
183
|
+
let lockGrace = null;
|
|
184
|
+
const lockListeners = new Set();
|
|
183
185
|
let serviceMode = 'open';
|
|
184
186
|
let haltedRefusal = false;
|
|
185
187
|
let openSignals = 0;
|
|
186
188
|
const serviceModeListeners = new Set();
|
|
187
189
|
let member = null;
|
|
188
190
|
const memberListeners = new Set();
|
|
189
|
-
const prepared = new Set();
|
|
190
|
-
const projecting = new Set();
|
|
191
191
|
const unpreparedDemand = new Set();
|
|
192
|
-
const
|
|
192
|
+
const unpreparable = new Set();
|
|
193
193
|
let preparing = false;
|
|
194
194
|
let targets = {};
|
|
195
195
|
let persistence = options.shell.persistence;
|
|
196
|
-
let
|
|
197
|
-
|
|
198
|
-
let
|
|
196
|
+
let full = false;
|
|
197
|
+
let probing = null;
|
|
198
|
+
let testing = null;
|
|
199
|
+
const database = {
|
|
200
|
+
async ask(request) {
|
|
201
|
+
try {
|
|
202
|
+
return await options.database.ask(request);
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
if (refusedForSpace(error))
|
|
206
|
+
filled();
|
|
207
|
+
throw error;
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
};
|
|
199
211
|
const readinessWatches = new Set();
|
|
200
212
|
let readinessQueued = false;
|
|
201
213
|
const catching = new Set();
|
|
202
214
|
const transfers = new FencedMap(isFenced);
|
|
215
|
+
const installs = new Map();
|
|
216
|
+
const keptParts = new Map();
|
|
217
|
+
const arrivals = new Map();
|
|
218
|
+
const underived = new Map();
|
|
219
|
+
let arrivalReported = 0;
|
|
220
|
+
let onScreen = options.shell.onScreen;
|
|
221
|
+
const shownWaiters = [];
|
|
222
|
+
let partsFree = PARTS_HELD;
|
|
223
|
+
let queuedBytes = 0;
|
|
224
|
+
const partWaiters = [];
|
|
225
|
+
const stageBudget = {
|
|
226
|
+
bytes: STAGE_FLOOR * 4,
|
|
227
|
+
floor: STAGE_FLOOR,
|
|
228
|
+
ceiling: STAGE_BYTES,
|
|
229
|
+
aim: STAGE_AIM_MS,
|
|
230
|
+
};
|
|
231
|
+
const deriveBudget = {
|
|
232
|
+
bytes: DERIVE_FLOOR * 4,
|
|
233
|
+
floor: DERIVE_FLOOR,
|
|
234
|
+
ceiling: DERIVE_BYTES,
|
|
235
|
+
aim: DERIVE_AIM_MS,
|
|
236
|
+
};
|
|
237
|
+
let installWrites = Promise.resolve();
|
|
203
238
|
const conversations = createConversationStore({
|
|
204
239
|
agents: declaredAgents,
|
|
205
|
-
documents: (
|
|
240
|
+
documents: (declaration, after) => rowsQuery(planOf({
|
|
241
|
+
rooms: [declaration.locator],
|
|
242
|
+
types: [declaration.type],
|
|
243
|
+
select: {
|
|
244
|
+
fields: [
|
|
245
|
+
declaration.identity,
|
|
246
|
+
declaration.name,
|
|
247
|
+
...(declaration.retired === undefined ? [] : [declaration.retired]),
|
|
248
|
+
].map((field) => `/${escapePointer(field)}`),
|
|
249
|
+
},
|
|
250
|
+
limit: MAXIMUM_WINDOW,
|
|
251
|
+
...(after === undefined ? {} : { after }),
|
|
252
|
+
}, roomFacts())),
|
|
206
253
|
call: (name, input) => invokeAction(name, input),
|
|
207
254
|
durably,
|
|
208
255
|
stream: () => options.adapter.conversations,
|
|
209
256
|
publish: publishChangeSet,
|
|
210
257
|
online: () => connectionOf() !== 'unreachable' && refusal === null,
|
|
258
|
+
full: () => full,
|
|
211
259
|
closed: () => closed,
|
|
212
260
|
unanswered: resendLater,
|
|
213
261
|
expired: (operation) => pastHorizon(operation),
|
|
214
262
|
});
|
|
215
|
-
const searchIndex = createSearchIndex({
|
|
216
|
-
room: (room) => readSide.room(room),
|
|
217
|
-
document: (room, path) => readSide.document(room, path),
|
|
218
|
-
});
|
|
219
|
-
const roomsToIndex = new Set();
|
|
220
|
-
const conversationsToIndex = new Set();
|
|
221
|
-
let indexing = false;
|
|
222
263
|
const journal = createLog();
|
|
264
|
+
const keptFiles = openedFiles(database, isFenced);
|
|
223
265
|
let notifyReady = () => { };
|
|
224
266
|
const ready = new Promise((resolve) => {
|
|
225
267
|
notifyReady = resolve;
|
|
@@ -237,16 +279,36 @@ export function composeRuntime(options) {
|
|
|
237
279
|
if (room.size === 0)
|
|
238
280
|
roomDrafts.delete(locator);
|
|
239
281
|
}
|
|
240
|
-
function
|
|
282
|
+
async function visibleOf(locator, paths) {
|
|
283
|
+
const held = replicas.get(locator);
|
|
284
|
+
if (held === undefined)
|
|
285
|
+
return undefined;
|
|
286
|
+
const local = await openLocal();
|
|
287
|
+
const slice = await stepped(slicing(await local.entries(held.record.key, 'visible', paths), paths));
|
|
288
|
+
return { ...slice, collection: collectionOf(slice.documents, composed(held.record)) };
|
|
289
|
+
}
|
|
290
|
+
function roomDraftOf(record, visible) {
|
|
241
291
|
return structuredClone({
|
|
242
292
|
room: record.locator,
|
|
243
293
|
path: record.path,
|
|
244
294
|
revision: record.revision,
|
|
245
295
|
parts: record.parts,
|
|
246
|
-
changedUnderneath: underneath(record.parts, record.path,
|
|
296
|
+
changedUnderneath: underneath(record.parts, record.path, visible),
|
|
247
297
|
returned: record.returned,
|
|
248
298
|
});
|
|
249
299
|
}
|
|
300
|
+
async function roomDraftsOf(records) {
|
|
301
|
+
const rooms = new Map();
|
|
302
|
+
for (const record of records) {
|
|
303
|
+
const paths = rooms.get(record.locator) ?? [];
|
|
304
|
+
paths.push(record.path);
|
|
305
|
+
rooms.set(record.locator, paths);
|
|
306
|
+
}
|
|
307
|
+
const layers = new Map();
|
|
308
|
+
for (const [locator, paths] of rooms)
|
|
309
|
+
layers.set(locator, await visibleOf(locator, paths));
|
|
310
|
+
return records.map((record) => roomDraftOf(record, layers.get(record.locator)));
|
|
311
|
+
}
|
|
250
312
|
function snapshotRoomDrafts() {
|
|
251
313
|
const records = [...roomDrafts.values()].flatMap((room) => [...room.values()]);
|
|
252
314
|
records.sort((left, right) => {
|
|
@@ -256,31 +318,61 @@ export function composeRuntime(options) {
|
|
|
256
318
|
return left.path < right.path ? -1 : 1;
|
|
257
319
|
return 0;
|
|
258
320
|
});
|
|
259
|
-
return records
|
|
321
|
+
return roomDraftsOf(records);
|
|
260
322
|
}
|
|
323
|
+
let draftsShown = Promise.resolve();
|
|
261
324
|
function publishRoomDrafts(waitingOnly) {
|
|
262
325
|
if (closed)
|
|
263
326
|
return;
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
listener
|
|
270
|
-
|
|
327
|
+
const snapshot = snapshotRoomDrafts();
|
|
328
|
+
draftsShown = draftsShown.then(async () => {
|
|
329
|
+
const value = await snapshot.catch(() => null);
|
|
330
|
+
if (value === null || closed)
|
|
331
|
+
return;
|
|
332
|
+
for (const listener of roomDraftListeners) {
|
|
333
|
+
if (waitingOnly && listener.ready)
|
|
334
|
+
continue;
|
|
335
|
+
listener.ready = true;
|
|
336
|
+
listener.notify(value);
|
|
337
|
+
}
|
|
338
|
+
});
|
|
271
339
|
}
|
|
272
340
|
function presentationOf(app) {
|
|
273
341
|
const value = presentations.get(app);
|
|
274
342
|
return value === undefined ? null : structuredClone(value);
|
|
275
343
|
}
|
|
276
|
-
function
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
344
|
+
async function writtenRoomDraft(room, path, revision, input, next) {
|
|
345
|
+
const parts = readParts(input);
|
|
346
|
+
if (path === '' || normalize(path) !== path)
|
|
347
|
+
throw runtimeError('room-draft-malformed');
|
|
348
|
+
const local = await openLocal();
|
|
349
|
+
const current = roomDrafts.get(room)?.get(path);
|
|
350
|
+
if ((current?.revision ?? null) !== revision) {
|
|
351
|
+
return staleWrite(current === undefined ? null : ((await roomDraftsOf([current]))[0] ?? null));
|
|
352
|
+
}
|
|
353
|
+
const file = parts.file === undefined
|
|
354
|
+
? undefined
|
|
355
|
+
: await heldFile(parts.file.blob, binaryBase(await visibleOf(room, [path]), path));
|
|
356
|
+
const record = {
|
|
357
|
+
locator: room,
|
|
358
|
+
path,
|
|
359
|
+
revision: next,
|
|
360
|
+
parts: held(current?.parts, parts, file),
|
|
361
|
+
returned: current?.returned ?? null,
|
|
362
|
+
};
|
|
363
|
+
await local.keepRoomDraft(record, parts.file === undefined || file === undefined
|
|
364
|
+
? []
|
|
365
|
+
: [{ key: file.localId, blob: parts.file.blob }]);
|
|
366
|
+
rememberRoomDraft(record);
|
|
367
|
+
publishChangeSet(changeSet(room, { drafts: [path] }));
|
|
368
|
+
return { outcome: 'written', roomDraft: (await roomDraftsOf([record]))[0] ?? null };
|
|
369
|
+
}
|
|
370
|
+
async function keptPresentation(app, next) {
|
|
371
|
+
const local = await openLocal();
|
|
372
|
+
const stored = next(presentations.get(app));
|
|
373
|
+
await local.present({ app, value: stored });
|
|
374
|
+
presentations.set(app, stored);
|
|
375
|
+
publishChangeSet(changeSet('', { presentation: app }));
|
|
284
376
|
}
|
|
285
377
|
function emitPresentation(app) {
|
|
286
378
|
if (closed)
|
|
@@ -330,21 +422,69 @@ export function composeRuntime(options) {
|
|
|
330
422
|
return { attentionRequired, reset };
|
|
331
423
|
}
|
|
332
424
|
function documentsIn(locator) {
|
|
333
|
-
|
|
334
|
-
if (replica === undefined)
|
|
335
|
-
return 0;
|
|
336
|
-
const roots = replica.confirmed.record.typedSelection?.roots ?? [];
|
|
337
|
-
const weighed = outsideOf(roots);
|
|
338
|
-
return (tally(replica.visible.documents, weighed.documents) +
|
|
339
|
-
tally(replica.visible.binaries, weighed.binaries));
|
|
425
|
+
return replicas.get(locator)?.count ?? 0;
|
|
340
426
|
}
|
|
341
427
|
function listed() {
|
|
342
428
|
return structuredClone([...rooms.values()].map((room) => {
|
|
343
429
|
const documents = documentsIn(room.locator);
|
|
344
430
|
counted.set(room.locator, documents);
|
|
345
|
-
return
|
|
431
|
+
return locked(room.locator)
|
|
432
|
+
? {
|
|
433
|
+
...room,
|
|
434
|
+
model: modelTypes(NO_MODEL),
|
|
435
|
+
typedSelection: null,
|
|
436
|
+
documents: 0,
|
|
437
|
+
locked: true,
|
|
438
|
+
}
|
|
439
|
+
: { ...room, documents, locked: false };
|
|
346
440
|
}));
|
|
347
441
|
}
|
|
442
|
+
function locked(locator) {
|
|
443
|
+
return rooms.get(locator)?.replica.offline === 'locked' && !confirmed.has(locator);
|
|
444
|
+
}
|
|
445
|
+
function lockedRooms() {
|
|
446
|
+
return [...rooms.keys()].filter(locked).sort();
|
|
447
|
+
}
|
|
448
|
+
function relocked(before) {
|
|
449
|
+
const after = lockedRooms();
|
|
450
|
+
if (JSON.stringify(after) === JSON.stringify(before))
|
|
451
|
+
return;
|
|
452
|
+
journal.record(`locked ${after.length}`);
|
|
453
|
+
emitRooms();
|
|
454
|
+
for (const notify of [...lockListeners])
|
|
455
|
+
notify();
|
|
456
|
+
}
|
|
457
|
+
function confirmLocks(listed) {
|
|
458
|
+
if (!socketOpen)
|
|
459
|
+
return;
|
|
460
|
+
for (const room of listed) {
|
|
461
|
+
if (room.replica.admission === 'admitted')
|
|
462
|
+
confirmed.add(room.locator);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
function connectionLost() {
|
|
466
|
+
if (lockGrace !== null || closed)
|
|
467
|
+
return;
|
|
468
|
+
lockGrace = setTimeout(() => {
|
|
469
|
+
lockGrace = null;
|
|
470
|
+
if (socketOpen || closed)
|
|
471
|
+
return;
|
|
472
|
+
const before = lockedRooms();
|
|
473
|
+
confirmed.clear();
|
|
474
|
+
relocked(before);
|
|
475
|
+
}, LOCK_GRACE_MS);
|
|
476
|
+
}
|
|
477
|
+
function connectionLive() {
|
|
478
|
+
if (lockGrace !== null)
|
|
479
|
+
clearTimeout(lockGrace);
|
|
480
|
+
lockGrace = null;
|
|
481
|
+
}
|
|
482
|
+
function watchLocks(notify) {
|
|
483
|
+
lockListeners.add(notify);
|
|
484
|
+
return () => {
|
|
485
|
+
lockListeners.delete(notify);
|
|
486
|
+
};
|
|
487
|
+
}
|
|
348
488
|
function roomFacts(reach = null) {
|
|
349
489
|
const held = [...rooms.values()]
|
|
350
490
|
.filter((room) => holds(reach, room.locator))
|
|
@@ -352,7 +492,7 @@ export function composeRuntime(options) {
|
|
|
352
492
|
const governed = governs(reach, room);
|
|
353
493
|
const attached = [
|
|
354
494
|
...(governed.collection ? [modelOf(room.locator)] : []),
|
|
355
|
-
...(governed.selection ? [
|
|
495
|
+
...(governed.selection ? [builtIn] : []),
|
|
356
496
|
];
|
|
357
497
|
return {
|
|
358
498
|
locator: room.locator,
|
|
@@ -363,7 +503,7 @@ export function composeRuntime(options) {
|
|
|
363
503
|
return { listed: roomsListed, rooms: held };
|
|
364
504
|
}
|
|
365
505
|
function emitRooms() {
|
|
366
|
-
|
|
506
|
+
liveQueries.recover();
|
|
367
507
|
queueReadiness();
|
|
368
508
|
const value = listed();
|
|
369
509
|
for (const listener of roomListeners)
|
|
@@ -402,17 +542,17 @@ export function composeRuntime(options) {
|
|
|
402
542
|
for (const listener of statusListeners)
|
|
403
543
|
listener(shown);
|
|
404
544
|
}
|
|
405
|
-
function entryOf(batch) {
|
|
545
|
+
function entryOf(locator, batch) {
|
|
406
546
|
if (localSaves)
|
|
407
547
|
return Object.freeze({ batch, state: 'local' });
|
|
408
|
-
const state = pendings.get(batch.id);
|
|
548
|
+
const state = pendings.get(pendingKey(locator, batch.id));
|
|
409
549
|
if (state?.accepted !== undefined) {
|
|
410
550
|
return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
|
|
411
551
|
}
|
|
412
552
|
return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
|
|
413
553
|
}
|
|
414
554
|
function entriesOf(locator) {
|
|
415
|
-
return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map(entryOf);
|
|
555
|
+
return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map((batch) => entryOf(locator, batch));
|
|
416
556
|
}
|
|
417
557
|
function emitQueue(locator) {
|
|
418
558
|
const entries = entriesOf(locator);
|
|
@@ -422,66 +562,42 @@ export function composeRuntime(options) {
|
|
|
422
562
|
}
|
|
423
563
|
}
|
|
424
564
|
function applyChangeSet(set) {
|
|
425
|
-
|
|
426
|
-
...set,
|
|
427
|
-
documents: set.documents.filter(({ locator }) => prepared.has(locator) || projecting.has(locator)),
|
|
428
|
-
};
|
|
429
|
-
const applied = readSide.apply(held, () => searchIndex.changed(held));
|
|
430
|
-
if (applied instanceof Promise)
|
|
431
|
-
void applied.catch(unpublished);
|
|
565
|
+
liveQueries.touched(set);
|
|
432
566
|
}
|
|
433
567
|
function unpublished(error) {
|
|
434
568
|
journal.record(`publication failed ${error instanceof Error ? error.name : 'unknown'}`);
|
|
435
569
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
const record = replicas.get(locator)?.confirmed.record;
|
|
439
|
-
if (record === undefined || closed)
|
|
440
|
-
return;
|
|
441
|
-
const held = { text: record.documents, binaries: record.binaries };
|
|
442
|
-
const composition = compositionOf(record);
|
|
443
|
-
if (referencesIndexed(held, composition))
|
|
444
|
-
return;
|
|
445
|
-
await indexReferrers(held, composition);
|
|
446
|
-
}
|
|
570
|
+
function indexed(locator) {
|
|
571
|
+
return replicas.get(locator)?.indexed === true;
|
|
447
572
|
}
|
|
448
573
|
async function prepareReplica(locator) {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
return;
|
|
453
|
-
const visible = replica.visible;
|
|
454
|
-
const paths = [
|
|
455
|
-
...new Set([...Object.keys(visible.documents), ...Object.keys(visible.binaries)]),
|
|
456
|
-
];
|
|
457
|
-
const entered = [];
|
|
458
|
-
projecting.add(locator);
|
|
459
|
-
try {
|
|
460
|
-
await inChunks(paths, (path) => ROW_WEIGHT + (visible.documents[path]?.length ?? 0), (chunk) => {
|
|
461
|
-
const current = replicas.get(locator);
|
|
462
|
-
if (current === undefined || closed)
|
|
463
|
-
return;
|
|
464
|
-
const set = entering(locator, current, chunk);
|
|
465
|
-
entered.push(set);
|
|
466
|
-
return readSide.apply(set);
|
|
467
|
-
});
|
|
468
|
-
if (!replicas.has(locator) || closed)
|
|
574
|
+
for (;;) {
|
|
575
|
+
const held = replicas.get(locator);
|
|
576
|
+
if (held === undefined || closed || held.indexed)
|
|
469
577
|
return;
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
578
|
+
const key = held.record.key;
|
|
579
|
+
const done = await exclusive(async () => {
|
|
580
|
+
const local = await openLocal();
|
|
581
|
+
return timed(deriveBudget, () => local.rederive(key, deriveBudget.bytes));
|
|
582
|
+
});
|
|
583
|
+
const now = replicas.get(locator);
|
|
584
|
+
if (now?.record.key !== key)
|
|
585
|
+
continue;
|
|
586
|
+
if (!done)
|
|
587
|
+
continue;
|
|
588
|
+
replicas.set(locator, { ...now, indexed: true });
|
|
589
|
+
publishChangeSet(changeSet(locator, { documents: null }));
|
|
590
|
+
liveQueries.recover();
|
|
591
|
+
queueReadiness();
|
|
592
|
+
return;
|
|
474
593
|
}
|
|
475
|
-
conversations.changed(mergeChangeSets(entered));
|
|
476
|
-
readSide.recover();
|
|
477
|
-
queueReadiness();
|
|
478
594
|
}
|
|
479
595
|
function unprepared() {
|
|
480
|
-
return [...unpreparedDemand, ...replicas.keys()].find((locator) => !
|
|
596
|
+
return [...unpreparedDemand, ...replicas.keys()].find((locator) => !indexed(locator) && !unpreparable.has(locator));
|
|
481
597
|
}
|
|
482
|
-
function
|
|
483
|
-
|
|
484
|
-
journal.record(`
|
|
598
|
+
function unpreparedBy(locator, error) {
|
|
599
|
+
unpreparable.add(locator);
|
|
600
|
+
journal.record(`preparation failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
|
|
485
601
|
queueReadiness();
|
|
486
602
|
}
|
|
487
603
|
function prepare(locators = []) {
|
|
@@ -497,7 +613,7 @@ export function composeRuntime(options) {
|
|
|
497
613
|
if (closed)
|
|
498
614
|
return;
|
|
499
615
|
unpreparedDemand.delete(waiting);
|
|
500
|
-
await prepareReplica(waiting).catch((error) =>
|
|
616
|
+
await prepareReplica(waiting).catch((error) => unpreparedBy(waiting, error));
|
|
501
617
|
}
|
|
502
618
|
}
|
|
503
619
|
finally {
|
|
@@ -506,7 +622,11 @@ export function composeRuntime(options) {
|
|
|
506
622
|
})();
|
|
507
623
|
}
|
|
508
624
|
function roomsOfReach(reach) {
|
|
509
|
-
return new Set([
|
|
625
|
+
return new Set([
|
|
626
|
+
...reach.rooms,
|
|
627
|
+
...reach.documents.map(roomOfKey),
|
|
628
|
+
...reach.areas.flatMap((area) => (area.types.length === 0 ? [] : [area.room])),
|
|
629
|
+
]);
|
|
510
630
|
}
|
|
511
631
|
function reachedRooms(plan) {
|
|
512
632
|
const reached = plan.reach === null ? null : roomsOfReach(plan.reach);
|
|
@@ -514,7 +634,9 @@ export function composeRuntime(options) {
|
|
|
514
634
|
(reached === null
|
|
515
635
|
? [...new Set([...rooms.keys(), ...replicas.keys(), ...queuesWithoutReplica.keys()])]
|
|
516
636
|
: [...reached]);
|
|
517
|
-
return [...new Set(named)]
|
|
637
|
+
return [...new Set(named)]
|
|
638
|
+
.filter((locator) => (reached === null || reached.has(locator)) && !plan.locked.includes(locator))
|
|
639
|
+
.sort();
|
|
518
640
|
}
|
|
519
641
|
function awaited(locator) {
|
|
520
642
|
return (!roomsListed ||
|
|
@@ -528,6 +650,7 @@ export function composeRuntime(options) {
|
|
|
528
650
|
const pending = [];
|
|
529
651
|
const coverage = [];
|
|
530
652
|
const overlay = [];
|
|
653
|
+
const read = [];
|
|
531
654
|
for (const locator of locators) {
|
|
532
655
|
const replica = replicas.get(locator);
|
|
533
656
|
if (replica === undefined) {
|
|
@@ -535,21 +658,22 @@ export function composeRuntime(options) {
|
|
|
535
658
|
pending.push({ room: locator, awaits: 'acquisition' });
|
|
536
659
|
continue;
|
|
537
660
|
}
|
|
538
|
-
if (!
|
|
661
|
+
if (!replica.indexed) {
|
|
539
662
|
pending.push({ room: locator, awaits: 'preparation' });
|
|
540
663
|
continue;
|
|
541
664
|
}
|
|
542
665
|
coverage.push({
|
|
543
666
|
room: locator,
|
|
544
|
-
generation: replica.
|
|
667
|
+
generation: replica.record.transfer,
|
|
545
668
|
current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
|
|
546
669
|
});
|
|
670
|
+
read.push({ locator, key: replica.record.key });
|
|
547
671
|
for (const batch of replica.queue)
|
|
548
672
|
overlay.push(batch.id);
|
|
549
673
|
}
|
|
550
674
|
if (pending.length > 0)
|
|
551
675
|
return { pending };
|
|
552
|
-
return { coverage: { rooms: coverage, indexes: [], overlay } };
|
|
676
|
+
return { coverage: { rooms: coverage, indexes: [], overlay }, rooms: read };
|
|
553
677
|
}
|
|
554
678
|
function awaitPreparation(cover) {
|
|
555
679
|
if (!('pending' in cover))
|
|
@@ -567,7 +691,7 @@ export function composeRuntime(options) {
|
|
|
567
691
|
return { listed: true, rooms: named.filter((room) => rooms.has(room)) };
|
|
568
692
|
return { listed: named.every((room) => replicas.has(room)), rooms: named };
|
|
569
693
|
}
|
|
570
|
-
function roomState(locator
|
|
694
|
+
function roomState(locator) {
|
|
571
695
|
if (sourceRefusals.has(locator))
|
|
572
696
|
return 'refused';
|
|
573
697
|
const listedRoom = discovered.get(locator);
|
|
@@ -578,27 +702,17 @@ export function composeRuntime(options) {
|
|
|
578
702
|
return 'unadmitted';
|
|
579
703
|
if (!replicas.has(locator))
|
|
580
704
|
return 'acquiring';
|
|
581
|
-
if (!
|
|
705
|
+
if (!indexed(locator))
|
|
582
706
|
return 'preparing';
|
|
583
|
-
if (search && !searchIndex.rooms().has(locator))
|
|
584
|
-
return 'indexing';
|
|
585
707
|
return 'complete';
|
|
586
708
|
}
|
|
587
|
-
function conversationCoverage(reach
|
|
709
|
+
function conversationCoverage(reach) {
|
|
588
710
|
const declared = reach === null || reach.conversations;
|
|
589
711
|
if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
|
|
590
712
|
return 'undeclared';
|
|
591
713
|
if (!conversations.known() || !conversations.current())
|
|
592
714
|
return 'acquiring';
|
|
593
|
-
|
|
594
|
-
return 'complete';
|
|
595
|
-
const unindexed = conversations
|
|
596
|
-
.searchable()
|
|
597
|
-
.filter((one) => conversations.acquired(one) && !searchIndex.conversations().has(one));
|
|
598
|
-
if (unindexed.length === 0)
|
|
599
|
-
return 'complete';
|
|
600
|
-
index([], unindexed);
|
|
601
|
-
return 'indexing';
|
|
715
|
+
return 'complete';
|
|
602
716
|
}
|
|
603
717
|
function connectionOf() {
|
|
604
718
|
if (socketOpen)
|
|
@@ -618,6 +732,7 @@ export function composeRuntime(options) {
|
|
|
618
732
|
return [
|
|
619
733
|
{
|
|
620
734
|
room,
|
|
735
|
+
name: discovered.get(room)?.name ?? room,
|
|
621
736
|
file: typeof file === 'string' ? file : '',
|
|
622
737
|
rule: held.code,
|
|
623
738
|
location: typeof location === 'string' ? location : null,
|
|
@@ -625,34 +740,85 @@ export function composeRuntime(options) {
|
|
|
625
740
|
];
|
|
626
741
|
});
|
|
627
742
|
}
|
|
743
|
+
function installationOf(coverage) {
|
|
744
|
+
const rooms = { done: 0, total: 0 };
|
|
745
|
+
const parts = { done: 0, total: 0 };
|
|
746
|
+
const bytes = { done: 0, total: 0 };
|
|
747
|
+
const received = { done: 0, total: 0 };
|
|
748
|
+
let installing = 0;
|
|
749
|
+
let unlisted = false;
|
|
750
|
+
let listed = false;
|
|
751
|
+
let waiting = false;
|
|
752
|
+
for (const { room, state } of coverage) {
|
|
753
|
+
if (state === 'unadmitted')
|
|
754
|
+
continue;
|
|
755
|
+
rooms.total += 1;
|
|
756
|
+
if (state === 'complete')
|
|
757
|
+
rooms.done += 1;
|
|
758
|
+
const transfer = transfers.get(room);
|
|
759
|
+
const replica = replicas.get(room)?.record.inventory ?? null;
|
|
760
|
+
const inventory = transfer?.inventory ?? replica ?? installs.get(room) ?? null;
|
|
761
|
+
const underway = state === 'acquiring' || (installs.has(room) && state !== 'complete');
|
|
762
|
+
if (underway)
|
|
763
|
+
installing += 1;
|
|
764
|
+
if (underway && inventory === null)
|
|
765
|
+
unlisted = true;
|
|
766
|
+
if (inventory !== null) {
|
|
767
|
+
listed = true;
|
|
768
|
+
const done = transfer?.done ?? (replica === null ? NOTHING_DONE : inventory);
|
|
769
|
+
parts.total += inventory.parts;
|
|
770
|
+
bytes.total += inventory.bytes;
|
|
771
|
+
parts.done += done.parts;
|
|
772
|
+
bytes.done += done.bytes;
|
|
773
|
+
received.total += inventory.bytes;
|
|
774
|
+
received.done += done.bytes;
|
|
775
|
+
for (const held of [keptParts.get(room), arrivals.get(room)]) {
|
|
776
|
+
for (const arrived of held?.values() ?? [])
|
|
777
|
+
received.done += arrived;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
waiting ||= underway;
|
|
781
|
+
}
|
|
782
|
+
const step = installing === 0
|
|
783
|
+
? null
|
|
784
|
+
: !listed
|
|
785
|
+
? 'listing'
|
|
786
|
+
: unlisted || received.done < received.total
|
|
787
|
+
? 'downloading'
|
|
788
|
+
: bytes.done < bytes.total
|
|
789
|
+
? 'writing'
|
|
790
|
+
: 'replaying';
|
|
791
|
+
return {
|
|
792
|
+
step,
|
|
793
|
+
paused: waiting && !onScreen ? 'hidden' : null,
|
|
794
|
+
rooms,
|
|
795
|
+
parts,
|
|
796
|
+
bytes,
|
|
797
|
+
received,
|
|
798
|
+
};
|
|
799
|
+
}
|
|
628
800
|
function readinessFor(port, reach) {
|
|
629
801
|
const required = requiredRooms(reach);
|
|
630
802
|
const search = port.search === 'required';
|
|
631
|
-
const coverage = required.rooms.map((room) => ({ room, state: roomState(room
|
|
632
|
-
const
|
|
633
|
-
.filter((room) => room.state ===
|
|
803
|
+
const coverage = required.rooms.map((room) => ({ room, state: roomState(room) }));
|
|
804
|
+
const preparing = coverage
|
|
805
|
+
.filter((room) => room.state === 'preparing' && !unpreparable.has(room.room))
|
|
634
806
|
.map(({ room }) => room);
|
|
635
|
-
const preparing = building('preparing');
|
|
636
807
|
if (preparing.length > 0)
|
|
637
808
|
prepare(preparing);
|
|
638
|
-
const indexing = building('indexing');
|
|
639
|
-
if (indexing.length > 0)
|
|
640
|
-
index(indexing, []);
|
|
641
809
|
return readinessOf({
|
|
642
810
|
code: port.code,
|
|
643
811
|
coverage: {
|
|
644
812
|
listed: required.listed,
|
|
645
813
|
rooms: coverage,
|
|
646
|
-
conversations: conversationCoverage(reach
|
|
814
|
+
conversations: conversationCoverage(reach),
|
|
647
815
|
search: search ? 'required' : 'unused',
|
|
648
816
|
},
|
|
649
817
|
persistence,
|
|
650
818
|
connection: connectionOf(),
|
|
651
819
|
house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
? capacity
|
|
655
|
-
: port.capacity,
|
|
820
|
+
space: full ? 'full' : port.space,
|
|
821
|
+
installation: installationOf(coverage),
|
|
656
822
|
refused: refusedIn(required.rooms),
|
|
657
823
|
lost: storageLoss !== null,
|
|
658
824
|
established: targets[port.target]?.established === true,
|
|
@@ -661,7 +827,8 @@ export function composeRuntime(options) {
|
|
|
661
827
|
function establish(name) {
|
|
662
828
|
targets = { ...targets, [name]: { established: true } };
|
|
663
829
|
queueReadiness();
|
|
664
|
-
|
|
830
|
+
probeEachMinute();
|
|
831
|
+
void durably((local) => local.fact('targets', targets)).catch(() => {
|
|
665
832
|
journal.record(`target record failed ${name}`);
|
|
666
833
|
});
|
|
667
834
|
}
|
|
@@ -698,92 +865,51 @@ export function composeRuntime(options) {
|
|
|
698
865
|
readinessWatches.delete(watch);
|
|
699
866
|
};
|
|
700
867
|
}
|
|
701
|
-
function
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
}
|
|
707
|
-
function grownBy(next, previous) {
|
|
708
|
-
if (previous === undefined)
|
|
709
|
-
return layerBytes(next);
|
|
710
|
-
const documents = Object.fromEntries(Object.entries(next.documents).filter(([path, text]) => previous.documents[path] !== text));
|
|
711
|
-
return layerBytes({ ...next, documents });
|
|
712
|
-
}
|
|
713
|
-
function exceeded(error) {
|
|
714
|
-
return (typeof error === 'object' &&
|
|
715
|
-
error !== null &&
|
|
716
|
-
error.name === 'QuotaExceededError');
|
|
717
|
-
}
|
|
718
|
-
async function estimated() {
|
|
719
|
-
const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
|
|
720
|
-
return estimate === null ? null : Math.max(0, estimate.quota - estimate.usage);
|
|
721
|
-
}
|
|
722
|
-
function pause(locator, cause) {
|
|
723
|
-
paused.add(locator);
|
|
724
|
-
capacity = cause;
|
|
725
|
-
journal.record(`capacity ${cause.cause} ${locator} ${cause.needed} ${cause.available ?? '-'}`);
|
|
868
|
+
function filled() {
|
|
869
|
+
if (full || closed)
|
|
870
|
+
return;
|
|
871
|
+
full = true;
|
|
872
|
+
journal.record('space full');
|
|
726
873
|
queueReadiness();
|
|
727
|
-
|
|
874
|
+
probeEachMinute();
|
|
875
|
+
}
|
|
876
|
+
function installedTarget() {
|
|
877
|
+
return Object.values(targets).some((target) => target.established);
|
|
878
|
+
}
|
|
879
|
+
function probeEachMinute() {
|
|
880
|
+
if (!full || probing !== null || closed || !installedTarget())
|
|
728
881
|
return;
|
|
729
|
-
// A
|
|
730
|
-
|
|
731
|
-
void
|
|
882
|
+
// 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.
|
|
883
|
+
probing = setInterval(() => {
|
|
884
|
+
void probed();
|
|
732
885
|
}, 60_000);
|
|
733
886
|
}
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
887
|
+
function probed() {
|
|
888
|
+
testing ??= database.ask({ kind: 'space.probe' }).then(() => {
|
|
889
|
+
testing = null;
|
|
890
|
+
spaceFreed();
|
|
891
|
+
return true;
|
|
892
|
+
}, () => {
|
|
893
|
+
testing = null;
|
|
894
|
+
return false;
|
|
895
|
+
});
|
|
896
|
+
return testing;
|
|
744
897
|
}
|
|
745
|
-
function
|
|
746
|
-
if (
|
|
747
|
-
clearInterval(rechecking);
|
|
748
|
-
rechecking = null;
|
|
749
|
-
if (paused.size === 0 && capacity === null)
|
|
898
|
+
function spaceFreed() {
|
|
899
|
+
if (!full)
|
|
750
900
|
return;
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
901
|
+
full = false;
|
|
902
|
+
if (probing !== null)
|
|
903
|
+
clearInterval(probing);
|
|
904
|
+
probing = null;
|
|
905
|
+
unpreparable.clear();
|
|
906
|
+
journal.record('space freed');
|
|
754
907
|
queueReadiness();
|
|
755
908
|
if (!closed && authorization !== undefined)
|
|
756
909
|
void sync('unstreamed');
|
|
757
910
|
}
|
|
758
|
-
async function reclaimed(local) {
|
|
759
|
-
const unreferenced = [...transfers.keys()].filter((locator) => !discovered.has(locator));
|
|
760
|
-
for (const locator of unreferenced) {
|
|
761
|
-
await local.abandon(locator);
|
|
762
|
-
transfers.delete(locator);
|
|
763
|
-
}
|
|
764
|
-
if (unreferenced.length > 0)
|
|
765
|
-
journal.record(`capacity reclaimed ${unreferenced.length}`);
|
|
766
|
-
return unreferenced.length > 0;
|
|
767
|
-
}
|
|
768
|
-
async function admitted(local, locator, needed) {
|
|
769
|
-
if (paused.has(locator))
|
|
770
|
-
return false;
|
|
771
|
-
let available = await estimated().catch(() => null);
|
|
772
|
-
if (available === null || available >= needed)
|
|
773
|
-
return true;
|
|
774
|
-
if (await reclaimed(local))
|
|
775
|
-
available = await estimated().catch(() => null);
|
|
776
|
-
if (available === null || available >= needed)
|
|
777
|
-
return true;
|
|
778
|
-
pause(locator, { cause: 'insufficient-space', needed, available });
|
|
779
|
-
return false;
|
|
780
|
-
}
|
|
781
|
-
async function exceededOn(locator, bytes) {
|
|
782
|
-
const available = await estimated().catch(() => null);
|
|
783
|
-
pause(locator, { cause: 'quota-exceeded', needed: bytes, available });
|
|
784
|
-
}
|
|
785
911
|
function seedRoomHeld(room, version) {
|
|
786
|
-
return
|
|
912
|
+
return indexed(room) && reachedBy(room, version);
|
|
787
913
|
}
|
|
788
914
|
function seedHeld(at) {
|
|
789
915
|
return Object.entries(at).every(([room, version]) => (roomsListed && !rooms.has(room)) || seedRoomHeld(room, version));
|
|
@@ -817,6 +943,75 @@ export function composeRuntime(options) {
|
|
|
817
943
|
shown();
|
|
818
944
|
}
|
|
819
945
|
}
|
|
946
|
+
function sameCover(left, right) {
|
|
947
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
948
|
+
}
|
|
949
|
+
async function coveredRows(plan, cover) {
|
|
950
|
+
for (;;) {
|
|
951
|
+
const before = cover();
|
|
952
|
+
if ('pending' in before)
|
|
953
|
+
return { pending: before };
|
|
954
|
+
const local = await openLocal();
|
|
955
|
+
const keys = before.rooms.map(({ key }) => key);
|
|
956
|
+
const changes = local.changes(keys);
|
|
957
|
+
const current = () => local.changes(keys) === changes;
|
|
958
|
+
const rows = await windowRows(local, plan, before.rooms, current);
|
|
959
|
+
const after = cover();
|
|
960
|
+
if (rows !== null && sameCover(before, after))
|
|
961
|
+
return { rows, cover: before, current };
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
async function windowRows(local, plan, rooms, unchanged) {
|
|
965
|
+
const first = await local.query(plan, rooms, QUERY_BYTES);
|
|
966
|
+
if (first.more === undefined || !('documents' in first)) {
|
|
967
|
+
const { more: _more, ...rows } = first;
|
|
968
|
+
return rows;
|
|
969
|
+
}
|
|
970
|
+
const documents = [...first.documents];
|
|
971
|
+
for (let more = first.more; more !== undefined;) {
|
|
972
|
+
if (!unchanged())
|
|
973
|
+
return null;
|
|
974
|
+
const page = await local.query({
|
|
975
|
+
...plan,
|
|
976
|
+
after: { text: more, values: JSON.parse(more) },
|
|
977
|
+
limit: plan.limit === null ? null : plan.limit - documents.length,
|
|
978
|
+
total: false,
|
|
979
|
+
}, rooms, QUERY_BYTES);
|
|
980
|
+
if (!('documents' in page))
|
|
981
|
+
return null;
|
|
982
|
+
documents.push(...page.documents);
|
|
983
|
+
more = page.more;
|
|
984
|
+
if (more === undefined) {
|
|
985
|
+
return {
|
|
986
|
+
documents,
|
|
987
|
+
...(page.next === undefined ? {} : { next: page.next }),
|
|
988
|
+
...(first.total === undefined ? {} : { total: first.total }),
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
return null;
|
|
993
|
+
}
|
|
994
|
+
function keyedOf(rows, coverage) {
|
|
995
|
+
if ('groups' in rows) {
|
|
996
|
+
return {
|
|
997
|
+
result: { ...rows, coverage },
|
|
998
|
+
keys: rows.groups.map((group) => JSON.stringify(group.key)),
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
return {
|
|
1002
|
+
result: { ...rows, coverage },
|
|
1003
|
+
keys: rows.documents.map((document) => documentKey(document.room, document.path)),
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
async function answeredPlan(plan, cover) {
|
|
1007
|
+
const answered = await coveredRows(plan, cover);
|
|
1008
|
+
if ('pending' in answered) {
|
|
1009
|
+
const pending = answered.pending;
|
|
1010
|
+
return { result: pending, keys: [], current: () => true };
|
|
1011
|
+
}
|
|
1012
|
+
const held = answered.cover;
|
|
1013
|
+
return { ...keyedOf(answered.rows, held.coverage), current: answered.current };
|
|
1014
|
+
}
|
|
820
1015
|
async function rowsQuery(plan) {
|
|
821
1016
|
const before = covered(plan);
|
|
822
1017
|
awaitPreparation(before);
|
|
@@ -825,59 +1020,15 @@ export function composeRuntime(options) {
|
|
|
825
1020
|
return seededResult(seeded).result;
|
|
826
1021
|
if ('pending' in before)
|
|
827
1022
|
return before;
|
|
828
|
-
const
|
|
829
|
-
|
|
830
|
-
|
|
1023
|
+
const answered = await coveredRows(plan, () => {
|
|
1024
|
+
const cover = covered(plan);
|
|
1025
|
+
awaitPreparation(cover);
|
|
831
1026
|
return cover;
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
for (;;) {
|
|
838
|
-
const batch = await durably((local) => local.messagesAfter(conversation, after));
|
|
839
|
-
if (closed)
|
|
840
|
-
return;
|
|
841
|
-
searchIndex.indexMessages(conversation, batch.map(({ identity, text }) => ({ conversation, identity, text })), batch.length === 0);
|
|
842
|
-
if (batch.length === 0)
|
|
843
|
-
return;
|
|
844
|
-
after = batch[batch.length - 1].identity;
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
function indexNext() {
|
|
848
|
-
const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator) && !unindexable.has(locator));
|
|
849
|
-
if (room !== undefined) {
|
|
850
|
-
roomsToIndex.delete(room);
|
|
851
|
-
return () => prepared.has(room)
|
|
852
|
-
? searchIndex.indexRoom(room).catch((error) => unbuilt(room, error))
|
|
853
|
-
: undefined;
|
|
854
|
-
}
|
|
855
|
-
const conversation = [...conversationsToIndex].find((one) => !searchIndex.conversations().has(one));
|
|
856
|
-
if (conversation === undefined)
|
|
857
|
-
return null;
|
|
858
|
-
conversationsToIndex.delete(conversation);
|
|
859
|
-
return () => indexConversation(conversation);
|
|
860
|
-
}
|
|
861
|
-
function index(rooms, named) {
|
|
862
|
-
for (const room of rooms)
|
|
863
|
-
roomsToIndex.add(room);
|
|
864
|
-
for (const conversation of named)
|
|
865
|
-
conversationsToIndex.add(conversation);
|
|
866
|
-
if (indexing)
|
|
867
|
-
return;
|
|
868
|
-
indexing = true;
|
|
869
|
-
void (async () => {
|
|
870
|
-
try {
|
|
871
|
-
for (let unit = indexNext(); unit !== null && !closed; unit = indexNext()) {
|
|
872
|
-
await yielded();
|
|
873
|
-
await unit();
|
|
874
|
-
queueReadiness();
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
finally {
|
|
878
|
-
indexing = false;
|
|
879
|
-
}
|
|
880
|
-
})().catch(() => { });
|
|
1027
|
+
});
|
|
1028
|
+
if ('pending' in answered)
|
|
1029
|
+
return answered.pending;
|
|
1030
|
+
const held = answered.cover;
|
|
1031
|
+
return { ...answered.rows, coverage: held.coverage };
|
|
881
1032
|
}
|
|
882
1033
|
function publishChangeSet(set) {
|
|
883
1034
|
applyChangeSet(set);
|
|
@@ -887,7 +1038,10 @@ export function composeRuntime(options) {
|
|
|
887
1038
|
for (const locator of set.queues)
|
|
888
1039
|
emitQueue(locator);
|
|
889
1040
|
if (set.drafts.length > 0 ||
|
|
890
|
-
set.documents.some((change) =>
|
|
1041
|
+
set.documents.some((change) => {
|
|
1042
|
+
const held = roomDrafts.get(change.locator);
|
|
1043
|
+
return held !== undefined && (change.path === null || held.has(change.path));
|
|
1044
|
+
})) {
|
|
891
1045
|
publishRoomDrafts(false);
|
|
892
1046
|
}
|
|
893
1047
|
for (const app of set.presentations)
|
|
@@ -900,34 +1054,43 @@ export function composeRuntime(options) {
|
|
|
900
1054
|
if (reason === 'revoked-authorization') {
|
|
901
1055
|
await exclusive(async () => {
|
|
902
1056
|
const revoked = { status: 'unavailable', reason: 'revoked' };
|
|
1057
|
+
const records = [...replicas].map(([locator, held]) => ({
|
|
1058
|
+
locator,
|
|
1059
|
+
held,
|
|
1060
|
+
record: { ...held.record, model: revoked },
|
|
1061
|
+
}));
|
|
1062
|
+
let answer = null;
|
|
903
1063
|
try {
|
|
904
1064
|
const local = await openLocal();
|
|
905
|
-
await local.
|
|
1065
|
+
answer = await local.commit({
|
|
1066
|
+
rooms: records.map(({ locator, record }) => ({
|
|
1067
|
+
locator,
|
|
1068
|
+
key: record.key,
|
|
1069
|
+
record: storedRecord(record),
|
|
1070
|
+
composition: composed(record),
|
|
1071
|
+
})),
|
|
1072
|
+
});
|
|
906
1073
|
}
|
|
907
1074
|
catch {
|
|
908
1075
|
journal.record('model revocation persistence failed');
|
|
909
1076
|
}
|
|
910
|
-
const keys = new Set(models.keys());
|
|
911
|
-
for (const locator of rooms.keys()) {
|
|
912
|
-
keys.add(modelKey(locator, 'collection'));
|
|
913
|
-
keys.add(modelKey(locator, 'typed-selection'));
|
|
914
|
-
}
|
|
1077
|
+
const keys = new Set([...models.keys(), ...rooms.keys()]);
|
|
915
1078
|
for (const key of keys) {
|
|
916
1079
|
invalidModels.add(key);
|
|
917
1080
|
models.set(key, revoked);
|
|
918
1081
|
liveModels.delete(key);
|
|
919
1082
|
}
|
|
920
1083
|
const changed = [];
|
|
921
|
-
for (const
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
1084
|
+
for (const { locator, held, record } of records) {
|
|
1085
|
+
if (replicas.get(locator) !== held)
|
|
1086
|
+
continue;
|
|
1087
|
+
const indexed = answer === null ? held.indexed : committedRoom(locator, answer).indexed;
|
|
1088
|
+
replicas.set(locator, { ...held, record, indexed });
|
|
1089
|
+
}
|
|
1090
|
+
for (const locator of [...rooms.keys()]) {
|
|
1091
|
+
const room = rooms.get(locator);
|
|
1092
|
+
if (room === undefined)
|
|
1093
|
+
continue;
|
|
931
1094
|
rooms.set(locator, {
|
|
932
1095
|
...room,
|
|
933
1096
|
model: modelTypes(revoked),
|
|
@@ -935,9 +1098,10 @@ export function composeRuntime(options) {
|
|
|
935
1098
|
? null
|
|
936
1099
|
: { ...room.typedSelection, model: modelTypes(revoked) },
|
|
937
1100
|
});
|
|
938
|
-
changed.push(changeSet(locator,
|
|
1101
|
+
changed.push(changeSet(locator, { documents: null, room: true }));
|
|
939
1102
|
}
|
|
940
1103
|
publishChangeSet(mergeChangeSets(changed));
|
|
1104
|
+
prepare();
|
|
941
1105
|
});
|
|
942
1106
|
}
|
|
943
1107
|
}
|
|
@@ -965,15 +1129,16 @@ export function composeRuntime(options) {
|
|
|
965
1129
|
? null
|
|
966
1130
|
: {
|
|
967
1131
|
roots: selection.roots,
|
|
968
|
-
model: modelTypes(
|
|
1132
|
+
model: modelTypes(builtIn),
|
|
969
1133
|
},
|
|
970
1134
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
971
|
-
roomVersion: replicas.get(room.locator)?.
|
|
1135
|
+
roomVersion: replicas.get(room.locator)?.record.roomVersion ?? null,
|
|
972
1136
|
synchronizationAvailable,
|
|
973
1137
|
refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
|
|
974
1138
|
});
|
|
975
1139
|
}
|
|
976
1140
|
async function applyDiscovery(found, started, covered) {
|
|
1141
|
+
const lockedBefore = lockedRooms();
|
|
977
1142
|
for (const room of found) {
|
|
978
1143
|
const fence = fences.get(room.locator);
|
|
979
1144
|
if (fence !== undefined && fence.erased && fence.at < started)
|
|
@@ -999,15 +1164,17 @@ export function composeRuntime(options) {
|
|
|
999
1164
|
else
|
|
1000
1165
|
unlistedKept(unlisted);
|
|
1001
1166
|
for (const room of listed) {
|
|
1002
|
-
const held = replicas.get(room.locator)?.
|
|
1167
|
+
const held = replicas.get(room.locator)?.record;
|
|
1003
1168
|
if (held !== undefined && held.replica.selection !== room.replica.selection) {
|
|
1004
1169
|
list(held, false);
|
|
1005
1170
|
continue;
|
|
1006
1171
|
}
|
|
1007
1172
|
list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
1008
1173
|
}
|
|
1174
|
+
confirmLocks(listed);
|
|
1009
1175
|
roomsListed = true;
|
|
1010
1176
|
emitRooms();
|
|
1177
|
+
relocked(lockedBefore);
|
|
1011
1178
|
return listed;
|
|
1012
1179
|
}
|
|
1013
1180
|
function heldLocally(locator) {
|
|
@@ -1028,7 +1195,7 @@ export function composeRuntime(options) {
|
|
|
1028
1195
|
}
|
|
1029
1196
|
function unlistedKept(unlisted) {
|
|
1030
1197
|
for (const locator of unlisted) {
|
|
1031
|
-
const held = replicas.get(locator)?.
|
|
1198
|
+
const held = replicas.get(locator)?.record;
|
|
1032
1199
|
if (held !== undefined)
|
|
1033
1200
|
list(held, false);
|
|
1034
1201
|
else
|
|
@@ -1037,64 +1204,89 @@ export function composeRuntime(options) {
|
|
|
1037
1204
|
}
|
|
1038
1205
|
function fence(locators) {
|
|
1039
1206
|
const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
|
|
1040
|
-
const removed = fresh.map((locator) =>
|
|
1041
|
-
|
|
1042
|
-
queue: true,
|
|
1207
|
+
const removed = fresh.map((locator) => ({
|
|
1208
|
+
locator,
|
|
1043
1209
|
drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
|
|
1044
1210
|
}));
|
|
1045
1211
|
for (const locator of fresh) {
|
|
1046
1212
|
fences.set(locator, { at: discoveries, erased: false });
|
|
1047
1213
|
lastFenced.set(locator, ++fencings);
|
|
1214
|
+
confirmed.delete(locator);
|
|
1048
1215
|
for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
|
|
1049
|
-
pendings.delete(batch.id);
|
|
1216
|
+
pendings.delete(pendingKey(locator, batch.id));
|
|
1050
1217
|
replicas.delete(locator);
|
|
1051
1218
|
queuesWithoutReplica.delete(locator);
|
|
1052
1219
|
rooms.delete(locator);
|
|
1053
1220
|
roomDrafts.delete(locator);
|
|
1054
|
-
|
|
1221
|
+
untransferred(locator);
|
|
1055
1222
|
catching.delete(locator);
|
|
1056
1223
|
discovered.delete(locator);
|
|
1057
1224
|
announced.delete(locator);
|
|
1058
1225
|
unreadable.delete(locator);
|
|
1059
1226
|
stale.delete(locator);
|
|
1060
1227
|
repairable.delete(locator);
|
|
1061
|
-
|
|
1062
|
-
paused.delete(locator);
|
|
1228
|
+
unpreparable.delete(locator);
|
|
1063
1229
|
scopedRefusals.delete(locator);
|
|
1064
1230
|
sourceRefusals.delete(locator);
|
|
1065
1231
|
declarationRefusals.delete(locator);
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
liveModels.delete(key);
|
|
1070
|
-
invalidModels.delete(key);
|
|
1071
|
-
}
|
|
1232
|
+
models.delete(locator);
|
|
1233
|
+
liveModels.delete(locator);
|
|
1234
|
+
invalidModels.delete(locator);
|
|
1072
1235
|
journal.record(`room lost ${locator}`);
|
|
1073
1236
|
}
|
|
1074
1237
|
if (fresh.length === 0)
|
|
1075
1238
|
return fresh;
|
|
1076
|
-
publishChangeSet(mergeChangeSets(removed));
|
|
1077
|
-
|
|
1078
|
-
prepared.delete(locator);
|
|
1079
|
-
searchIndex.forget(fresh);
|
|
1080
|
-
readSide.recover();
|
|
1239
|
+
publishChangeSet(mergeChangeSets(removed.map(({ locator, drafts }) => changeSet(locator, { documents: null, room: true, queue: true, drafts }))));
|
|
1240
|
+
liveQueries.recover();
|
|
1081
1241
|
moveTo({ ...status, ...attention() });
|
|
1082
1242
|
return fresh;
|
|
1083
1243
|
}
|
|
1084
|
-
async function
|
|
1085
|
-
await local.erase(locators);
|
|
1244
|
+
async function eraseFenced(locators) {
|
|
1086
1245
|
for (const locator of locators) {
|
|
1246
|
+
while (await durably((local) => local.erase(locator, ERASE_BATCH)))
|
|
1247
|
+
await yielded();
|
|
1087
1248
|
const held = fences.get(locator);
|
|
1088
1249
|
if (held !== undefined)
|
|
1089
1250
|
held.erased = true;
|
|
1090
1251
|
}
|
|
1091
1252
|
}
|
|
1253
|
+
let sweeping = false;
|
|
1254
|
+
function sweep() {
|
|
1255
|
+
if (sweeping || closed)
|
|
1256
|
+
return;
|
|
1257
|
+
sweeping = true;
|
|
1258
|
+
void (async () => {
|
|
1259
|
+
try {
|
|
1260
|
+
while (!closed && (await durably((local) => local.erase(null, ERASE_BATCH)))) {
|
|
1261
|
+
await yielded();
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
catch {
|
|
1265
|
+
journal.record('erasure failed');
|
|
1266
|
+
}
|
|
1267
|
+
finally {
|
|
1268
|
+
sweeping = false;
|
|
1269
|
+
}
|
|
1270
|
+
})();
|
|
1271
|
+
}
|
|
1092
1272
|
async function loseRooms(locators) {
|
|
1093
1273
|
const fresh = fence(locators);
|
|
1094
|
-
if (fresh.length
|
|
1095
|
-
|
|
1274
|
+
if (fresh.length === 0)
|
|
1275
|
+
return;
|
|
1276
|
+
await durably(async (local) => {
|
|
1277
|
+
await local.fence(fresh);
|
|
1278
|
+
await unpair(local);
|
|
1279
|
+
});
|
|
1280
|
+
await eraseFenced(fresh);
|
|
1281
|
+
}
|
|
1282
|
+
function retyped(persisted) {
|
|
1283
|
+
if (persisted.status === 'unavailable')
|
|
1284
|
+
return persisted;
|
|
1285
|
+
const { state } = attachedModel({ kind: 'model', snapshot: persisted.snapshot }, NO_MODEL, roomTypes);
|
|
1286
|
+
return state.status === 'unavailable' ? state : { ...persisted, types: state.types };
|
|
1096
1287
|
}
|
|
1097
|
-
function restored(key,
|
|
1288
|
+
function restored(key, held) {
|
|
1289
|
+
const persisted = retyped(held);
|
|
1098
1290
|
const prior = models.get(key);
|
|
1099
1291
|
if (prior !== undefined &&
|
|
1100
1292
|
prior.status !== 'unavailable' &&
|
|
@@ -1113,54 +1305,58 @@ export function composeRuntime(options) {
|
|
|
1113
1305
|
return model;
|
|
1114
1306
|
}
|
|
1115
1307
|
async function hydrate(opened) {
|
|
1116
|
-
const [
|
|
1117
|
-
opened.
|
|
1308
|
+
const [held, queues, kept, heldConversations, chatDrafts] = await Promise.all([
|
|
1309
|
+
opened.opened(),
|
|
1118
1310
|
opened.queues(),
|
|
1119
1311
|
opened.roomDrafts(),
|
|
1120
|
-
opened.presentation(),
|
|
1121
1312
|
opened.conversations(),
|
|
1122
1313
|
opened.chatDrafts(),
|
|
1123
|
-
opened.lastCompleted(),
|
|
1124
|
-
opened.providerLimits(),
|
|
1125
|
-
opened.capabilities(),
|
|
1126
|
-
opened.member(),
|
|
1127
|
-
opened.transfers(),
|
|
1128
|
-
opened.targets(),
|
|
1129
1314
|
]);
|
|
1130
|
-
|
|
1315
|
+
const records = [...held.rooms.values()];
|
|
1316
|
+
const counts = await opened.counts(records.map(({ key, typedSelection }) => ({ key, roots: typedSelection?.roots ?? [] })));
|
|
1317
|
+
const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
|
|
1318
|
+
targets = held.targets;
|
|
1131
1319
|
transfers.clear();
|
|
1132
|
-
for (const record of
|
|
1133
|
-
transfers.set(
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1320
|
+
for (const [locator, record] of held.transfers)
|
|
1321
|
+
transfers.set(locator, record);
|
|
1322
|
+
arrivals.clear();
|
|
1323
|
+
keptParts.clear();
|
|
1324
|
+
for (const [locator, parts] of held.kept)
|
|
1325
|
+
keptParts.set(locator, new Map(parts));
|
|
1326
|
+
limits = held.providerLimits;
|
|
1327
|
+
capabilities = held.capabilities;
|
|
1328
|
+
member = held.member;
|
|
1137
1329
|
roomDrafts.clear();
|
|
1138
1330
|
for (const record of kept)
|
|
1139
1331
|
rememberRoomDraft(record);
|
|
1140
1332
|
presentations.clear();
|
|
1141
|
-
for (const record of
|
|
1333
|
+
for (const record of held.presentation)
|
|
1142
1334
|
presentations.set(record.app, record.value);
|
|
1143
|
-
conversations.hydrate(
|
|
1144
|
-
const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
|
|
1335
|
+
conversations.hydrate(heldConversations, chatDrafts);
|
|
1145
1336
|
pendings.clear();
|
|
1146
1337
|
for (const queue of queues) {
|
|
1147
|
-
for (const { batch, state } of queue.pending)
|
|
1148
|
-
pendings.set(batch.id, state);
|
|
1338
|
+
for (const { batch, state } of queue.pending) {
|
|
1339
|
+
pendings.set(pendingKey(queue.locator, batch.id), state);
|
|
1340
|
+
}
|
|
1149
1341
|
}
|
|
1150
|
-
const previousReplicas = new Map(replicas);
|
|
1151
1342
|
replicas.clear();
|
|
1152
1343
|
queuesWithoutReplica.clear();
|
|
1153
|
-
const replicated = new Set(records.map((record) => record.locator));
|
|
1154
1344
|
for (const [locator, queue] of queuedBatches) {
|
|
1155
|
-
if (!
|
|
1345
|
+
if (!held.rooms.has(locator))
|
|
1156
1346
|
queuesWithoutReplica.set(locator, queue);
|
|
1157
1347
|
}
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
const
|
|
1162
|
-
|
|
1163
|
-
|
|
1348
|
+
records.forEach((stored, index) => {
|
|
1349
|
+
if (fences.has(stored.locator))
|
|
1350
|
+
return;
|
|
1351
|
+
const record = { ...stored, model: restored(stored.locator, stored.model) };
|
|
1352
|
+
replicas.set(record.locator, {
|
|
1353
|
+
record,
|
|
1354
|
+
queue: queuedBatches.get(record.locator) ?? [],
|
|
1355
|
+
overlaid: held.overlaid.get(record.key) ?? new Set(),
|
|
1356
|
+
count: counts[index] ?? 0,
|
|
1357
|
+
confirmed: 0,
|
|
1358
|
+
indexed: !held.unindexed.has(record.key),
|
|
1359
|
+
});
|
|
1164
1360
|
const existing = rooms.get(record.locator);
|
|
1165
1361
|
if (existing === undefined)
|
|
1166
1362
|
list(record, false);
|
|
@@ -1170,20 +1366,21 @@ export function composeRuntime(options) {
|
|
|
1170
1366
|
roomVersion: record.roomVersion,
|
|
1171
1367
|
operations: record.operations,
|
|
1172
1368
|
replica: record.replica,
|
|
1173
|
-
model: modelTypes(model),
|
|
1369
|
+
model: modelTypes(record.model),
|
|
1174
1370
|
typedSelection: record.typedSelection === null
|
|
1175
1371
|
? null
|
|
1176
|
-
: {
|
|
1177
|
-
roots: record.typedSelection.roots,
|
|
1178
|
-
model: modelTypes(selectionModel),
|
|
1179
|
-
},
|
|
1372
|
+
: { roots: record.typedSelection.roots, model: modelTypes(builtIn) },
|
|
1180
1373
|
});
|
|
1181
1374
|
}
|
|
1182
|
-
}
|
|
1375
|
+
});
|
|
1376
|
+
for (const key of held.orphans)
|
|
1377
|
+
await opened.release(key);
|
|
1378
|
+
if (held.erasing || held.orphans.length > 0)
|
|
1379
|
+
queueMicrotask(sweep);
|
|
1183
1380
|
return {
|
|
1184
|
-
lastCompleted,
|
|
1381
|
+
lastCompleted: held.lastCompleted,
|
|
1185
1382
|
locators: new Set([...queuedBatches.keys(), ...replicas.keys()]),
|
|
1186
|
-
read: mergeChangeSets([...
|
|
1383
|
+
read: mergeChangeSets([...replicas.keys()].map((locator) => changeSet(locator, { documents: null }))),
|
|
1187
1384
|
};
|
|
1188
1385
|
}
|
|
1189
1386
|
async function readState(opened) {
|
|
@@ -1202,8 +1399,7 @@ export function composeRuntime(options) {
|
|
|
1202
1399
|
async function open() {
|
|
1203
1400
|
if (closed)
|
|
1204
1401
|
throw new Error('runtime closed');
|
|
1205
|
-
const opened =
|
|
1206
|
-
store = opened;
|
|
1402
|
+
const opened = openRuntimeStore(isFenced, database);
|
|
1207
1403
|
const interrupted = await opened.fences();
|
|
1208
1404
|
for (const locator of interrupted) {
|
|
1209
1405
|
fences.set(locator, { at: discoveries, erased: false });
|
|
@@ -1222,9 +1418,17 @@ export function composeRuntime(options) {
|
|
|
1222
1418
|
emitStorageLoss();
|
|
1223
1419
|
}
|
|
1224
1420
|
const lastCompleted = await readState(opened);
|
|
1225
|
-
if (interrupted.length > 0)
|
|
1226
|
-
|
|
1421
|
+
if (interrupted.length > 0) {
|
|
1422
|
+
queueMicrotask(() => {
|
|
1423
|
+
void eraseFenced(interrupted).catch(() => {
|
|
1424
|
+
journal.record('erasure failed');
|
|
1425
|
+
});
|
|
1426
|
+
});
|
|
1427
|
+
}
|
|
1227
1428
|
moveTo({ ...status, lastCompleted, ...attention() });
|
|
1429
|
+
void exclusive(() => unpair(opened)).catch(() => {
|
|
1430
|
+
journal.record('unpair failed');
|
|
1431
|
+
});
|
|
1228
1432
|
return opened;
|
|
1229
1433
|
}
|
|
1230
1434
|
function durably(work) {
|
|
@@ -1241,180 +1445,376 @@ export function composeRuntime(options) {
|
|
|
1241
1445
|
return opening;
|
|
1242
1446
|
}
|
|
1243
1447
|
function exclusive(work) {
|
|
1448
|
+
gathering = null;
|
|
1244
1449
|
const next = mutations.then(work, work);
|
|
1245
1450
|
mutations = next.then(() => undefined, () => undefined);
|
|
1246
1451
|
return next;
|
|
1247
1452
|
}
|
|
1248
|
-
function
|
|
1249
|
-
const
|
|
1250
|
-
|
|
1453
|
+
function gathered(key, input, merge, work) {
|
|
1454
|
+
const tail = gathering;
|
|
1455
|
+
if (tail !== null && tail.key === key) {
|
|
1456
|
+
const merged = merge(tail.input, input);
|
|
1457
|
+
if (merged !== null) {
|
|
1458
|
+
tail.input = merged;
|
|
1459
|
+
return tail.done;
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
const entry = { key, input, done: Promise.resolve() };
|
|
1463
|
+
entry.done = exclusive(() => {
|
|
1464
|
+
if (gathering === entry)
|
|
1465
|
+
gathering = null;
|
|
1466
|
+
return work(entry.input);
|
|
1467
|
+
});
|
|
1468
|
+
gathering = entry;
|
|
1469
|
+
return entry.done;
|
|
1470
|
+
}
|
|
1471
|
+
function withdrawable(locator) {
|
|
1472
|
+
return (batch) => {
|
|
1473
|
+
const state = pendings.get(pendingKey(locator, batch.id));
|
|
1474
|
+
return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1477
|
+
const REWRITE_LINKS = ['field', 'body'];
|
|
1478
|
+
const CHECKED_LINKS = ['field', 'section'];
|
|
1479
|
+
async function sliceAt(record, view, queue, extra = [], moved = []) {
|
|
1480
|
+
const local = await openLocal();
|
|
1481
|
+
const needed = new Set([...touchedBy(queue), ...extra]);
|
|
1482
|
+
const movedPaths = [...new Set([...movedBy(queue, record.locator), ...moved])];
|
|
1483
|
+
if (movedPaths.length > 0) {
|
|
1484
|
+
for (const { path } of await local.referrers(record.key, view, movedPaths, REWRITE_LINKS)) {
|
|
1485
|
+
needed.add(path);
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
const paths = [...needed];
|
|
1489
|
+
return stepped(slicing(await local.entries(record.key, view, paths), paths));
|
|
1251
1490
|
}
|
|
1252
|
-
async function
|
|
1491
|
+
async function landedWhole(locator, batch, confirmedLayer, current) {
|
|
1492
|
+
if (!landed(batch, confirmedLayer, current))
|
|
1493
|
+
return false;
|
|
1494
|
+
const partner = partnerOf(batch, locator);
|
|
1495
|
+
if (partner === null)
|
|
1496
|
+
return true;
|
|
1497
|
+
const other = replicas.get(partner);
|
|
1498
|
+
const half = other?.queue.find((one) => one.id === batch.id);
|
|
1499
|
+
if (other === undefined || half === undefined)
|
|
1500
|
+
return false;
|
|
1501
|
+
return landed(half, await sliceAt(other.record, 'confirmed', [half]));
|
|
1502
|
+
}
|
|
1503
|
+
function carriedBack(locator, unlanded, written) {
|
|
1504
|
+
const back = new Map();
|
|
1505
|
+
const taken = [];
|
|
1506
|
+
for (const batch of unlanded) {
|
|
1507
|
+
const between = batch.between;
|
|
1508
|
+
if (between === undefined)
|
|
1509
|
+
continue;
|
|
1510
|
+
const source = between.source.room;
|
|
1511
|
+
const destination = between.destination.room;
|
|
1512
|
+
for (const { path: to } of pendings.get(pendingKey(destination, batch.id))?.drafts ?? []) {
|
|
1513
|
+
const record = roomDrafts.get(destination)?.get(to);
|
|
1514
|
+
const path = between.moves.find((move) => move.to === to)?.path;
|
|
1515
|
+
if (record === undefined || path === undefined)
|
|
1516
|
+
continue;
|
|
1517
|
+
const [into, key] = source === locator ? [written, path] : [back, documentKey(source, path)];
|
|
1518
|
+
const existing = into.get(key) ?? roomDrafts.get(source)?.get(path);
|
|
1519
|
+
taken.push({ locator: destination, path: to });
|
|
1520
|
+
into.set(key, {
|
|
1521
|
+
locator: source,
|
|
1522
|
+
path,
|
|
1523
|
+
revision: roomDraftRevision(),
|
|
1524
|
+
parts: returnedInto(existing?.parts, record.parts),
|
|
1525
|
+
returned: existing?.returned ?? null,
|
|
1526
|
+
});
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
const records = [...back.values()];
|
|
1530
|
+
const elsewhere = taken.filter((one) => one.locator !== locator);
|
|
1531
|
+
const rooms = new Set([...records, ...elsewhere].map((one) => one.locator));
|
|
1532
|
+
return {
|
|
1533
|
+
taken: taken.filter((one) => one.locator === locator).map(({ path }) => path),
|
|
1534
|
+
writes: [...rooms].map((room) => ({
|
|
1535
|
+
locator: room,
|
|
1536
|
+
roomDrafts: records.filter((record) => record.locator === room),
|
|
1537
|
+
consumedRoomDrafts: elsewhere.filter((one) => one.locator === room).map(({ path }) => path),
|
|
1538
|
+
})),
|
|
1539
|
+
changes: [...rooms].map((room) => changeSet(room, {
|
|
1540
|
+
drafts: [...records, ...elsewhere]
|
|
1541
|
+
.filter((one) => one.locator === room)
|
|
1542
|
+
.map(({ path }) => path),
|
|
1543
|
+
})),
|
|
1544
|
+
remember() {
|
|
1545
|
+
for (const one of taken)
|
|
1546
|
+
forgetRoomDraft(one.locator, one.path);
|
|
1547
|
+
for (const record of records)
|
|
1548
|
+
rememberRoomDraft(record);
|
|
1549
|
+
},
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1552
|
+
function committedRoom(locator, answer) {
|
|
1553
|
+
const indexed = answer.indexed[locator] ?? replicas.get(locator)?.indexed ?? false;
|
|
1554
|
+
if (!indexed)
|
|
1555
|
+
prepare([locator]);
|
|
1556
|
+
return { count: answer.counts[locator] ?? replicas.get(locator)?.count ?? 0, indexed };
|
|
1557
|
+
}
|
|
1558
|
+
function replayed(record, confirmedLayer, queue) {
|
|
1559
|
+
return stepped(replaying(confirmedLayer, queue, record.locator, composed(record)));
|
|
1560
|
+
}
|
|
1561
|
+
async function overlayDiff(record, before, visible, touched) {
|
|
1562
|
+
const kept = before !== undefined && before.record.key === record.key
|
|
1563
|
+
? before.overlaid
|
|
1564
|
+
: new Set();
|
|
1565
|
+
return {
|
|
1566
|
+
overlay: {
|
|
1567
|
+
set: await stepped(overlayOf(visible, touched)),
|
|
1568
|
+
cleared: [...kept].filter((path) => !touched.has(path)),
|
|
1569
|
+
},
|
|
1570
|
+
touched,
|
|
1571
|
+
};
|
|
1572
|
+
}
|
|
1573
|
+
async function settle(local, locator, before, record, queue, settling = {}) {
|
|
1574
|
+
return together(local, [await settlement(local, locator, before, record, queue, settling)]);
|
|
1575
|
+
}
|
|
1576
|
+
function unpairedIn(locator, batch) {
|
|
1577
|
+
if (!queueIn(locator).some((held) => held.id === batch.id))
|
|
1578
|
+
return false;
|
|
1579
|
+
if (pendings.get(pendingKey(locator, batch.id))?.accepted !== undefined)
|
|
1580
|
+
return false;
|
|
1581
|
+
return !leavingIn(batch, locator) || withdrawable(locator)(batch);
|
|
1582
|
+
}
|
|
1583
|
+
async function together(local, settlements) {
|
|
1584
|
+
const settled = new Set(settlements.map(({ locator }) => locator));
|
|
1585
|
+
const all = [...settlements];
|
|
1586
|
+
for (let wave = settlements; wave.length > 0;) {
|
|
1587
|
+
const partnered = new Map();
|
|
1588
|
+
for (const { locator, left } of wave) {
|
|
1589
|
+
for (const batch of left) {
|
|
1590
|
+
const partner = partnerOf(batch, locator);
|
|
1591
|
+
if (partner === null || settled.has(partner) || !unpairedIn(partner, batch))
|
|
1592
|
+
continue;
|
|
1593
|
+
partnered.set(partner, [...(partnered.get(partner) ?? []), batch]);
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
const partners = [];
|
|
1597
|
+
for (const [partner, batches] of partnered) {
|
|
1598
|
+
settled.add(partner);
|
|
1599
|
+
partners.push(await unpairing(local, partner, batches));
|
|
1600
|
+
}
|
|
1601
|
+
all.push(...partners);
|
|
1602
|
+
wave = partners;
|
|
1603
|
+
}
|
|
1604
|
+
const answer = await local.commit({
|
|
1605
|
+
rooms: all.flatMap(({ commit }) => commit.rooms),
|
|
1606
|
+
writes: all.flatMap(({ commit }) => commit.writes),
|
|
1607
|
+
});
|
|
1608
|
+
return mergeChangeSets(all.map((one) => one.apply(answer)));
|
|
1609
|
+
}
|
|
1610
|
+
async function settlement(local, locator, before, record, queue, settling = {}) {
|
|
1611
|
+
const view = settling.view ?? 'confirmed';
|
|
1253
1612
|
const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
1254
|
-
const
|
|
1613
|
+
const overrides = settling.changes;
|
|
1614
|
+
const read = await sliceAt(record, view, queue, [
|
|
1615
|
+
...(before !== undefined && before.record.key === record.key ? before.overlaid : []),
|
|
1616
|
+
...(overrides?.set.map(({ path }) => path) ?? []),
|
|
1617
|
+
...(overrides?.removed ?? []),
|
|
1618
|
+
]);
|
|
1619
|
+
const confirmedLayer = overrides === undefined
|
|
1620
|
+
? read
|
|
1621
|
+
: await stepped(slicingWith(read, overrides.set, overrides.removed));
|
|
1622
|
+
const composition = composed(record);
|
|
1623
|
+
const { withdrawn, kept } = await stepped(standing(confirmedLayer, queue, withdrawable(locator), localSaves, locator, composition));
|
|
1255
1624
|
const founded = kept.filter((batch) => !queue.includes(batch));
|
|
1256
1625
|
const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
|
|
1257
|
-
const unqueued =
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
}
|
|
1626
|
+
const unqueued = [];
|
|
1627
|
+
for (const batch of withdrawn) {
|
|
1628
|
+
unqueued.push({ batch, landed: await landedWhole(locator, batch, confirmedLayer) });
|
|
1629
|
+
}
|
|
1261
1630
|
const returning = [
|
|
1262
|
-
...[...unqueued].reverse(),
|
|
1263
|
-
|
|
1631
|
+
...[...unqueued].reverse().map(({ batch, landed: held }) => ({
|
|
1632
|
+
batch,
|
|
1633
|
+
returned: localSaves || held ? null : returnedBy('withdrawn'),
|
|
1634
|
+
})),
|
|
1635
|
+
...(settling.leaving ?? []),
|
|
1264
1636
|
];
|
|
1265
1637
|
const written = new Map();
|
|
1266
|
-
for (const { batch, returned } of returning) {
|
|
1267
|
-
if (
|
|
1638
|
+
for (const { batch, returned: reason } of returning) {
|
|
1639
|
+
if (reason === null || batch.between?.destination.room === locator)
|
|
1268
1640
|
continue;
|
|
1269
|
-
for (const saved of pendings.get(batch.id)?.drafts ?? []) {
|
|
1641
|
+
for (const saved of pendings.get(pendingKey(locator, batch.id))?.drafts ?? []) {
|
|
1270
1642
|
const existing = written.get(saved.path) ?? roomDrafts.get(locator)?.get(saved.path);
|
|
1271
1643
|
written.set(saved.path, {
|
|
1272
1644
|
locator,
|
|
1273
1645
|
path: saved.path,
|
|
1274
1646
|
revision: roomDraftRevision(),
|
|
1275
1647
|
parts: returnedInto(existing?.parts, saved.parts),
|
|
1276
|
-
returned,
|
|
1648
|
+
returned: reason,
|
|
1277
1649
|
});
|
|
1278
1650
|
}
|
|
1279
1651
|
}
|
|
1280
|
-
|
|
1652
|
+
const carried = carriedBack(locator, [
|
|
1653
|
+
...[...unqueued].reverse().flatMap(({ batch, landed: held }) => (held ? [] : [batch])),
|
|
1654
|
+
...(settling.leaving ?? []).flatMap(({ batch, returned }) => returned === null ? [] : [batch]),
|
|
1655
|
+
], written);
|
|
1656
|
+
const replayedLayer = await replayed(record, confirmedLayer, kept);
|
|
1657
|
+
const { overlay, touched } = await overlayDiff(record, before, replayedLayer.visible, replayedLayer.touched);
|
|
1658
|
+
const commit = {
|
|
1659
|
+
rooms: [
|
|
1660
|
+
{
|
|
1661
|
+
locator,
|
|
1662
|
+
key: record.key,
|
|
1663
|
+
record: storedRecord(record),
|
|
1664
|
+
roots: record.typedSelection?.roots ?? [],
|
|
1665
|
+
...settling.commit,
|
|
1666
|
+
...(overrides === undefined
|
|
1667
|
+
? {}
|
|
1668
|
+
: { confirmed: { set: overrides.set, removed: overrides.removed } }),
|
|
1669
|
+
overlay,
|
|
1670
|
+
},
|
|
1671
|
+
],
|
|
1672
|
+
writes: [
|
|
1673
|
+
{
|
|
1674
|
+
locator,
|
|
1675
|
+
queue: kept,
|
|
1676
|
+
founded,
|
|
1677
|
+
roomDrafts: [...written.values()],
|
|
1678
|
+
consumedRoomDrafts: carried.taken,
|
|
1679
|
+
},
|
|
1680
|
+
...carried.writes,
|
|
1681
|
+
],
|
|
1682
|
+
};
|
|
1281
1683
|
const staying = new Set(kept.map((batch) => batch.id));
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
catch (error) {
|
|
1296
|
-
const held = before === undefined ? null : await stepped(replicating(before.confirmed, kept, before));
|
|
1297
|
-
const set = await stepped(changing(locator, before ?? null, held, {
|
|
1298
|
-
queue: queueChanged,
|
|
1299
|
-
drafts: [...written.keys()],
|
|
1300
|
-
}));
|
|
1301
|
-
if (held !== null)
|
|
1302
|
-
replicas.set(locator, held);
|
|
1303
|
-
else if (kept.length > 0)
|
|
1304
|
-
queuesWithoutReplica.set(locator, kept);
|
|
1305
|
-
else
|
|
1684
|
+
return {
|
|
1685
|
+
locator,
|
|
1686
|
+
left: previousQueue.filter((batch) => !staying.has(batch.id)),
|
|
1687
|
+
commit,
|
|
1688
|
+
apply: (answer) => {
|
|
1689
|
+
for (const batch of previousQueue) {
|
|
1690
|
+
if (!staying.has(batch.id))
|
|
1691
|
+
pendings.delete(pendingKey(locator, batch.id));
|
|
1692
|
+
}
|
|
1693
|
+
for (const draft of written.values())
|
|
1694
|
+
rememberRoomDraft(draft);
|
|
1695
|
+
carried.remember();
|
|
1696
|
+
const drafts = [...written.keys(), ...carried.taken];
|
|
1306
1697
|
queuesWithoutReplica.delete(locator);
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1698
|
+
for (const { batch, landed: held } of unqueued) {
|
|
1699
|
+
const outcome = localSaves ? 'dropped' : held ? 'landed' : 'withdrawn';
|
|
1700
|
+
journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
|
|
1701
|
+
}
|
|
1702
|
+
const changed = answer.changed[locator];
|
|
1703
|
+
const confirmedChanged = settling.commit?.published !== undefined ||
|
|
1704
|
+
settling.commit?.refreshed === true ||
|
|
1705
|
+
overrides !== undefined;
|
|
1706
|
+
const { count, indexed } = committedRoom(locator, answer);
|
|
1707
|
+
replicas.set(locator, {
|
|
1708
|
+
record,
|
|
1709
|
+
queue: kept,
|
|
1710
|
+
overlaid: touched,
|
|
1711
|
+
count,
|
|
1712
|
+
confirmed: (before?.confirmed ?? 0) + (confirmedChanged ? 1 : 0),
|
|
1713
|
+
indexed,
|
|
1714
|
+
});
|
|
1715
|
+
return mergeChangeSets([
|
|
1716
|
+
changeSet(locator, {
|
|
1717
|
+
documents: changed === undefined ? [] : changed,
|
|
1718
|
+
room: settling.room,
|
|
1719
|
+
queue: queueChanged,
|
|
1720
|
+
drafts,
|
|
1721
|
+
}),
|
|
1722
|
+
...carried.changes,
|
|
1723
|
+
]);
|
|
1724
|
+
},
|
|
1322
1725
|
};
|
|
1323
1726
|
}
|
|
1324
|
-
function heldChange(locator, before, next, set, changed) {
|
|
1325
|
-
const held = replicas.get(locator) ?? null;
|
|
1326
|
-
return held === next ? set : changeSet(locator, before, held, changed);
|
|
1327
|
-
}
|
|
1328
1727
|
function adoptModels(locator, record) {
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
if (model.status === 'current')
|
|
1337
|
-
liveModels.set(key, JSON.stringify(model.snapshot));
|
|
1338
|
-
else if (model.status === 'unavailable')
|
|
1339
|
-
liveModels.delete(key);
|
|
1340
|
-
}
|
|
1728
|
+
const model = record.model;
|
|
1729
|
+
models.set(locator, model);
|
|
1730
|
+
invalidModels.delete(locator);
|
|
1731
|
+
if (model.status === 'current')
|
|
1732
|
+
liveModels.set(locator, JSON.stringify(model.snapshot));
|
|
1733
|
+
else if (model.status === 'unavailable')
|
|
1734
|
+
liveModels.delete(locator);
|
|
1341
1735
|
}
|
|
1342
1736
|
function remodel(locator, answers, held) {
|
|
1343
|
-
const
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
selection: taken(answers?.selection, held.selection, 'typed-selection'),
|
|
1357
|
-
};
|
|
1358
|
-
}
|
|
1359
|
-
function publish(local, room, confirmed, landing, live) {
|
|
1360
|
-
const { base, stored } = landing;
|
|
1361
|
-
return exclusive(async () => {
|
|
1362
|
-
if (stored !== undefined) {
|
|
1363
|
-
await indexReferrers({ text: confirmed.record.documents, binaries: confirmed.record.binaries }, compositionOf(confirmed.record));
|
|
1364
|
-
}
|
|
1737
|
+
const answer = answers?.collection;
|
|
1738
|
+
if (answer === undefined)
|
|
1739
|
+
return held;
|
|
1740
|
+
const { state, refused } = attachedModel(answer, held.collection, roomTypes);
|
|
1741
|
+
if (refused !== null) {
|
|
1742
|
+
const where = refused.location === undefined ? '' : ` ${refused.location}`;
|
|
1743
|
+
journal.record(`model invalid ${locator} collection ${refused.identity} ${refused.rule}${where}`);
|
|
1744
|
+
}
|
|
1745
|
+
models.set(locator, state);
|
|
1746
|
+
return { collection: state };
|
|
1747
|
+
}
|
|
1748
|
+
function publish(local, room, record, landing, live) {
|
|
1749
|
+
const landed = exclusive(async () => {
|
|
1365
1750
|
if (!live()) {
|
|
1366
1751
|
caughtUpTo(room.locator);
|
|
1367
1752
|
return true;
|
|
1368
1753
|
}
|
|
1369
1754
|
const prior = replicas.get(room.locator);
|
|
1370
|
-
if (
|
|
1371
|
-
(prior
|
|
1372
|
-
|
|
1755
|
+
if (landing.kind === 'refreshed' &&
|
|
1756
|
+
(prior === undefined || prior.record.key !== record.key || prior.confirmed !== landing.base)) {
|
|
1757
|
+
await local.unrefresh(record.key);
|
|
1373
1758
|
return false;
|
|
1374
1759
|
}
|
|
1375
|
-
adoptModels(room.locator,
|
|
1760
|
+
adoptModels(room.locator, record);
|
|
1376
1761
|
const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
|
|
1377
|
-
const roomVersion =
|
|
1762
|
+
const roomVersion = record.roomVersion;
|
|
1378
1763
|
const accepted = queue.filter((batch) => {
|
|
1379
|
-
const
|
|
1380
|
-
return
|
|
1764
|
+
const at = pendings.get(pendingKey(room.locator, batch.id))?.accepted;
|
|
1765
|
+
return at !== undefined && options.adapter.reached(room.locator, roomVersion, at);
|
|
1381
1766
|
});
|
|
1382
|
-
let
|
|
1767
|
+
let set;
|
|
1383
1768
|
try {
|
|
1384
|
-
|
|
1769
|
+
set = await settle(local, room.locator, prior, record, queue.filter((batch) => !accepted.includes(batch)), landing.kind === 'refreshed'
|
|
1770
|
+
? {
|
|
1771
|
+
room: true,
|
|
1772
|
+
view: 'refreshed',
|
|
1773
|
+
commit: { refreshed: true, composition: composed(record) },
|
|
1774
|
+
}
|
|
1775
|
+
: {
|
|
1776
|
+
room: true,
|
|
1777
|
+
view: landing.refreshed ? 'refreshed' : 'confirmed',
|
|
1778
|
+
commit: {
|
|
1779
|
+
published: { from: prior?.record.key ?? null },
|
|
1780
|
+
composition: composed(record),
|
|
1781
|
+
...(landing.refreshed ? { refreshed: true } : {}),
|
|
1782
|
+
},
|
|
1783
|
+
});
|
|
1385
1784
|
}
|
|
1386
1785
|
catch (error) {
|
|
1387
|
-
if (!
|
|
1786
|
+
if (!refusedForSpace(error))
|
|
1388
1787
|
throw error;
|
|
1389
|
-
await exceededOn(room.locator, grownBy(confirmed.record, prior?.confirmed.record));
|
|
1390
1788
|
caughtUpTo(room.locator);
|
|
1391
1789
|
return true;
|
|
1392
1790
|
}
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
transfers.delete(room.locator);
|
|
1791
|
+
if (landing.kind === 'bootstrapped') {
|
|
1792
|
+
untransferred(room.locator);
|
|
1396
1793
|
catching.delete(room.locator);
|
|
1794
|
+
sweep();
|
|
1397
1795
|
}
|
|
1398
1796
|
for (const batch of accepted)
|
|
1399
|
-
pendings.delete(batch.id);
|
|
1797
|
+
pendings.delete(pendingKey(room.locator, batch.id));
|
|
1400
1798
|
stale.delete(room.locator);
|
|
1401
|
-
|
|
1799
|
+
unpreparable.delete(room.locator);
|
|
1402
1800
|
if (unreadable.delete(room.locator))
|
|
1403
1801
|
scopedRefusals.delete(room.locator);
|
|
1404
1802
|
sourceRefusals.delete(room.locator);
|
|
1405
1803
|
declarationRefusals.delete(room.locator);
|
|
1406
1804
|
list(room, true);
|
|
1407
|
-
if (prior === undefined)
|
|
1408
|
-
prepared.add(room.locator);
|
|
1409
1805
|
publishChangeSet(set);
|
|
1806
|
+
await unpair(local);
|
|
1410
1807
|
moveTo({ ...status, ...attention() });
|
|
1411
1808
|
options.adapter.roomStream?.published(room.locator, roomVersion);
|
|
1809
|
+
if (accepted.length > 0 && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
|
|
1810
|
+
submitLater(room.locator);
|
|
1811
|
+
}
|
|
1412
1812
|
return true;
|
|
1413
1813
|
});
|
|
1814
|
+
return landing.kind === 'refreshed' ? landed : landed.finally(() => installed(room.locator));
|
|
1414
1815
|
}
|
|
1415
1816
|
async function savedRoom(locator) {
|
|
1416
1817
|
const local = await openLocal();
|
|
1417
|
-
await referenced(locator);
|
|
1418
1818
|
const replica = replicas.get(locator);
|
|
1419
1819
|
const room = rooms.get(locator);
|
|
1420
1820
|
if (replica === undefined || room === undefined) {
|
|
@@ -1444,13 +1844,19 @@ export function composeRuntime(options) {
|
|
|
1444
1844
|
}
|
|
1445
1845
|
const target = await savedRoom(locator);
|
|
1446
1846
|
const saved = [];
|
|
1847
|
+
const files = [];
|
|
1848
|
+
const filed = read.filter(({ parts }) => parts.file !== undefined).map(({ path }) => path);
|
|
1849
|
+
const visible = filed.length === 0 ? undefined : await visibleOf(locator, filed);
|
|
1447
1850
|
for (const { path, parts } of read) {
|
|
1448
1851
|
const file = parts.file === undefined
|
|
1449
1852
|
? undefined
|
|
1450
|
-
: await heldFile(parts.file.blob, binaryBase(
|
|
1853
|
+
: await heldFile(parts.file.blob, binaryBase(visible, path));
|
|
1854
|
+
if (parts.file !== undefined && file !== undefined) {
|
|
1855
|
+
files.push({ key: file.localId, blob: parts.file.blob });
|
|
1856
|
+
}
|
|
1451
1857
|
saved.push({ path, parts: held(undefined, parts, file) });
|
|
1452
1858
|
}
|
|
1453
|
-
return committed(locator, target, saved, new Set(), writtenBy);
|
|
1859
|
+
return committed(locator, target, saved, new Set(), writtenBy, files);
|
|
1454
1860
|
}
|
|
1455
1861
|
function carriedDrafts(locator, saved, consumed) {
|
|
1456
1862
|
const holding = roomDrafts.get(locator);
|
|
@@ -1464,13 +1870,355 @@ export function composeRuntime(options) {
|
|
|
1464
1870
|
return [[path, { ...record, path: to, revision: roomDraftRevision() }]];
|
|
1465
1871
|
}));
|
|
1466
1872
|
}
|
|
1467
|
-
async function
|
|
1468
|
-
const
|
|
1873
|
+
async function savingIn(replica, changes, extra, leaving = []) {
|
|
1874
|
+
const record = replica.record;
|
|
1875
|
+
const local = await openLocal();
|
|
1876
|
+
const paths = [
|
|
1877
|
+
...extra,
|
|
1878
|
+
...changes.flatMap((change) => change.kind === 'move' ? [change.path, change.to] : [change.path]),
|
|
1879
|
+
];
|
|
1880
|
+
const moved = [
|
|
1881
|
+
...leaving,
|
|
1882
|
+
...changes.flatMap((change) => (change.kind === 'move' ? [change.path] : [])),
|
|
1883
|
+
];
|
|
1884
|
+
const removed = changes.flatMap((change) => (change.kind === 'remove' ? [change.path] : []));
|
|
1885
|
+
const referring = removed.length === 0
|
|
1886
|
+
? []
|
|
1887
|
+
: (await local.referrers(record.key, 'confirmed', removed, CHECKED_LINKS)).map(({ path }) => path);
|
|
1888
|
+
const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...referring], moved);
|
|
1889
|
+
const { visible } = await replayed(record, confirmedLayer, replica.queue);
|
|
1890
|
+
const composition = composed(record);
|
|
1891
|
+
return {
|
|
1892
|
+
record,
|
|
1893
|
+
confirmed: confirmedLayer,
|
|
1894
|
+
visible,
|
|
1895
|
+
*visibleOver(queue) {
|
|
1896
|
+
return (yield* replaying(confirmedLayer, queue, record.locator, composition)).visible;
|
|
1897
|
+
},
|
|
1898
|
+
};
|
|
1899
|
+
}
|
|
1900
|
+
async function unmodeledIn(record) {
|
|
1901
|
+
const composition = compositionFor(record);
|
|
1902
|
+
if (available(composition))
|
|
1903
|
+
return false;
|
|
1904
|
+
const local = await openLocal();
|
|
1905
|
+
const roots = record.typedSelection?.roots ?? [];
|
|
1906
|
+
return local.typedWithin(record.key, 'visible', roots, false);
|
|
1907
|
+
}
|
|
1908
|
+
function textDiffers(left, right) {
|
|
1909
|
+
return ((left?.content ?? undefined) !== (right?.content ?? undefined) ||
|
|
1910
|
+
!sameBinary(left?.binary ?? undefined, right?.binary ?? undefined));
|
|
1911
|
+
}
|
|
1912
|
+
function textOf(entry) {
|
|
1913
|
+
if (entry === null)
|
|
1914
|
+
return undefined;
|
|
1915
|
+
return entry.binary !== null ? null : entry.content;
|
|
1916
|
+
}
|
|
1917
|
+
function walkOf(local, key, view) {
|
|
1918
|
+
return async (after) => {
|
|
1919
|
+
const page = await local.walk(key, view, after, WALK_BYTES);
|
|
1920
|
+
return {
|
|
1921
|
+
entries: page.entries.map(({ path, content, binary }) => ({
|
|
1922
|
+
path,
|
|
1923
|
+
content: binary !== null ? null : content,
|
|
1924
|
+
})),
|
|
1925
|
+
next: page.next,
|
|
1926
|
+
};
|
|
1927
|
+
};
|
|
1928
|
+
}
|
|
1929
|
+
function lookupOf(local, key, view) {
|
|
1930
|
+
return async (paths) => {
|
|
1931
|
+
if (paths.length === 0)
|
|
1932
|
+
return new Map();
|
|
1933
|
+
return new Map((await local.typed(key, view, paths)).map(({ path, type, binary }) => [
|
|
1934
|
+
path,
|
|
1935
|
+
{ type: type ?? undefined, binary },
|
|
1936
|
+
]));
|
|
1937
|
+
};
|
|
1938
|
+
}
|
|
1939
|
+
function declaredOf(entry) {
|
|
1940
|
+
if (entry.binary !== null)
|
|
1941
|
+
return { type: undefined, binary: true };
|
|
1942
|
+
const read = parseFrontmatter(entry.content ?? '');
|
|
1943
|
+
return { type: read.ok ? (read.frontmatter.type ?? 'any') : undefined, binary: false };
|
|
1944
|
+
}
|
|
1945
|
+
function overriddenWalk(walk, overrides) {
|
|
1946
|
+
const sorted = [...overrides.keys()].sort();
|
|
1947
|
+
const beyond = (path) => {
|
|
1948
|
+
if (path === null)
|
|
1949
|
+
return 0;
|
|
1950
|
+
let low = 0;
|
|
1951
|
+
let high = sorted.length;
|
|
1952
|
+
while (low < high) {
|
|
1953
|
+
const middle = (low + high) >>> 1;
|
|
1954
|
+
if (sorted[middle] <= path)
|
|
1955
|
+
low = middle + 1;
|
|
1956
|
+
else
|
|
1957
|
+
high = middle;
|
|
1958
|
+
}
|
|
1959
|
+
return low;
|
|
1960
|
+
};
|
|
1961
|
+
return async (after) => {
|
|
1962
|
+
const page = await walk(after);
|
|
1963
|
+
const end = page.next;
|
|
1964
|
+
const added = sorted
|
|
1965
|
+
.slice(beyond(after), end === null ? sorted.length : beyond(end))
|
|
1966
|
+
.flatMap((path) => {
|
|
1967
|
+
const entry = overrides.get(path);
|
|
1968
|
+
return entry === null || entry === undefined
|
|
1969
|
+
? []
|
|
1970
|
+
: [{ path, content: entry.binary !== null ? null : entry.content }];
|
|
1971
|
+
});
|
|
1972
|
+
const entries = [...page.entries.filter(({ path }) => !overrides.has(path)), ...added].sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0));
|
|
1973
|
+
return { entries, next: end };
|
|
1974
|
+
};
|
|
1975
|
+
}
|
|
1976
|
+
function overriddenLookup(lookup, overrides) {
|
|
1977
|
+
return async (paths) => {
|
|
1978
|
+
const known = new Map(await lookup(paths.filter((path) => !overrides.has(path))));
|
|
1979
|
+
for (const path of paths) {
|
|
1980
|
+
const entry = overrides.get(path);
|
|
1981
|
+
if (entry === undefined)
|
|
1982
|
+
continue;
|
|
1983
|
+
if (entry === null)
|
|
1984
|
+
known.delete(path);
|
|
1985
|
+
else
|
|
1986
|
+
known.set(path, declaredOf(entry));
|
|
1987
|
+
}
|
|
1988
|
+
return known;
|
|
1989
|
+
};
|
|
1990
|
+
}
|
|
1991
|
+
function validatorOf(record, confirmedLayer) {
|
|
1992
|
+
return async (resulting, touched) => {
|
|
1993
|
+
const composition = compositionFor(record);
|
|
1994
|
+
const local = await openLocal();
|
|
1995
|
+
const changed = [];
|
|
1996
|
+
const overrides = new Map();
|
|
1997
|
+
const pace = paced();
|
|
1998
|
+
for (const path of new Set([...confirmedLayer.held, ...touched])) {
|
|
1999
|
+
await pace(ROW_WEIGHT);
|
|
2000
|
+
const was = entryAt(confirmedLayer, path);
|
|
2001
|
+
const now = entryAt(resulting, path);
|
|
2002
|
+
if (!textDiffers(was, now))
|
|
2003
|
+
continue;
|
|
2004
|
+
overrides.set(path, now);
|
|
2005
|
+
if (textOf(was) !== textOf(now))
|
|
2006
|
+
changed.push({ path, was: textOf(was), now: textOf(now) });
|
|
2007
|
+
}
|
|
2008
|
+
const lookup = overriddenLookup(lookupOf(local, record.key, 'confirmed'), overrides);
|
|
2009
|
+
const clean = available(composition) &&
|
|
2010
|
+
changed.length <= CLEAN_CHANGES &&
|
|
2011
|
+
(await changedCleanly(changed, composed(record), composition, {
|
|
2012
|
+
lookup,
|
|
2013
|
+
texts: textsOf(local, record.key, 'confirmed'),
|
|
2014
|
+
referrers: referrersOf(local, record.key, 'confirmed'),
|
|
2015
|
+
}));
|
|
2016
|
+
if (clean)
|
|
2017
|
+
return { verdict: 'accept' };
|
|
2018
|
+
return validatedCollection(overriddenWalk(walkOf(local, record.key, 'confirmed'), overrides), lookup, composition);
|
|
2019
|
+
};
|
|
2020
|
+
}
|
|
2021
|
+
function textsOf(local, key, view) {
|
|
2022
|
+
return async (paths) => new Map((await local.entries(key, view, paths)).map(({ path, content, binary }) => [
|
|
2023
|
+
path,
|
|
2024
|
+
binary !== null ? null : content,
|
|
2025
|
+
]));
|
|
2026
|
+
}
|
|
2027
|
+
function referrersOf(local, key, view) {
|
|
2028
|
+
return async (targets) => [
|
|
2029
|
+
...new Set((await local.referrers(key, view, targets, CHECKED_LINKS)).map(({ path }) => path)),
|
|
2030
|
+
];
|
|
2031
|
+
}
|
|
2032
|
+
async function sideOf(locator, replica, room, saving) {
|
|
2033
|
+
const record = replica.record;
|
|
2034
|
+
return {
|
|
2035
|
+
room: locator,
|
|
2036
|
+
handle: room.handle,
|
|
2037
|
+
baseRoomVersion: record.roomVersion,
|
|
2038
|
+
composition: compositionFor(record),
|
|
2039
|
+
operations: room.operations,
|
|
2040
|
+
providerOwnedPaths: room.providerOwnedPaths,
|
|
2041
|
+
visible: saving.visible,
|
|
2042
|
+
queued: queuedDocuments(replica.queue),
|
|
2043
|
+
unmodeled: await unmodeledIn(record),
|
|
2044
|
+
validate: validatorOf(record, saving.confirmed),
|
|
2045
|
+
};
|
|
2046
|
+
}
|
|
2047
|
+
function movedInto(locator, saved) {
|
|
2048
|
+
const into = saved.find(({ parts }) => parts.move?.room !== undefined && parts.move.room !== locator);
|
|
2049
|
+
return into?.parts.move?.room ?? null;
|
|
2050
|
+
}
|
|
2051
|
+
async function saveCommit(local, files, rooms) {
|
|
2052
|
+
const touched = new Map();
|
|
2053
|
+
const commits = [];
|
|
2054
|
+
for (const { locator, saving, before, queue } of rooms) {
|
|
2055
|
+
const replayedLayer = await replayed(saving.record, saving.confirmed, queue);
|
|
2056
|
+
const diff = await overlayDiff(saving.record, before, replayedLayer.visible, replayedLayer.touched);
|
|
2057
|
+
touched.set(locator, diff.touched);
|
|
2058
|
+
commits.push({
|
|
2059
|
+
locator,
|
|
2060
|
+
key: saving.record.key,
|
|
2061
|
+
roots: saving.record.typedSelection?.roots ?? [],
|
|
2062
|
+
overlay: diff.overlay,
|
|
2063
|
+
});
|
|
2064
|
+
}
|
|
2065
|
+
try {
|
|
2066
|
+
const answer = await local.commit({
|
|
2067
|
+
rooms: commits,
|
|
2068
|
+
writes: rooms.map(({ write }) => write),
|
|
2069
|
+
files,
|
|
2070
|
+
whole: true,
|
|
2071
|
+
});
|
|
2072
|
+
return { answer, touched };
|
|
2073
|
+
}
|
|
2074
|
+
catch {
|
|
2075
|
+
return null;
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
function savedReplica(locator, before, queue, touched, answer) {
|
|
2079
|
+
replicas.set(locator, {
|
|
2080
|
+
...before,
|
|
2081
|
+
queue,
|
|
2082
|
+
overlaid: touched,
|
|
2083
|
+
count: answer.counts[locator] ?? before.count,
|
|
2084
|
+
});
|
|
2085
|
+
}
|
|
2086
|
+
async function committedMove(locator, destination, { local, replica, room }, saved, consumed, writtenBy, files) {
|
|
2087
|
+
const mixed = saved.find(({ parts }) => Object.keys(parts).length !== 1 || parts.move?.room !== destination);
|
|
2088
|
+
if (mixed !== undefined) {
|
|
2089
|
+
return { outcome: 'refused', code: 'mixed-room-move', retryable: false, file: mixed.path };
|
|
2090
|
+
}
|
|
2091
|
+
const moves = saved.map(({ path, parts }) => ({ path, to: moveTarget(parts.move) }));
|
|
2092
|
+
const sourceSaving = await savingIn(replica, moves.map(({ path }) => ({ kind: 'remove', path })), saved.map(({ path }) => path), moves.map(({ path }) => path));
|
|
2093
|
+
const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, sourceSaving.visible)));
|
|
1469
2094
|
if (changed.length > 0)
|
|
1470
2095
|
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
1471
|
-
const
|
|
1472
|
-
const
|
|
1473
|
-
|
|
2096
|
+
const target = replicas.get(destination);
|
|
2097
|
+
const listedRoom = rooms.get(destination);
|
|
2098
|
+
if (target === undefined || listedRoom === undefined) {
|
|
2099
|
+
throw runtimeError('room-not-held', `No Local Replica for ${destination}`);
|
|
2100
|
+
}
|
|
2101
|
+
const arriving = await savingIn(target, [], moves.map(({ to }) => to));
|
|
2102
|
+
const built = await buildMove({
|
|
2103
|
+
source: await sideOf(locator, replica, room, sourceSaving),
|
|
2104
|
+
destination: await sideOf(destination, target, listedRoom, arriving),
|
|
2105
|
+
moves,
|
|
2106
|
+
providerLimits: limits,
|
|
2107
|
+
local: localSaves,
|
|
2108
|
+
...(writtenBy === undefined ? {} : { writtenBy }),
|
|
2109
|
+
});
|
|
2110
|
+
if (built.kind === 'refused')
|
|
2111
|
+
return { outcome: 'refused', ...built.refusal };
|
|
2112
|
+
const id = built.leaving.id;
|
|
2113
|
+
const carrying = saved.flatMap(({ path, parts }) => {
|
|
2114
|
+
const record = roomDrafts.get(locator)?.get(path);
|
|
2115
|
+
const to = moveTarget(parts.move);
|
|
2116
|
+
if (record === undefined || consumed.has(path) || roomDrafts.get(destination)?.has(to)) {
|
|
2117
|
+
return [];
|
|
2118
|
+
}
|
|
2119
|
+
const moved = {
|
|
2120
|
+
...record,
|
|
2121
|
+
locator: destination,
|
|
2122
|
+
path: to,
|
|
2123
|
+
revision: roomDraftRevision(),
|
|
2124
|
+
};
|
|
2125
|
+
return [{ from: path, record: moved }];
|
|
2126
|
+
});
|
|
2127
|
+
const carried = carrying.map(({ record }) => record);
|
|
2128
|
+
const marked = carried.map(({ path }) => ({ path, parts: {} }));
|
|
2129
|
+
const left = [...consumed, ...carrying.map(({ from }) => from)];
|
|
2130
|
+
const leavingQueue = [...replica.queue, built.leaving];
|
|
2131
|
+
const arrivingQueue = [...target.queue, built.arriving];
|
|
2132
|
+
const committedBoth = await saveCommit(local, files, [
|
|
2133
|
+
{
|
|
2134
|
+
locator,
|
|
2135
|
+
saving: sourceSaving,
|
|
2136
|
+
before: replica,
|
|
2137
|
+
queue: leavingQueue,
|
|
2138
|
+
write: {
|
|
2139
|
+
locator,
|
|
2140
|
+
queue: leavingQueue,
|
|
2141
|
+
saved: new Map([[id, saved]]),
|
|
2142
|
+
consumedRoomDrafts: left,
|
|
2143
|
+
},
|
|
2144
|
+
},
|
|
2145
|
+
{
|
|
2146
|
+
locator: destination,
|
|
2147
|
+
saving: arriving,
|
|
2148
|
+
before: target,
|
|
2149
|
+
queue: arrivingQueue,
|
|
2150
|
+
write: {
|
|
2151
|
+
locator: destination,
|
|
2152
|
+
queue: arrivingQueue,
|
|
2153
|
+
saved: new Map([[id, marked]]),
|
|
2154
|
+
roomDrafts: carried,
|
|
2155
|
+
},
|
|
2156
|
+
},
|
|
2157
|
+
]);
|
|
2158
|
+
if (committedBoth === null)
|
|
2159
|
+
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
2160
|
+
const { answer, touched } = committedBoth;
|
|
2161
|
+
if (answer.fenced !== null) {
|
|
2162
|
+
throw runtimeError('room-not-held', `No Local Replica for ${answer.fenced}`);
|
|
2163
|
+
}
|
|
2164
|
+
if (fences.has(locator) || fences.has(destination)) {
|
|
2165
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
2166
|
+
}
|
|
2167
|
+
pendings.set(pendingKey(locator, id), {
|
|
2168
|
+
drafts: saved,
|
|
2169
|
+
submission: { state: 'waiting' },
|
|
2170
|
+
lost: false,
|
|
2171
|
+
});
|
|
2172
|
+
pendings.set(pendingKey(destination, id), {
|
|
2173
|
+
drafts: marked,
|
|
2174
|
+
submission: { state: 'waiting' },
|
|
2175
|
+
lost: false,
|
|
2176
|
+
});
|
|
2177
|
+
for (const path of left)
|
|
2178
|
+
forgetRoomDraft(locator, path);
|
|
2179
|
+
for (const record of carried)
|
|
2180
|
+
rememberRoomDraft(record);
|
|
2181
|
+
savedReplica(locator, replica, leavingQueue, touched.get(locator), answer);
|
|
2182
|
+
savedReplica(destination, target, arrivingQueue, touched.get(destination), answer);
|
|
2183
|
+
publishChangeSet(mergeChangeSets([
|
|
2184
|
+
changeSet(locator, {
|
|
2185
|
+
documents: answer.changed[locator] ?? [],
|
|
2186
|
+
queue: true,
|
|
2187
|
+
drafts: left,
|
|
2188
|
+
}),
|
|
2189
|
+
changeSet(destination, {
|
|
2190
|
+
documents: answer.changed[destination] ?? [],
|
|
2191
|
+
queue: true,
|
|
2192
|
+
drafts: carried.map(({ path }) => path),
|
|
2193
|
+
}),
|
|
2194
|
+
]));
|
|
2195
|
+
queuedSave(locator);
|
|
2196
|
+
return { outcome: 'saved', identifier: id };
|
|
2197
|
+
}
|
|
2198
|
+
function queuedSave(locator) {
|
|
2199
|
+
if (!localSaves)
|
|
2200
|
+
recorded = false;
|
|
2201
|
+
moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
|
|
2202
|
+
submitLater(locator);
|
|
2203
|
+
}
|
|
2204
|
+
async function committed(locator, target, saved, consumed, writtenBy, files = []) {
|
|
2205
|
+
if (full)
|
|
2206
|
+
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
2207
|
+
const into = movedInto(locator, saved);
|
|
2208
|
+
if (into !== null) {
|
|
2209
|
+
return committedMove(locator, into, target, saved, consumed, writtenBy, files);
|
|
2210
|
+
}
|
|
2211
|
+
const { local, replica, room } = target;
|
|
2212
|
+
const parts = saved.map(({ path, parts: held }) => changesOf(path, held));
|
|
2213
|
+
const changes = parts.flatMap(({ changes: made }) => made);
|
|
2214
|
+
const saving = await savingIn(replica, changes, [
|
|
2215
|
+
...saved.map(({ path }) => path),
|
|
2216
|
+
...parts.flatMap(({ bases }) => bases.map(({ path }) => path)),
|
|
2217
|
+
]);
|
|
2218
|
+
const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, underneath(held, path, saving.visible)));
|
|
2219
|
+
if (changed.length > 0)
|
|
2220
|
+
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
2221
|
+
const requeue = await stepped(requeuing(replica.queue, changes, saving.visible, withdrawable(locator), saving.visibleOver));
|
|
1474
2222
|
if (requeue.kind === 'refused')
|
|
1475
2223
|
return { outcome: 'refused', ...requeue.refusal };
|
|
1476
2224
|
const foreign = [...requeue.withdrawn.keys()].some((origin) => {
|
|
@@ -1488,22 +2236,19 @@ export function composeRuntime(options) {
|
|
|
1488
2236
|
const withdrawing = requeue.withdrawn.size > 0;
|
|
1489
2237
|
const input = {
|
|
1490
2238
|
local: localSaves,
|
|
1491
|
-
baseRoomVersion: replica.
|
|
1492
|
-
composition: compositionFor(replica.
|
|
2239
|
+
baseRoomVersion: replica.record.roomVersion,
|
|
2240
|
+
composition: compositionFor(replica.record),
|
|
1493
2241
|
operations: room.operations,
|
|
1494
2242
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
1495
2243
|
providerLimits: limits,
|
|
1496
2244
|
handle: room.handle,
|
|
1497
|
-
visible:
|
|
2245
|
+
visible: saving.visible,
|
|
1498
2246
|
changes,
|
|
1499
2247
|
bases: parts.flatMap(({ bases }) => bases),
|
|
1500
2248
|
queued: queuedDocuments(replica.queue),
|
|
1501
2249
|
writtenBy,
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
binaries: replica.confirmed.record.binaries,
|
|
1505
|
-
composition: compositionOf(replica.confirmed.record),
|
|
1506
|
-
},
|
|
2250
|
+
unmodeled: await unmodeledIn(replica.record),
|
|
2251
|
+
validate: validatorOf(replica.record, saving.confirmed),
|
|
1507
2252
|
};
|
|
1508
2253
|
const checked = withdrawing
|
|
1509
2254
|
? await buildBatch({ ...input, queued: new Set([...input.queued, ...requeue.queued]) })
|
|
@@ -1528,8 +2273,7 @@ export function composeRuntime(options) {
|
|
|
1528
2273
|
? null
|
|
1529
2274
|
: await buildBatch({
|
|
1530
2275
|
...input,
|
|
1531
|
-
visible:
|
|
1532
|
-
.visible,
|
|
2276
|
+
visible: await stepped(saving.visibleOver(requeue.queue)),
|
|
1533
2277
|
changes: requeue.changes,
|
|
1534
2278
|
queued: queuedDocuments(requeue.queue),
|
|
1535
2279
|
});
|
|
@@ -1548,7 +2292,7 @@ export function composeRuntime(options) {
|
|
|
1548
2292
|
for (const [origin, replaced] of withdrawn) {
|
|
1549
2293
|
if (replaced === null)
|
|
1550
2294
|
continue;
|
|
1551
|
-
const carried = withdrawnDrafts(pendings.get(origin)?.drafts ?? [], changes);
|
|
2295
|
+
const carried = withdrawnDrafts(pendings.get(pendingKey(locator, origin))?.drafts ?? [], changes);
|
|
1552
2296
|
drafted.set(replaced.id, replaced === join?.batch
|
|
1553
2297
|
? [
|
|
1554
2298
|
...carried.map((held) => joinedDraft(held, saved)),
|
|
@@ -1562,66 +2306,77 @@ export function composeRuntime(options) {
|
|
|
1562
2306
|
for (const replaced of withdrawn.values()) {
|
|
1563
2307
|
if (replaced === null || !batches.includes(replaced))
|
|
1564
2308
|
continue;
|
|
1565
|
-
const limited =
|
|
2309
|
+
const limited = await stepped(limiting(batches, replaced, limits, saving.visibleOver));
|
|
1566
2310
|
if (limited !== null)
|
|
1567
2311
|
return { outcome: 'refused', ...limited };
|
|
1568
2312
|
}
|
|
1569
2313
|
const carried = carriedDrafts(locator, saved, consumed);
|
|
1570
2314
|
const left = [...consumed, ...carried.keys()];
|
|
1571
2315
|
const arrived = [...carried.values()];
|
|
1572
|
-
|
|
1573
|
-
|
|
2316
|
+
const done = await saveCommit(local, files, [
|
|
2317
|
+
{
|
|
1574
2318
|
locator,
|
|
2319
|
+
saving,
|
|
2320
|
+
before: replica,
|
|
1575
2321
|
queue: batches,
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
2322
|
+
write: {
|
|
2323
|
+
locator,
|
|
2324
|
+
queue: batches,
|
|
2325
|
+
saved: drafted,
|
|
2326
|
+
consumedRoomDrafts: left,
|
|
2327
|
+
roomDrafts: arrived,
|
|
2328
|
+
},
|
|
2329
|
+
},
|
|
2330
|
+
]);
|
|
2331
|
+
if (done === null)
|
|
1582
2332
|
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
2333
|
+
const { answer, touched } = done;
|
|
2334
|
+
if (answer.fenced !== null) {
|
|
2335
|
+
throw runtimeError('room-not-held', `No Local Replica for ${answer.fenced}`);
|
|
1583
2336
|
}
|
|
1584
|
-
const next = await stepped(replicating(replica.confirmed, batches, replica));
|
|
1585
|
-
const set = await stepped(changing(locator, replica, next, {
|
|
1586
|
-
queue: true,
|
|
1587
|
-
drafts: [...left, ...arrived.map(({ path }) => path)],
|
|
1588
|
-
}));
|
|
1589
2337
|
if (fences.has(locator))
|
|
1590
2338
|
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
1591
2339
|
for (const origin of withdrawn.keys())
|
|
1592
|
-
pendings.delete(origin);
|
|
2340
|
+
pendings.delete(pendingKey(locator, origin));
|
|
1593
2341
|
for (const [id, drafts] of drafted) {
|
|
1594
|
-
pendings.set(
|
|
2342
|
+
pendings.set(pendingKey(locator, id), {
|
|
2343
|
+
drafts,
|
|
2344
|
+
submission: { state: 'waiting' },
|
|
2345
|
+
lost: false,
|
|
2346
|
+
});
|
|
1595
2347
|
}
|
|
1596
|
-
if (!localSaves)
|
|
1597
|
-
recorded = false;
|
|
1598
2348
|
for (const path of left)
|
|
1599
2349
|
forgetRoomDraft(locator, path);
|
|
1600
2350
|
for (const record of arrived)
|
|
1601
2351
|
rememberRoomDraft(record);
|
|
1602
|
-
|
|
1603
|
-
publishChangeSet(
|
|
1604
|
-
|
|
1605
|
-
|
|
2352
|
+
savedReplica(locator, replica, batches, touched.get(locator), answer);
|
|
2353
|
+
publishChangeSet(changeSet(locator, {
|
|
2354
|
+
documents: answer.changed[locator] ?? [],
|
|
2355
|
+
queue: true,
|
|
2356
|
+
drafts: [...left, ...arrived.map(({ path }) => path)],
|
|
2357
|
+
}));
|
|
2358
|
+
queuedSave(locator);
|
|
1606
2359
|
const identifier = join?.batch.id ??
|
|
1607
2360
|
built?.batch.id ??
|
|
1608
2361
|
[...requeue.withdrawn].map(([origin, replaced]) => replaced?.id ?? origin).at(-1);
|
|
1609
2362
|
return { outcome: 'saved', identifier };
|
|
1610
2363
|
}
|
|
1611
|
-
async function mark(local, locator,
|
|
1612
|
-
const
|
|
1613
|
-
|
|
1614
|
-
state
|
|
1615
|
-
(
|
|
1616
|
-
|
|
2364
|
+
async function mark(local, locator, batch, submission, lost) {
|
|
2365
|
+
const partner = partnerOf(batch, locator);
|
|
2366
|
+
for (const held of partner === null ? [locator] : [locator, partner]) {
|
|
2367
|
+
const state = pendings.get(pendingKey(held, batch.id));
|
|
2368
|
+
if (!continuing() ||
|
|
2369
|
+
state === undefined ||
|
|
2370
|
+
(JSON.stringify(state.submission) === JSON.stringify(submission) && state.lost === lost)) {
|
|
2371
|
+
continue;
|
|
2372
|
+
}
|
|
2373
|
+
await local.submit(held, batch.id, submission, lost);
|
|
2374
|
+
if (fences.has(held))
|
|
2375
|
+
continue;
|
|
2376
|
+
pendings.set(pendingKey(held, batch.id), { ...state, submission, lost });
|
|
2377
|
+
if (replicas.has(held))
|
|
2378
|
+
publishChangeSet(changeSet(held, { queue: true }));
|
|
1617
2379
|
}
|
|
1618
|
-
await local.submit(locator, identifier, submission, lost);
|
|
1619
|
-
if (fences.has(locator))
|
|
1620
|
-
return;
|
|
1621
|
-
pendings.set(identifier, { ...state, submission, lost });
|
|
1622
|
-
const replica = replicas.get(locator);
|
|
1623
|
-
if (replica !== undefined)
|
|
1624
|
-
publishChangeSet(changeSet(locator, replica, replica, { queue: true }));
|
|
1625
2380
|
moveTo({ ...status, ...attention() });
|
|
1626
2381
|
}
|
|
1627
2382
|
async function keepUpload(local, locator, identifier, upload) {
|
|
@@ -1630,7 +2385,8 @@ export function composeRuntime(options) {
|
|
|
1630
2385
|
await local.keepUpload(locator, identifier, upload);
|
|
1631
2386
|
if (fences.has(locator))
|
|
1632
2387
|
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
1633
|
-
|
|
2388
|
+
const key = pendingKey(locator, identifier);
|
|
2389
|
+
pendings.set(key, { ...pendings.get(key), upload });
|
|
1634
2390
|
if (!continuing())
|
|
1635
2391
|
throw new Error('connection refused');
|
|
1636
2392
|
}
|
|
@@ -1663,7 +2419,7 @@ export function composeRuntime(options) {
|
|
|
1663
2419
|
if (fencedSince(locator, since))
|
|
1664
2420
|
return failure({ kind: 'room-not-readable', locator });
|
|
1665
2421
|
if (result.kind === 'scoped-refusal') {
|
|
1666
|
-
const room = replicas.get(result.locator)?.
|
|
2422
|
+
const room = replicas.get(result.locator)?.record;
|
|
1667
2423
|
if (room !== undefined)
|
|
1668
2424
|
await scoped(result, room, await openLocal());
|
|
1669
2425
|
}
|
|
@@ -1676,19 +2432,55 @@ export function composeRuntime(options) {
|
|
|
1676
2432
|
return UNCERTAIN;
|
|
1677
2433
|
}
|
|
1678
2434
|
}
|
|
1679
|
-
function
|
|
2435
|
+
function located(room, path, destination) {
|
|
2436
|
+
const crossing = roomTarget(destination);
|
|
2437
|
+
if (crossing === null) {
|
|
2438
|
+
const named = target(path, destination);
|
|
2439
|
+
return named === null || !rooms.has(room) ? null : { room, path: named };
|
|
2440
|
+
}
|
|
2441
|
+
const held = [...rooms.values()].find((listed) => listed.handle === crossing.handle);
|
|
2442
|
+
return held === undefined ? null : { room: held.locator, path: crossing.path };
|
|
2443
|
+
}
|
|
2444
|
+
async function resolveReference(room, path, destination) {
|
|
2445
|
+
if (roomTarget(destination) === null && target(path, destination) === null) {
|
|
2446
|
+
return ABSENT_TARGET;
|
|
2447
|
+
}
|
|
2448
|
+
const at = located(room, path, destination);
|
|
2449
|
+
if (at === null)
|
|
2450
|
+
return UNREADABLE;
|
|
2451
|
+
if (locked(at.room))
|
|
2452
|
+
return LOCKED_UNREADABLE;
|
|
2453
|
+
const held = replicas.get(at.room);
|
|
2454
|
+
if (held !== undefined &&
|
|
2455
|
+
(await (await openLocal()).entries(held.record.key, 'visible', [at.path])).length > 0) {
|
|
2456
|
+
return { kind: 'document', ...at };
|
|
2457
|
+
}
|
|
2458
|
+
const history = await readHistory(at.room, at.path);
|
|
2459
|
+
if (history.kind === 'no-history' || history.kind === 'revision')
|
|
2460
|
+
return ABSENT_TARGET;
|
|
2461
|
+
if (history.kind !== 'history')
|
|
2462
|
+
return history;
|
|
2463
|
+
const [last] = history.revisions;
|
|
2464
|
+
if (last === undefined)
|
|
2465
|
+
return ABSENT_TARGET;
|
|
2466
|
+
return history.deleted === true
|
|
2467
|
+
? { kind: 'deleted', ...at, revision: last }
|
|
2468
|
+
: { kind: 'document', ...at };
|
|
2469
|
+
}
|
|
2470
|
+
function queuedFacts(locator, path, localId) {
|
|
1680
2471
|
for (const batch of replicas.get(locator)?.queue ?? []) {
|
|
1681
2472
|
const change = batch.changes[queuedFile(batch, path, localId)];
|
|
1682
2473
|
if (change?.kind === 'binary')
|
|
1683
|
-
return change
|
|
2474
|
+
return change;
|
|
1684
2475
|
}
|
|
1685
|
-
|
|
2476
|
+
const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
|
|
2477
|
+
return drafted?.localId === localId ? drafted : null;
|
|
1686
2478
|
}
|
|
1687
|
-
function
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
2479
|
+
function batchFile(batch) {
|
|
2480
|
+
return async (localId) => {
|
|
2481
|
+
const change = batch.changes.find((held) => held.kind === 'binary' && held.localId === localId);
|
|
2482
|
+
return (await openLocal()).file(localId, change?.kind === 'binary' ? change.mediaType : '');
|
|
2483
|
+
};
|
|
1692
2484
|
}
|
|
1693
2485
|
async function transferBinary(request, transfer, answer) {
|
|
1694
2486
|
if (closed || refusal !== null)
|
|
@@ -1702,18 +2494,15 @@ export function composeRuntime(options) {
|
|
|
1702
2494
|
if (closed || authorization === undefined || refusal !== null)
|
|
1703
2495
|
return UNCERTAIN;
|
|
1704
2496
|
const since = fencings;
|
|
1705
|
-
journal.record(request.
|
|
1706
|
-
? `transfer binary ${request.locator} ${request.path}`
|
|
1707
|
-
: `transfer binary ${request.fileRef}`);
|
|
2497
|
+
journal.record(`transfer binary ${request.locator} ${request.path}`);
|
|
1708
2498
|
try {
|
|
1709
2499
|
const result = await transfer(authorization);
|
|
1710
2500
|
const settled = result;
|
|
1711
2501
|
journal.record(`outcome binary ${settled.kind}`);
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
return failure({ kind: 'room-not-readable', locator });
|
|
2502
|
+
if (fencedSince(request.locator, since))
|
|
2503
|
+
return failure({ kind: 'room-not-readable', locator: request.locator });
|
|
1715
2504
|
if (settled.kind === 'scoped-refusal') {
|
|
1716
|
-
const room = replicas.get(settled.locator)?.
|
|
2505
|
+
const room = replicas.get(settled.locator)?.record;
|
|
1717
2506
|
if (room !== undefined)
|
|
1718
2507
|
await scoped(settled, room, await openLocal());
|
|
1719
2508
|
}
|
|
@@ -1726,68 +2515,130 @@ export function composeRuntime(options) {
|
|
|
1726
2515
|
return UNCERTAIN;
|
|
1727
2516
|
}
|
|
1728
2517
|
}
|
|
1729
|
-
function
|
|
1730
|
-
return transferBinary(request, (
|
|
2518
|
+
function fetchBinary(request, name) {
|
|
2519
|
+
return transferBinary(request, (authorization) => keptFiles.fetch(name, () => options.adapter.binary(authorization, request)), binaryAnswer);
|
|
2520
|
+
}
|
|
2521
|
+
async function readBinary(request, name) {
|
|
2522
|
+
if (closed)
|
|
2523
|
+
return UNCERTAIN;
|
|
2524
|
+
const held = await keptFiles.find(name);
|
|
2525
|
+
if (held !== null)
|
|
2526
|
+
return held;
|
|
2527
|
+
return fetchBinary(request, name);
|
|
1731
2528
|
}
|
|
1732
|
-
function binaryPath(locator, path, revision) {
|
|
2529
|
+
async function binaryPath(locator, path, revision) {
|
|
1733
2530
|
if (revision !== undefined)
|
|
1734
2531
|
return { kind: 'path', locator, path, revision };
|
|
1735
|
-
const
|
|
1736
|
-
|
|
1737
|
-
if (visible === undefined || held === undefined)
|
|
2532
|
+
const replica = replicas.get(locator);
|
|
2533
|
+
if (replica === undefined)
|
|
1738
2534
|
return null;
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
2535
|
+
const local = await openLocal();
|
|
2536
|
+
const [held] = await local.entries(replica.record.key, 'visible', [path]);
|
|
2537
|
+
if (held?.binary === undefined || held.binary === null)
|
|
2538
|
+
return null;
|
|
2539
|
+
if (held.binary.state === 'pending')
|
|
2540
|
+
return { kind: 'pending', localId: held.binary.localId };
|
|
2541
|
+
const record = binaryRecord(path, held.binary, held.revision ?? undefined);
|
|
1742
2542
|
if (record.state !== 'accepted')
|
|
1743
2543
|
return null;
|
|
1744
|
-
const
|
|
1745
|
-
const
|
|
2544
|
+
const queued = replica.queue.flatMap((batch) => batch.changes);
|
|
2545
|
+
const sources = queued.flatMap((change) => (change.kind === 'move' ? [change.path] : []));
|
|
2546
|
+
const confirmedLayer = await stepped(slicing(await local.entries(replica.record.key, 'confirmed', [path, ...sources]), [
|
|
2547
|
+
path,
|
|
2548
|
+
...sources,
|
|
2549
|
+
]));
|
|
1746
2550
|
const holds = (at) => {
|
|
1747
|
-
const kept =
|
|
2551
|
+
const kept = confirmedLayer.binaries[at];
|
|
1748
2552
|
return (kept?.state === 'accepted' &&
|
|
1749
|
-
kept.
|
|
1750
|
-
|
|
2553
|
+
kept.sha256 === record.sha256 &&
|
|
2554
|
+
confirmedLayer.revisions[at] === record.revision);
|
|
1751
2555
|
};
|
|
1752
|
-
let
|
|
1753
|
-
for (let index = queued.length - 1; index >= 0 && !holds(
|
|
2556
|
+
let confirmedAt = path;
|
|
2557
|
+
for (let index = queued.length - 1; index >= 0 && !holds(confirmedAt); index -= 1) {
|
|
1754
2558
|
const change = queued[index];
|
|
1755
|
-
if (change.kind === 'move' && change.to ===
|
|
1756
|
-
|
|
2559
|
+
if (change.kind === 'move' && change.to === confirmedAt)
|
|
2560
|
+
confirmedAt = change.path;
|
|
1757
2561
|
}
|
|
1758
2562
|
return {
|
|
1759
2563
|
kind: 'path',
|
|
1760
2564
|
locator,
|
|
1761
|
-
path:
|
|
2565
|
+
path: confirmedAt,
|
|
1762
2566
|
revision: record.revision,
|
|
1763
|
-
fileRef: record.fileRef,
|
|
1764
2567
|
};
|
|
1765
2568
|
}
|
|
1766
|
-
async function
|
|
2569
|
+
async function askedBinary(locator, path, revision) {
|
|
1767
2570
|
await ready;
|
|
1768
|
-
const
|
|
2571
|
+
const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
|
|
2572
|
+
return drafted !== undefined && drafted.localId === revision
|
|
2573
|
+
? { kind: 'pending', localId: revision }
|
|
2574
|
+
: binaryPath(locator, path, revision);
|
|
2575
|
+
}
|
|
2576
|
+
function pendingBinary(locator, path, localId) {
|
|
2577
|
+
const facts = queuedFacts(locator, path, localId);
|
|
2578
|
+
return facts === null
|
|
2579
|
+
? absentBinary(path)
|
|
2580
|
+
: {
|
|
2581
|
+
kind: 'binary',
|
|
2582
|
+
file: localId,
|
|
2583
|
+
byteLength: facts.byteLength,
|
|
2584
|
+
mediaType: facts.mediaType,
|
|
2585
|
+
};
|
|
2586
|
+
}
|
|
2587
|
+
async function readBinaryPath(locator, path, revision) {
|
|
2588
|
+
const asked = await askedBinary(locator, path, revision);
|
|
1769
2589
|
if (asked === null)
|
|
1770
2590
|
return absentBinary(path);
|
|
1771
|
-
if (asked.kind === 'pending')
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
2591
|
+
if (asked.kind === 'pending')
|
|
2592
|
+
return pendingBinary(locator, path, asked.localId);
|
|
2593
|
+
return readBinary(asked, { locator, path: asked.path, revision: asked.revision });
|
|
2594
|
+
}
|
|
2595
|
+
async function heldMediaType(locator, path) {
|
|
2596
|
+
const replica = replicas.get(locator);
|
|
2597
|
+
if (replica === undefined)
|
|
2598
|
+
return undefined;
|
|
2599
|
+
const [held] = await (await openLocal()).entries(replica.record.key, 'visible', [path]);
|
|
2600
|
+
return held?.binary?.mediaType;
|
|
2601
|
+
}
|
|
2602
|
+
function linkOf(asked, held) {
|
|
2603
|
+
return transferBinary(asked, (authorization) => options.adapter.link(authorization, asked), (result) => binaryLinkAnswer(result, held));
|
|
1776
2604
|
}
|
|
1777
2605
|
async function linkBinaryPath(locator, path, revision) {
|
|
1778
2606
|
await ready;
|
|
1779
|
-
const asked = binaryPath(locator, path, revision);
|
|
2607
|
+
const asked = await binaryPath(locator, path, revision);
|
|
1780
2608
|
if (asked === null || asked.kind === 'pending')
|
|
1781
2609
|
return absentBinary(path);
|
|
1782
|
-
return
|
|
2610
|
+
return linkOf(asked, revision === undefined ? await heldMediaType(locator, path) : undefined);
|
|
1783
2611
|
}
|
|
1784
|
-
async function
|
|
2612
|
+
async function showBinaryPath(locator, path, revision) {
|
|
2613
|
+
const asked = await askedBinary(locator, path, revision);
|
|
2614
|
+
if (asked === null)
|
|
2615
|
+
return absentBinary(path);
|
|
2616
|
+
if (asked.kind === 'pending')
|
|
2617
|
+
return pendingBinary(locator, path, asked.localId);
|
|
2618
|
+
if (closed)
|
|
2619
|
+
return UNCERTAIN;
|
|
2620
|
+
const name = { locator, path: asked.path, revision: asked.revision };
|
|
2621
|
+
const kept = await keptFiles.find(name);
|
|
2622
|
+
if (kept !== null)
|
|
2623
|
+
return kept;
|
|
2624
|
+
const mediaType = await heldMediaType(locator, path);
|
|
2625
|
+
if (mediaType?.startsWith('image/') === true)
|
|
2626
|
+
return fetchBinary(asked, name);
|
|
2627
|
+
return linkOf(asked, revision === undefined ? mediaType : undefined);
|
|
2628
|
+
}
|
|
2629
|
+
async function attachBinary(locator, file) {
|
|
1785
2630
|
await ready;
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
2631
|
+
if (closed || connectionOf() === 'unreachable' || refusal !== null)
|
|
2632
|
+
return OFFLINE;
|
|
2633
|
+
const request = await attachedFile(locator, file);
|
|
2634
|
+
return transferBinary({ locator, path: file.name }, (held) => options.adapter.attach(held, request), attachAnswer);
|
|
2635
|
+
}
|
|
2636
|
+
function readPart(file, part) {
|
|
2637
|
+
return database.ask({ kind: 'file.part', key: file, part });
|
|
2638
|
+
}
|
|
2639
|
+
async function cleanUp() {
|
|
2640
|
+
const freed = await database.ask({ kind: 'opened.clean' });
|
|
2641
|
+
return freed + ((await options.shell.cleanUp?.()) ?? 0);
|
|
1791
2642
|
}
|
|
1792
2643
|
function declaredAgents() {
|
|
1793
2644
|
for (const capability of capabilities) {
|
|
@@ -1805,6 +2656,13 @@ export function composeRuntime(options) {
|
|
|
1805
2656
|
}
|
|
1806
2657
|
return undefined;
|
|
1807
2658
|
}
|
|
2659
|
+
function calledRooms(name, input) {
|
|
2660
|
+
const member = declaredAction(name)?.room;
|
|
2661
|
+
if (member === undefined || input === null || typeof input !== 'object')
|
|
2662
|
+
return [];
|
|
2663
|
+
const named = input[member];
|
|
2664
|
+
return (Array.isArray(named) ? named : [named]).filter((room) => typeof room === 'string');
|
|
2665
|
+
}
|
|
1808
2666
|
async function invokeAction(name, input, dispatching) {
|
|
1809
2667
|
await ready;
|
|
1810
2668
|
if (closed || refusal !== null)
|
|
@@ -1824,7 +2682,7 @@ export function composeRuntime(options) {
|
|
|
1824
2682
|
try {
|
|
1825
2683
|
const result = await options.adapter.action(authorization, { name, input });
|
|
1826
2684
|
if (result.kind === 'scoped-refusal') {
|
|
1827
|
-
const room = replicas.get(result.locator)?.
|
|
2685
|
+
const room = replicas.get(result.locator)?.record;
|
|
1828
2686
|
if (room !== undefined)
|
|
1829
2687
|
await scoped(result, room, await openLocal());
|
|
1830
2688
|
}
|
|
@@ -1892,100 +2750,442 @@ export function composeRuntime(options) {
|
|
|
1892
2750
|
calling.delete(held.operation);
|
|
1893
2751
|
}
|
|
1894
2752
|
}
|
|
1895
|
-
return 'done';
|
|
2753
|
+
return 'done';
|
|
2754
|
+
}
|
|
2755
|
+
function continuing() {
|
|
2756
|
+
return !closed && !discoveryPaused;
|
|
2757
|
+
}
|
|
2758
|
+
function reachedBy(locator, roomVersion) {
|
|
2759
|
+
const held = replicas.get(locator);
|
|
2760
|
+
return (held !== undefined && options.adapter.reached(locator, held.record.roomVersion, roomVersion));
|
|
2761
|
+
}
|
|
2762
|
+
async function writtenAfter(locator, roomVersion, path) {
|
|
2763
|
+
const held = replicas.get(locator);
|
|
2764
|
+
if (held === undefined)
|
|
2765
|
+
return false;
|
|
2766
|
+
const [entry] = await (await openLocal()).entries(held.record.key, 'confirmed', [path]);
|
|
2767
|
+
const written = entry?.written ?? undefined;
|
|
2768
|
+
return (written !== undefined &&
|
|
2769
|
+
options.adapter.reached(locator, written, roomVersion) &&
|
|
2770
|
+
!options.adapter.reached(locator, roomVersion, written));
|
|
2771
|
+
}
|
|
2772
|
+
function overriddenTexts(texts, overrides) {
|
|
2773
|
+
return async (paths) => {
|
|
2774
|
+
const known = new Map(await texts(paths.filter((path) => !overrides.has(path))));
|
|
2775
|
+
for (const path of paths) {
|
|
2776
|
+
const entry = overrides.get(path);
|
|
2777
|
+
if (entry === undefined)
|
|
2778
|
+
continue;
|
|
2779
|
+
if (entry === null)
|
|
2780
|
+
known.delete(path);
|
|
2781
|
+
else
|
|
2782
|
+
known.set(path, entry.binary !== null ? null : entry.content);
|
|
2783
|
+
}
|
|
2784
|
+
return known;
|
|
2785
|
+
};
|
|
2786
|
+
}
|
|
2787
|
+
async function unreadableRoom(texts, walk, paths) {
|
|
2788
|
+
if (paths !== null) {
|
|
2789
|
+
const text = {};
|
|
2790
|
+
for (const [path, content] of await texts(paths))
|
|
2791
|
+
if (content !== null)
|
|
2792
|
+
text[path] = content;
|
|
2793
|
+
return stepped(unreadableIn(text, Object.keys(text)));
|
|
2794
|
+
}
|
|
2795
|
+
for (let after = null;;) {
|
|
2796
|
+
const page = await walk(after);
|
|
2797
|
+
const text = {};
|
|
2798
|
+
for (const { path, content } of page.entries)
|
|
2799
|
+
if (content !== null)
|
|
2800
|
+
text[path] = content;
|
|
2801
|
+
const refused = await stepped(unreadableIn(text, Object.keys(text)));
|
|
2802
|
+
if (refused !== null)
|
|
2803
|
+
return refused;
|
|
2804
|
+
if (page.next === null)
|
|
2805
|
+
return null;
|
|
2806
|
+
after = page.next;
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
async function validatedRoom(local, record, view, previous, overrides = null) {
|
|
2810
|
+
const composition = composed(record);
|
|
2811
|
+
const modeled = record.model.status !== 'unavailable' || record.typedSelection !== null;
|
|
2812
|
+
const lookup = overrides === null
|
|
2813
|
+
? lookupOf(local, record.key, view)
|
|
2814
|
+
: overriddenLookup(lookupOf(local, record.key, view), overrides);
|
|
2815
|
+
const texts = overrides === null
|
|
2816
|
+
? textsOf(local, record.key, view)
|
|
2817
|
+
: overriddenTexts(textsOf(local, record.key, view), overrides);
|
|
2818
|
+
const walk = overrides === null
|
|
2819
|
+
? walkOf(local, record.key, view)
|
|
2820
|
+
: overriddenWalk(walkOf(local, record.key, view), overrides);
|
|
2821
|
+
let changes = null;
|
|
2822
|
+
let reshaped = false;
|
|
2823
|
+
if (previous !== null && previous.changed.length <= CLEAN_CHANGES) {
|
|
2824
|
+
const [was, now] = await Promise.all([
|
|
2825
|
+
local.entries(record.key, 'confirmed', previous.changed),
|
|
2826
|
+
overrides === null
|
|
2827
|
+
? local.entries(record.key, view, previous.changed)
|
|
2828
|
+
: Promise.resolve([]),
|
|
2829
|
+
]);
|
|
2830
|
+
const before = new Map(was.map((entry) => [entry.path, entry]));
|
|
2831
|
+
const after = overrides === null
|
|
2832
|
+
? new Map(now.map((entry) => [entry.path, entry]))
|
|
2833
|
+
: overrides;
|
|
2834
|
+
changes = previous.changed.flatMap((path) => {
|
|
2835
|
+
const one = before.get(path) ?? null;
|
|
2836
|
+
const other = after.get(path) ?? null;
|
|
2837
|
+
reshaped ||= textDiffers(one, other);
|
|
2838
|
+
const held = textOf(one);
|
|
2839
|
+
const next = textOf(other);
|
|
2840
|
+
return held === next ? [] : [{ path, was: held, now: next }];
|
|
2841
|
+
});
|
|
2842
|
+
}
|
|
2843
|
+
const named = changes === null ? null : changes.map(({ path }) => path);
|
|
2844
|
+
const textChanged = changes === null || changes.length > 0 || reshaped;
|
|
2845
|
+
const modelsChanged = previous === null || !sameComposition(composed(previous.record), composition);
|
|
2846
|
+
if (modeled && (textChanged || modelsChanged)) {
|
|
2847
|
+
const clean = previous !== null &&
|
|
2848
|
+
changes !== null &&
|
|
2849
|
+
(await changedCleanly(changes, composed(previous.record), composition, {
|
|
2850
|
+
lookup,
|
|
2851
|
+
texts,
|
|
2852
|
+
referrers: referrersOf(local, record.key, 'confirmed'),
|
|
2853
|
+
}));
|
|
2854
|
+
if (clean)
|
|
2855
|
+
return null;
|
|
2856
|
+
const verdict = await validatedCollection(walk, lookup, composition);
|
|
2857
|
+
if (verdict.verdict === 'accept')
|
|
2858
|
+
return null;
|
|
2859
|
+
if (verdict.rule !== 'model-unavailable')
|
|
2860
|
+
return { kind: 'refused', refusal: verdict };
|
|
2861
|
+
return unreadableRoom(texts, walk, named);
|
|
2862
|
+
}
|
|
2863
|
+
if (textChanged)
|
|
2864
|
+
return unreadableRoom(texts, walk, named);
|
|
2865
|
+
return null;
|
|
1896
2866
|
}
|
|
1897
|
-
function
|
|
1898
|
-
|
|
2867
|
+
function refreshingOf(local, key, liveness, replaying = null) {
|
|
2868
|
+
const write = replaying === null ? exclusive : installWrite;
|
|
2869
|
+
return {
|
|
2870
|
+
async turn() {
|
|
2871
|
+
if (replaying === null)
|
|
2872
|
+
return;
|
|
2873
|
+
installing(replaying);
|
|
2874
|
+
await screened();
|
|
2875
|
+
},
|
|
2876
|
+
prior: async (paths) => new Map((await local.entries(key, 'refreshed', paths)).map((entry) => [entry.path, entry])),
|
|
2877
|
+
async stage(page) {
|
|
2878
|
+
for (let written = 0;;) {
|
|
2879
|
+
if (replaying !== null)
|
|
2880
|
+
await screened();
|
|
2881
|
+
const end = batchEnd(page.entries, written);
|
|
2882
|
+
const last = end === page.entries.length;
|
|
2883
|
+
const entries = page.entries.slice(written, end);
|
|
2884
|
+
const staged = await write(async () => {
|
|
2885
|
+
if (!liveness())
|
|
2886
|
+
return false;
|
|
2887
|
+
await timed(stageBudget, () => local.refresh(key, last
|
|
2888
|
+
? { ...page, entries }
|
|
2889
|
+
: { entries, removals: [], folders: [], folderRemovals: [] }));
|
|
2890
|
+
return true;
|
|
2891
|
+
});
|
|
2892
|
+
if (!staged)
|
|
2893
|
+
return false;
|
|
2894
|
+
if (last)
|
|
2895
|
+
return true;
|
|
2896
|
+
written = end;
|
|
2897
|
+
}
|
|
2898
|
+
},
|
|
2899
|
+
};
|
|
1899
2900
|
}
|
|
1900
|
-
function
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
2901
|
+
function batchEnd(entries, from) {
|
|
2902
|
+
let end = from;
|
|
2903
|
+
let spent = 0;
|
|
2904
|
+
while (end < entries.length) {
|
|
2905
|
+
const size = (entries[end].content?.length ?? 0) + ROW_WEIGHT;
|
|
2906
|
+
if (spent + size > stageBudget.bytes && end > from)
|
|
2907
|
+
break;
|
|
2908
|
+
spent += size;
|
|
2909
|
+
end += 1;
|
|
2910
|
+
}
|
|
2911
|
+
return end;
|
|
2912
|
+
}
|
|
2913
|
+
async function timed(budget, work) {
|
|
2914
|
+
const started = performance.now();
|
|
2915
|
+
const done = await work();
|
|
2916
|
+
const spent = performance.now() - started;
|
|
2917
|
+
if (spent < budget.aim / 2)
|
|
2918
|
+
budget.bytes = Math.min(budget.ceiling, Math.round(budget.bytes * 1.5));
|
|
2919
|
+
else if (spent > budget.aim * 2) {
|
|
2920
|
+
budget.bytes = Math.max(budget.floor, Math.round(budget.bytes / 1.5));
|
|
2921
|
+
}
|
|
2922
|
+
return done;
|
|
2923
|
+
}
|
|
2924
|
+
function installWrite(work) {
|
|
2925
|
+
const next = installWrites.then(() => exclusive(work), () => exclusive(work));
|
|
2926
|
+
installWrites = next.then(() => undefined, () => undefined);
|
|
2927
|
+
return next;
|
|
1904
2928
|
}
|
|
1905
|
-
function
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
2929
|
+
function screened() {
|
|
2930
|
+
if (onScreen || closed)
|
|
2931
|
+
return Promise.resolve();
|
|
2932
|
+
queueReadiness();
|
|
2933
|
+
return new Promise((resolve) => shownWaiters.push(resolve));
|
|
1910
2934
|
}
|
|
1911
|
-
async function
|
|
1912
|
-
|
|
2935
|
+
async function heldPart() {
|
|
2936
|
+
while (partsFree === 0 || queuedBytes >= QUEUED_BYTES) {
|
|
2937
|
+
await new Promise((admit) => partWaiters.push(admit));
|
|
2938
|
+
}
|
|
2939
|
+
partsFree -= 1;
|
|
2940
|
+
return once(() => {
|
|
2941
|
+
partsFree += 1;
|
|
2942
|
+
});
|
|
2943
|
+
}
|
|
2944
|
+
function once(release) {
|
|
2945
|
+
let released = false;
|
|
2946
|
+
return () => {
|
|
2947
|
+
if (released)
|
|
2948
|
+
return;
|
|
2949
|
+
released = true;
|
|
2950
|
+
release();
|
|
2951
|
+
for (const admit of partWaiters.splice(0))
|
|
2952
|
+
admit();
|
|
2953
|
+
};
|
|
2954
|
+
}
|
|
2955
|
+
function installing(locator, inventory = null) {
|
|
2956
|
+
installs.set(locator, inventory ?? installs.get(locator) ?? null);
|
|
2957
|
+
queueReadiness();
|
|
2958
|
+
}
|
|
2959
|
+
function untransferred(locator) {
|
|
2960
|
+
transfers.delete(locator);
|
|
2961
|
+
keptParts.delete(locator);
|
|
2962
|
+
underived.delete(locator);
|
|
2963
|
+
}
|
|
2964
|
+
function installed(locator) {
|
|
2965
|
+
if (installs.delete(locator))
|
|
2966
|
+
queueReadiness();
|
|
2967
|
+
}
|
|
2968
|
+
function screen(value) {
|
|
2969
|
+
if (value === onScreen)
|
|
2970
|
+
return;
|
|
2971
|
+
onScreen = value;
|
|
2972
|
+
journal.record(`installation ${value ? 'shown' : 'hidden'}`);
|
|
2973
|
+
queueReadiness();
|
|
2974
|
+
if (!value)
|
|
2975
|
+
return;
|
|
2976
|
+
for (const resolve of shownWaiters.splice(0))
|
|
2977
|
+
resolve();
|
|
2978
|
+
}
|
|
2979
|
+
async function pullRefresh(authorized, held, liveness) {
|
|
2980
|
+
if (!liveness())
|
|
1913
2981
|
return unconfirmed;
|
|
1914
2982
|
const turn = generation;
|
|
2983
|
+
const { record } = held;
|
|
2984
|
+
const base = held.confirmed;
|
|
2985
|
+
const local = await openLocal();
|
|
1915
2986
|
try {
|
|
1916
|
-
|
|
1917
|
-
const
|
|
1918
|
-
|
|
1919
|
-
|
|
2987
|
+
await exclusive(() => local.unrefresh(record.key));
|
|
2988
|
+
const result = await advancement(reads, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.key, liveness));
|
|
2989
|
+
if (result.kind !== 'advanced') {
|
|
2990
|
+
journal.record(`transfer refresh ${record.locator} ${result.kind}`);
|
|
2991
|
+
return result;
|
|
2992
|
+
}
|
|
2993
|
+
const refused = await validatedRoom(local, result.record, 'refreshed', {
|
|
2994
|
+
record,
|
|
2995
|
+
changed: [...result.changed],
|
|
2996
|
+
});
|
|
2997
|
+
if (refused !== null) {
|
|
2998
|
+
journal.record(`transfer refresh ${record.locator} refused`);
|
|
2999
|
+
return refused;
|
|
3000
|
+
}
|
|
3001
|
+
journal.record(`transfer refresh ${record.locator} ${result.record.roomVersion}`);
|
|
3002
|
+
return { kind: 'advanced', record: result.record, base };
|
|
1920
3003
|
}
|
|
1921
3004
|
catch (error) {
|
|
1922
|
-
|
|
3005
|
+
if (refusedForSpace(error))
|
|
3006
|
+
return unconfirmed;
|
|
3007
|
+
journal.record(`transfer refresh ${record.locator} transport-uncertainty`);
|
|
1923
3008
|
throw error;
|
|
1924
3009
|
}
|
|
1925
3010
|
finally {
|
|
1926
|
-
reads.finished(
|
|
3011
|
+
reads.finished(record.locator, turn);
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
3014
|
+
const heldContents = new Map();
|
|
3015
|
+
function heldContentOf(local, key) {
|
|
3016
|
+
const known = heldContents.get(key);
|
|
3017
|
+
if (known !== undefined)
|
|
3018
|
+
return known;
|
|
3019
|
+
if (heldContents.size > ROOM_READS)
|
|
3020
|
+
heldContents.clear();
|
|
3021
|
+
const made = {
|
|
3022
|
+
files: (after, limit) => local.files(key, 'confirmed', after, limit),
|
|
3023
|
+
async contents(paths) {
|
|
3024
|
+
return new Map((await local.entries(key, 'confirmed', paths)).flatMap(({ path, content, revision }) => content === null || revision === null ? [] : [[path, { content, revision }]]));
|
|
3025
|
+
},
|
|
3026
|
+
};
|
|
3027
|
+
heldContents.set(key, made);
|
|
3028
|
+
return made;
|
|
3029
|
+
}
|
|
3030
|
+
async function keptTogether({ local, parts }) {
|
|
3031
|
+
const live = parts.filter((one) => one.live() && transfers.has(one.locator));
|
|
3032
|
+
await local.keepParts(live.map(({ locator, token, part }) => ({ locator, token, part })));
|
|
3033
|
+
for (const { locator, token, part } of live) {
|
|
3034
|
+
const held = keptParts.get(locator) ?? new Map();
|
|
3035
|
+
keptParts.set(locator, held.set(token, part.byteLength));
|
|
3036
|
+
arrivals.get(locator)?.delete(token);
|
|
1927
3037
|
}
|
|
3038
|
+
queueReadiness();
|
|
1928
3039
|
}
|
|
1929
|
-
function staging(local,
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
if (!
|
|
3040
|
+
function staging(local, liveness) {
|
|
3041
|
+
const derived = async (locator, key) => {
|
|
3042
|
+
underived.delete(locator);
|
|
3043
|
+
for (;;) {
|
|
3044
|
+
await screened();
|
|
3045
|
+
if (!liveness())
|
|
1935
3046
|
return false;
|
|
1936
|
-
const
|
|
1937
|
-
if (
|
|
3047
|
+
const done = await installWrite(async () => liveness() ? timed(deriveBudget, () => local.rederive(key, deriveBudget.bytes)) : null);
|
|
3048
|
+
if (done === null)
|
|
1938
3049
|
return false;
|
|
1939
|
-
|
|
1940
|
-
|
|
3050
|
+
if (done)
|
|
3051
|
+
return true;
|
|
3052
|
+
}
|
|
3053
|
+
};
|
|
3054
|
+
return {
|
|
3055
|
+
held: (locator) => transfers.get(locator) ?? null,
|
|
3056
|
+
async turn(locator, step) {
|
|
3057
|
+
installing(locator);
|
|
3058
|
+
for (;;) {
|
|
3059
|
+
await screened();
|
|
3060
|
+
const release = step === 'downloading' ? await heldPart() : () => { };
|
|
3061
|
+
if (onScreen || closed)
|
|
3062
|
+
return release;
|
|
3063
|
+
release();
|
|
1941
3064
|
}
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
3065
|
+
},
|
|
3066
|
+
queued(bytes) {
|
|
3067
|
+
queuedBytes += bytes;
|
|
3068
|
+
return once(() => {
|
|
3069
|
+
queuedBytes -= bytes;
|
|
3070
|
+
});
|
|
3071
|
+
},
|
|
3072
|
+
arriving(locator) {
|
|
3073
|
+
let ended = false;
|
|
3074
|
+
return {
|
|
3075
|
+
at(token, bytes) {
|
|
3076
|
+
if (ended || keptParts.get(locator)?.has(token) === true)
|
|
3077
|
+
return;
|
|
3078
|
+
const held = arrivals.get(locator) ?? new Map();
|
|
3079
|
+
arrivals.set(locator, held.set(token, bytes));
|
|
3080
|
+
const now = performance.now();
|
|
3081
|
+
if (now - arrivalReported < ARRIVAL_REPORT_MS)
|
|
3082
|
+
return;
|
|
3083
|
+
arrivalReported = now;
|
|
3084
|
+
queueReadiness();
|
|
3085
|
+
},
|
|
3086
|
+
end() {
|
|
3087
|
+
ended = true;
|
|
3088
|
+
if (arrivals.delete(locator))
|
|
3089
|
+
queueReadiness();
|
|
3090
|
+
},
|
|
3091
|
+
};
|
|
3092
|
+
},
|
|
3093
|
+
received: (locator, token) => local.transferPart(locator, token),
|
|
3094
|
+
keep: (locator, token, received) => gathered('kept parts', { local, parts: [{ locator, token, part: received, live: liveness }] }, (earlier, later) => earlier.local === later.local
|
|
3095
|
+
? { local: earlier.local, parts: [...earlier.parts, ...later.parts] }
|
|
3096
|
+
: null, keptTogether),
|
|
3097
|
+
announced(locator) {
|
|
3098
|
+
const later = announced.get(locator);
|
|
3099
|
+
return later === undefined ? [] : [later];
|
|
3100
|
+
},
|
|
3101
|
+
async stage(record, page, composition, written) {
|
|
3102
|
+
const { locator } = record;
|
|
3103
|
+
installing(locator, record.inventory);
|
|
3104
|
+
await screened();
|
|
3105
|
+
if (!liveness())
|
|
3106
|
+
return null;
|
|
3107
|
+
const key = await installWrite(async () => {
|
|
3108
|
+
if (!liveness())
|
|
3109
|
+
return null;
|
|
3110
|
+
return local.stage(record, composition, page, written ?? undefined);
|
|
3111
|
+
});
|
|
3112
|
+
if (key === null || !liveness())
|
|
3113
|
+
return null;
|
|
3114
|
+
const arrived = !transfers.has(locator);
|
|
3115
|
+
const held = { ...record, key };
|
|
3116
|
+
transfers.set(locator, held);
|
|
3117
|
+
if (written !== null) {
|
|
3118
|
+
keptParts.get(locator)?.delete(written);
|
|
3119
|
+
arrivals.get(locator)?.delete(written);
|
|
1947
3120
|
}
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
3121
|
+
if (arrived)
|
|
3122
|
+
liveQueries.recover();
|
|
3123
|
+
installing(locator);
|
|
3124
|
+
liveQueries.recover();
|
|
3125
|
+
const owed = page.entries.reduce((sum, entry) => sum + (entry.content?.length ?? 0), underived.get(locator) ?? 0);
|
|
3126
|
+
underived.set(locator, owed);
|
|
3127
|
+
if (owed < deriveBudget.bytes)
|
|
3128
|
+
return held;
|
|
3129
|
+
return (await derived(locator, key)) ? held : null;
|
|
3130
|
+
},
|
|
3131
|
+
derive: derived,
|
|
3132
|
+
async refreshing(locator, key) {
|
|
3133
|
+
await exclusive(() => local.unrefresh(key));
|
|
3134
|
+
return refreshingOf(local, key, liveness, locator);
|
|
3135
|
+
},
|
|
1952
3136
|
abandon: (locator) => exclusive(async () => {
|
|
1953
|
-
if (!
|
|
3137
|
+
if (!liveness())
|
|
1954
3138
|
return;
|
|
1955
3139
|
await local.abandon(locator);
|
|
1956
|
-
|
|
1957
|
-
|
|
3140
|
+
untransferred(locator);
|
|
3141
|
+
liveQueries.recover();
|
|
3142
|
+
sweep();
|
|
3143
|
+
}),
|
|
3144
|
+
setAside: (locator) => exclusive(async () => {
|
|
3145
|
+
if (!liveness())
|
|
3146
|
+
return;
|
|
3147
|
+
await local.abandon(locator, true);
|
|
3148
|
+
untransferred(locator);
|
|
3149
|
+
liveQueries.recover();
|
|
3150
|
+
}),
|
|
3151
|
+
release: (key) => exclusive(async () => {
|
|
3152
|
+
await local.release(key);
|
|
3153
|
+
sweep();
|
|
1958
3154
|
}),
|
|
3155
|
+
content: (key) => heldContentOf(local, key),
|
|
3156
|
+
validate: (record, view) => validatedRoom(local, record, view, null),
|
|
1959
3157
|
};
|
|
1960
3158
|
}
|
|
1961
|
-
function reusable(room) {
|
|
1962
|
-
const record = replicas.get(room.locator)?.
|
|
3159
|
+
function reusable(room, local) {
|
|
3160
|
+
const record = replicas.get(room.locator)?.record;
|
|
1963
3161
|
if (record === undefined || record.replica.selection !== room.replica.selection)
|
|
1964
3162
|
return null;
|
|
1965
|
-
return
|
|
3163
|
+
return heldContentOf(local, record.key);
|
|
1966
3164
|
}
|
|
1967
|
-
async function pullBootstrap(authorized, room, local,
|
|
1968
|
-
if (!
|
|
3165
|
+
async function pullBootstrap(authorized, room, local, liveness) {
|
|
3166
|
+
if (!liveness())
|
|
1969
3167
|
return unconfirmed;
|
|
1970
3168
|
const turn = generation;
|
|
1971
3169
|
if (replicas.has(room.locator)) {
|
|
1972
3170
|
catching.add(room.locator);
|
|
1973
|
-
|
|
3171
|
+
liveQueries.recover();
|
|
1974
3172
|
}
|
|
1975
3173
|
try {
|
|
1976
|
-
const result = await bootstrapRoom(reads, authorized, room, {
|
|
1977
|
-
|
|
1978
|
-
selection: modelOf(room.locator, 'typed-selection'),
|
|
1979
|
-
}, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
|
|
1980
|
-
const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
|
|
3174
|
+
const result = await bootstrapRoom(reads, authorized, room, { collection: modelOf(room.locator) }, builtIn, (models, held) => (liveness() ? remodel(room.locator, models, held) : held), liveness, staging(local, liveness), reusable(room, local));
|
|
3175
|
+
const detail = result.kind === 'staged' ? result.record.roomVersion : result.kind;
|
|
1981
3176
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
1982
|
-
if (result.kind !== 'staged')
|
|
3177
|
+
if (result.kind !== 'staged') {
|
|
1983
3178
|
caughtUpTo(room.locator);
|
|
3179
|
+
installed(room.locator);
|
|
3180
|
+
}
|
|
1984
3181
|
return result;
|
|
1985
3182
|
}
|
|
1986
3183
|
catch (error) {
|
|
1987
|
-
journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
|
|
1988
3184
|
caughtUpTo(room.locator);
|
|
3185
|
+
installed(room.locator);
|
|
3186
|
+
if (refusedForSpace(error))
|
|
3187
|
+
return unconfirmed;
|
|
3188
|
+
journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
|
|
1989
3189
|
throw error;
|
|
1990
3190
|
}
|
|
1991
3191
|
finally {
|
|
@@ -1994,10 +3194,10 @@ export function composeRuntime(options) {
|
|
|
1994
3194
|
}
|
|
1995
3195
|
function caughtUpTo(locator) {
|
|
1996
3196
|
if (catching.delete(locator))
|
|
1997
|
-
|
|
3197
|
+
liveQueries.recover();
|
|
1998
3198
|
}
|
|
1999
3199
|
function behind(room) {
|
|
2000
|
-
const held = replicas.get(room.locator)?.
|
|
3200
|
+
const held = replicas.get(room.locator)?.record.roomVersion;
|
|
2001
3201
|
return (held !== undefined &&
|
|
2002
3202
|
[announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
|
|
2003
3203
|
}
|
|
@@ -2009,13 +3209,13 @@ export function composeRuntime(options) {
|
|
|
2009
3209
|
}
|
|
2010
3210
|
return advanceOnce(room, local, authorized, live);
|
|
2011
3211
|
}
|
|
2012
|
-
async function advanceOnce(room, local, authorized,
|
|
2013
|
-
|
|
2014
|
-
if (!live()
|
|
3212
|
+
async function advanceOnce(room, local, authorized, running) {
|
|
3213
|
+
const live = () => running() && !full;
|
|
3214
|
+
if (!live())
|
|
2015
3215
|
return unconfirmed;
|
|
2016
3216
|
const held = replicas.get(room.locator);
|
|
2017
3217
|
if (held !== undefined) {
|
|
2018
|
-
const refreshed = await pullRefresh(authorized, held
|
|
3218
|
+
const refreshed = await pullRefresh(authorized, held, live);
|
|
2019
3219
|
if (!live())
|
|
2020
3220
|
return unconfirmed;
|
|
2021
3221
|
if (refreshed.kind === 'still-current') {
|
|
@@ -2030,32 +3230,32 @@ export function composeRuntime(options) {
|
|
|
2030
3230
|
return null;
|
|
2031
3231
|
}
|
|
2032
3232
|
if (refreshed.kind === 'advanced') {
|
|
2033
|
-
|
|
2034
|
-
|
|
3233
|
+
const landing = { kind: 'refreshed', base: refreshed.base };
|
|
3234
|
+
if (!(await publish(local, room, refreshed.record, landing, live))) {
|
|
3235
|
+
const now = replicas.get(room.locator)?.record.roomVersion;
|
|
2035
3236
|
const reached = now !== undefined &&
|
|
2036
|
-
options.adapter.reached(room.locator, now, refreshed.
|
|
3237
|
+
options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
|
|
2037
3238
|
return reached ? null : advance(room, local, authorized, live);
|
|
2038
3239
|
}
|
|
2039
|
-
await retainModel(local, room,
|
|
3240
|
+
await retainModel(local, room, modelOf(room.locator));
|
|
2040
3241
|
return null;
|
|
2041
3242
|
}
|
|
2042
3243
|
if (refreshed.kind !== 'beyond-horizon')
|
|
2043
3244
|
return refreshed;
|
|
2044
3245
|
if (opened !== null) {
|
|
2045
3246
|
owed.add(room.locator);
|
|
2046
|
-
|
|
3247
|
+
liveQueries.recover();
|
|
2047
3248
|
return unconfirmed;
|
|
2048
3249
|
}
|
|
2049
3250
|
if (reloadsDue.get(room.locator) === generation)
|
|
2050
3251
|
return unconfirmed;
|
|
2051
3252
|
}
|
|
2052
3253
|
const bootstrapped = await pullBootstrap(authorized, room, local, live);
|
|
2053
|
-
if (
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
return null;
|
|
3254
|
+
if (bootstrapped.kind === 'staged') {
|
|
3255
|
+
await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
|
|
3256
|
+
return live() ? null : unconfirmed;
|
|
3257
|
+
}
|
|
3258
|
+
return live() ? bootstrapped : unconfirmed;
|
|
2059
3259
|
}
|
|
2060
3260
|
async function withhold(withheld, room, local, progress) {
|
|
2061
3261
|
if (withheld.kind === 'unconfirmed')
|
|
@@ -2127,57 +3327,254 @@ export function composeRuntime(options) {
|
|
|
2127
3327
|
(accepted.author === undefined ? '' : ` author ${accepted.author}`) +
|
|
2128
3328
|
(accepted.time === undefined ? '' : ` time ${accepted.time}`));
|
|
2129
3329
|
}
|
|
2130
|
-
|
|
2131
|
-
|
|
3330
|
+
function settling(acceptance) {
|
|
3331
|
+
return acceptance !== null && 'apply' in acceptance;
|
|
3332
|
+
}
|
|
3333
|
+
function refusalOf(acceptance) {
|
|
3334
|
+
return settling(acceptance) ? (acceptance.refused ?? null) : acceptance;
|
|
3335
|
+
}
|
|
3336
|
+
async function acceptance(room, local, submitted, accepted) {
|
|
2132
3337
|
if (!continuing())
|
|
2133
3338
|
return unconfirmed;
|
|
2134
3339
|
const current = replicas.get(room.locator);
|
|
2135
3340
|
if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
|
|
2136
3341
|
return null;
|
|
2137
3342
|
}
|
|
2138
|
-
const held = current.
|
|
3343
|
+
const held = current.record.roomVersion;
|
|
2139
3344
|
logAccepted(room.locator, submitted, accepted);
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
3345
|
+
let record = current.record;
|
|
3346
|
+
let changes;
|
|
3347
|
+
if (!options.adapter.reached(room.locator, held, accepted.roomVersion)) {
|
|
3348
|
+
const paths = [...returnedPaths(submitted, accepted)];
|
|
3349
|
+
const confirmedLayer = await stepped(slicing(await local.entries(record.key, 'confirmed', paths), paths));
|
|
3350
|
+
const back = await stepped(returning(confirmedLayer, submitted, accepted, accepted.roomVersion));
|
|
3351
|
+
record = {
|
|
3352
|
+
...record,
|
|
3353
|
+
roomVersion: options.adapter.follows(room.locator, held, accepted.roomVersion)
|
|
3354
|
+
? accepted.roomVersion
|
|
3355
|
+
: held,
|
|
3356
|
+
};
|
|
3357
|
+
const overrides = new Map([
|
|
3358
|
+
...back.set.map((entry) => [entry.path, entry]),
|
|
3359
|
+
...back.removed.map((path) => [path, null]),
|
|
3360
|
+
]);
|
|
3361
|
+
const refused = await validatedRoom(local, record, 'confirmed', { record: current.record, changed: [...overrides.keys()] }, overrides);
|
|
3362
|
+
if (refused !== null) {
|
|
3363
|
+
const key = pendingKey(room.locator, submitted.id);
|
|
3364
|
+
return {
|
|
3365
|
+
locator: room.locator,
|
|
3366
|
+
left: [],
|
|
3367
|
+
commit: {
|
|
3368
|
+
rooms: [],
|
|
3369
|
+
writes: [
|
|
3370
|
+
{
|
|
3371
|
+
locator: room.locator,
|
|
3372
|
+
accepted: [{ id: submitted.id, roomVersion: accepted.roomVersion }],
|
|
3373
|
+
},
|
|
3374
|
+
],
|
|
3375
|
+
},
|
|
3376
|
+
refused,
|
|
3377
|
+
apply() {
|
|
3378
|
+
const state = pendings.get(key);
|
|
3379
|
+
if (state !== undefined && !fences.has(room.locator))
|
|
3380
|
+
pendings.set(key, { ...state, accepted: accepted.roomVersion });
|
|
3381
|
+
return changeSet(room.locator, { queue: true });
|
|
3382
|
+
},
|
|
3383
|
+
};
|
|
3384
|
+
}
|
|
3385
|
+
changes = back;
|
|
3386
|
+
}
|
|
3387
|
+
const settled = await settlement(local, room.locator, current, record, current.queue.filter((batch) => batch.id !== submitted.id), { room: true, ...(changes === undefined ? {} : { changes }) });
|
|
3388
|
+
return {
|
|
3389
|
+
...settled,
|
|
3390
|
+
apply(answer) {
|
|
3391
|
+
const set = settled.apply(answer);
|
|
3392
|
+
pendings.delete(pendingKey(room.locator, submitted.id));
|
|
3393
|
+
unreadable.delete(room.locator);
|
|
3394
|
+
sourceRefusals.delete(room.locator);
|
|
3395
|
+
declarationRefusals.delete(room.locator);
|
|
3396
|
+
list(room, true);
|
|
3397
|
+
return set;
|
|
3398
|
+
},
|
|
3399
|
+
};
|
|
3400
|
+
}
|
|
3401
|
+
async function adoptedTogether(local, acceptances) {
|
|
3402
|
+
const settlements = acceptances.filter(settling);
|
|
3403
|
+
if (settlements.length > 0) {
|
|
3404
|
+
publishChangeSet(await together(local, settlements));
|
|
3405
|
+
moveTo({ ...status, ...attention() });
|
|
3406
|
+
}
|
|
3407
|
+
await unpair(local);
|
|
2163
3408
|
}
|
|
2164
3409
|
async function adopted(room, local, submitted, accepted) {
|
|
2165
|
-
const failed = await exclusive(() =>
|
|
3410
|
+
const failed = await exclusive(async () => {
|
|
3411
|
+
const adopting = await acceptance(room, local, submitted, accepted);
|
|
3412
|
+
await adoptedTogether(local, [adopting]);
|
|
3413
|
+
return refusalOf(adopting);
|
|
3414
|
+
});
|
|
3415
|
+
if (failed === null)
|
|
3416
|
+
return true;
|
|
3417
|
+
if (failed.kind === 'refused' && continuing())
|
|
3418
|
+
refusedSource(room, failed);
|
|
3419
|
+
return false;
|
|
3420
|
+
}
|
|
3421
|
+
async function adoptedMove(room, partner, local, submitted, accepted) {
|
|
3422
|
+
const other = discovered.get(partner);
|
|
3423
|
+
const failed = await exclusive(async () => {
|
|
3424
|
+
const left = await acceptance(room, local, submitted, accepted);
|
|
3425
|
+
const arriving = replicas.get(partner)?.queue.find((batch) => batch.id === submitted.id);
|
|
3426
|
+
const arrived = other === undefined || arriving === undefined || accepted.destination === undefined
|
|
3427
|
+
? null
|
|
3428
|
+
: await acceptance(other, local, arriving, {
|
|
3429
|
+
kind: 'accepted',
|
|
3430
|
+
...accepted.destination,
|
|
3431
|
+
});
|
|
3432
|
+
await adoptedTogether(local, [left, arrived]);
|
|
3433
|
+
const refusedArrival = refusalOf(arrived);
|
|
3434
|
+
if (other !== undefined && refusedArrival?.kind === 'refused' && continuing()) {
|
|
3435
|
+
refusedSource(other, refusedArrival);
|
|
3436
|
+
}
|
|
3437
|
+
return refusalOf(left);
|
|
3438
|
+
});
|
|
3439
|
+
submitLater(partner);
|
|
2166
3440
|
if (failed === null)
|
|
2167
3441
|
return true;
|
|
2168
3442
|
if (failed.kind === 'refused' && continuing())
|
|
2169
3443
|
refusedSource(room, failed);
|
|
2170
3444
|
return false;
|
|
2171
3445
|
}
|
|
3446
|
+
function queueIn(locator) {
|
|
3447
|
+
return replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
3448
|
+
}
|
|
3449
|
+
function behindIn(locator, batch) {
|
|
3450
|
+
const held = replicas.get(locator)?.queue;
|
|
3451
|
+
if (held === undefined)
|
|
3452
|
+
return queuesWithoutReplica.get(locator) !== undefined;
|
|
3453
|
+
return held.findIndex((one) => one.id === batch.id) > 0;
|
|
3454
|
+
}
|
|
3455
|
+
function unpaired(skipped) {
|
|
3456
|
+
for (const locator of [...replicas.keys(), ...queuesWithoutReplica.keys()]) {
|
|
3457
|
+
for (const batch of queueIn(locator)) {
|
|
3458
|
+
const partner = partnerOf(batch, locator);
|
|
3459
|
+
if (partner === null || skipped.has(pendingKey(locator, batch.id)))
|
|
3460
|
+
continue;
|
|
3461
|
+
if (queueIn(partner).some((held) => held.id === batch.id))
|
|
3462
|
+
continue;
|
|
3463
|
+
if (unpairedIn(locator, batch))
|
|
3464
|
+
return { locator, batch };
|
|
3465
|
+
}
|
|
3466
|
+
}
|
|
3467
|
+
return null;
|
|
3468
|
+
}
|
|
3469
|
+
async function unpairing(local, locator, batches) {
|
|
3470
|
+
const ids = new Set(batches.map(({ id }) => id));
|
|
3471
|
+
const leaving = batches.map((batch) => ({
|
|
3472
|
+
batch,
|
|
3473
|
+
returned: !localSaves && leavingIn(batch, locator) ? returnedBy('withdrawn') : null,
|
|
3474
|
+
}));
|
|
3475
|
+
const outcome = localSaves ? 'dropped' : 'unpaired';
|
|
3476
|
+
const current = replicas.get(locator);
|
|
3477
|
+
if (current !== undefined) {
|
|
3478
|
+
const settled = await settlement(local, locator, current, current.record, current.queue.filter((one) => !ids.has(one.id)), { leaving });
|
|
3479
|
+
return {
|
|
3480
|
+
...settled,
|
|
3481
|
+
apply(answer) {
|
|
3482
|
+
for (const batch of batches)
|
|
3483
|
+
journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
|
|
3484
|
+
const set = settled.apply(answer);
|
|
3485
|
+
submitLater(locator);
|
|
3486
|
+
return set;
|
|
3487
|
+
},
|
|
3488
|
+
};
|
|
3489
|
+
}
|
|
3490
|
+
const queue = queueIn(locator).filter((one) => !ids.has(one.id));
|
|
3491
|
+
const written = new Map();
|
|
3492
|
+
for (const { batch, returned } of leaving) {
|
|
3493
|
+
if (returned === null)
|
|
3494
|
+
continue;
|
|
3495
|
+
for (const saved of pendings.get(pendingKey(locator, batch.id))?.drafts ?? []) {
|
|
3496
|
+
written.set(saved.path, {
|
|
3497
|
+
locator,
|
|
3498
|
+
path: saved.path,
|
|
3499
|
+
revision: roomDraftRevision(),
|
|
3500
|
+
parts: returnedInto((written.get(saved.path) ?? roomDrafts.get(locator)?.get(saved.path))?.parts, saved.parts),
|
|
3501
|
+
returned,
|
|
3502
|
+
});
|
|
3503
|
+
}
|
|
3504
|
+
}
|
|
3505
|
+
const carried = carriedBack(locator, leaving.flatMap(({ batch, returned }) => (returned === null ? [] : [batch])), written);
|
|
3506
|
+
return {
|
|
3507
|
+
locator,
|
|
3508
|
+
left: batches,
|
|
3509
|
+
commit: {
|
|
3510
|
+
rooms: [],
|
|
3511
|
+
writes: [
|
|
3512
|
+
{
|
|
3513
|
+
locator,
|
|
3514
|
+
queue,
|
|
3515
|
+
roomDrafts: [...written.values()],
|
|
3516
|
+
consumedRoomDrafts: carried.taken,
|
|
3517
|
+
},
|
|
3518
|
+
...carried.writes,
|
|
3519
|
+
],
|
|
3520
|
+
},
|
|
3521
|
+
apply() {
|
|
3522
|
+
for (const batch of batches) {
|
|
3523
|
+
journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
|
|
3524
|
+
pendings.delete(pendingKey(locator, batch.id));
|
|
3525
|
+
}
|
|
3526
|
+
if (queue.length > 0)
|
|
3527
|
+
queuesWithoutReplica.set(locator, queue);
|
|
3528
|
+
else
|
|
3529
|
+
queuesWithoutReplica.delete(locator);
|
|
3530
|
+
for (const record of written.values())
|
|
3531
|
+
rememberRoomDraft(record);
|
|
3532
|
+
carried.remember();
|
|
3533
|
+
submitLater(locator);
|
|
3534
|
+
return mergeChangeSets([
|
|
3535
|
+
changeSet(locator, {
|
|
3536
|
+
queue: true,
|
|
3537
|
+
drafts: [...written.keys(), ...carried.taken],
|
|
3538
|
+
}),
|
|
3539
|
+
...carried.changes,
|
|
3540
|
+
]);
|
|
3541
|
+
},
|
|
3542
|
+
};
|
|
3543
|
+
}
|
|
3544
|
+
async function unpair(local) {
|
|
3545
|
+
const handled = new Set();
|
|
3546
|
+
for (let held = unpaired(handled); held !== null && continuing(); held = unpaired(handled)) {
|
|
3547
|
+
const { locator, batch } = held;
|
|
3548
|
+
handled.add(pendingKey(locator, batch.id));
|
|
3549
|
+
publishChangeSet(await together(local, [await unpairing(local, locator, [batch])]));
|
|
3550
|
+
}
|
|
3551
|
+
if (handled.size === 0)
|
|
3552
|
+
return;
|
|
3553
|
+
moveTo({ ...status, ...attention() });
|
|
3554
|
+
}
|
|
3555
|
+
async function discardHeld(head, room, local, signal) {
|
|
3556
|
+
const partner = partnerOf(head, room.locator);
|
|
3557
|
+
const other = partner === null ? undefined : discovered.get(partner);
|
|
3558
|
+
await discardPending(other === undefined ? [room] : [room, other], local, signal);
|
|
3559
|
+
}
|
|
2172
3560
|
async function submitRoom(room, local, authorized, progress, signal) {
|
|
2173
3561
|
while (continuing() && authorization !== undefined && !signal.aborted) {
|
|
2174
3562
|
const replica = replicas.get(room.locator);
|
|
2175
3563
|
const head = replica?.queue[0];
|
|
2176
3564
|
if (replica === undefined || head === undefined)
|
|
2177
3565
|
return 'done';
|
|
2178
|
-
|
|
3566
|
+
const state = pendings.get(pendingKey(room.locator, head.id));
|
|
3567
|
+
if (state?.accepted !== undefined)
|
|
3568
|
+
return 'done';
|
|
3569
|
+
const partner = partnerOf(head, room.locator);
|
|
3570
|
+
if (partner !== null && !leavingIn(head, room.locator)) {
|
|
3571
|
+
submitLater(partner);
|
|
3572
|
+
return 'done';
|
|
3573
|
+
}
|
|
3574
|
+
if (partner !== null && behindIn(partner, head)) {
|
|
3575
|
+
progress.complete = false;
|
|
2179
3576
|
return 'done';
|
|
2180
|
-
|
|
3577
|
+
}
|
|
2181
3578
|
const { submission, lost: earlier } = state ?? {
|
|
2182
3579
|
submission: { state: 'waiting' },
|
|
2183
3580
|
lost: false,
|
|
@@ -2193,16 +3590,16 @@ export function composeRuntime(options) {
|
|
|
2193
3590
|
}
|
|
2194
3591
|
const lost = earlier || submission.state === 'submitted';
|
|
2195
3592
|
if (lost && pastHorizon(head.id)) {
|
|
2196
|
-
await
|
|
3593
|
+
await discardHeld(head, room, local, signal);
|
|
2197
3594
|
return 'done';
|
|
2198
3595
|
}
|
|
2199
3596
|
let result;
|
|
2200
3597
|
journal.record(`submission ${room.locator} ${head.id}`);
|
|
2201
|
-
await exclusive(() => mark(local, room.locator, head
|
|
3598
|
+
await exclusive(() => mark(local, room.locator, head, { state: 'submitted' }, lost));
|
|
2202
3599
|
if (discoveryPaused || fences.has(room.locator))
|
|
2203
3600
|
return 'done';
|
|
2204
3601
|
if (signal.aborted || writesHeld()) {
|
|
2205
|
-
await exclusive(() => mark(local, room.locator, head
|
|
3602
|
+
await exclusive(() => mark(local, room.locator, head, submission, earlier));
|
|
2206
3603
|
if (signal.aborted)
|
|
2207
3604
|
return 'done';
|
|
2208
3605
|
progress.complete = false;
|
|
@@ -2210,11 +3607,17 @@ export function composeRuntime(options) {
|
|
|
2210
3607
|
return 'done';
|
|
2211
3608
|
}
|
|
2212
3609
|
const opensBefore = openSignals;
|
|
2213
|
-
const
|
|
3610
|
+
const answeredHere = awaitingAnswer(room.locator);
|
|
3611
|
+
const answeredThere = partner === null ? () => { } : awaitingAnswer(partner);
|
|
3612
|
+
const answered = () => {
|
|
3613
|
+
answeredHere();
|
|
3614
|
+
answeredThere();
|
|
3615
|
+
};
|
|
2214
3616
|
try {
|
|
2215
3617
|
result = await options.adapter.mutate(authorized, {
|
|
2216
3618
|
locator: room.locator,
|
|
2217
3619
|
changeBatch: head,
|
|
3620
|
+
file: batchFile(head),
|
|
2218
3621
|
...(state?.upload === undefined ? {} : { upload: state.upload }),
|
|
2219
3622
|
}, (upload) => exclusive(() => keepUpload(local, room.locator, head.id, upload)));
|
|
2220
3623
|
}
|
|
@@ -2239,9 +3642,10 @@ export function composeRuntime(options) {
|
|
|
2239
3642
|
return 'again';
|
|
2240
3643
|
}
|
|
2241
3644
|
if (result.kind === 'room-not-readable') {
|
|
3645
|
+
const unread = discovered.get(result.locator) ?? room;
|
|
2242
3646
|
progress.complete = false;
|
|
2243
|
-
unreadable.add(
|
|
2244
|
-
list(
|
|
3647
|
+
unreadable.add(unread.locator);
|
|
3648
|
+
list(unread, false);
|
|
2245
3649
|
emitRooms();
|
|
2246
3650
|
return 'done';
|
|
2247
3651
|
}
|
|
@@ -2256,24 +3660,30 @@ export function composeRuntime(options) {
|
|
|
2256
3660
|
}
|
|
2257
3661
|
if (result.kind === 'write-halted') {
|
|
2258
3662
|
journal.record(`outcome ${room.locator} ${head.id} write-halted`);
|
|
2259
|
-
await exclusive(() => mark(local, room.locator, head
|
|
3663
|
+
await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, lost));
|
|
2260
3664
|
if (serviceMode === 'open' && openSignals !== opensBefore)
|
|
2261
3665
|
continue;
|
|
2262
3666
|
haltedRefusal = true;
|
|
2263
3667
|
progress.complete = false;
|
|
2264
3668
|
return 'done';
|
|
2265
3669
|
}
|
|
3670
|
+
const other = partner === null ? undefined : discovered.get(partner);
|
|
2266
3671
|
if (result.kind === 'conflicted' ||
|
|
2267
3672
|
result.kind === 'rejected' ||
|
|
2268
3673
|
result.kind === 'expired' ||
|
|
2269
3674
|
result.kind === 'retryable-refusal') {
|
|
2270
3675
|
if (result.kind === 'expired' && lost && result.restored !== true) {
|
|
2271
3676
|
journal.record(`outcome ${room.locator} ${head.id} expired`);
|
|
2272
|
-
await
|
|
3677
|
+
await discardHeld(head, room, local, signal);
|
|
2273
3678
|
return 'done';
|
|
2274
3679
|
}
|
|
2275
3680
|
if (result.kind === 'expired') {
|
|
2276
|
-
await exclusive(() => mark(local, room.locator, head
|
|
3681
|
+
await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, partner !== null && lost));
|
|
3682
|
+
const there = other === undefined ? null : await advance(other, local, authorized, continuing);
|
|
3683
|
+
if (other !== undefined && there !== null) {
|
|
3684
|
+
await withhold(there, other, local, progress);
|
|
3685
|
+
return 'done';
|
|
3686
|
+
}
|
|
2277
3687
|
const withheld = await advance(room, local, authorized, continuing);
|
|
2278
3688
|
if (withheld !== null) {
|
|
2279
3689
|
await withhold(withheld, room, local, progress);
|
|
@@ -2290,11 +3700,25 @@ export function composeRuntime(options) {
|
|
|
2290
3700
|
if (result.kind === 'retryable-refusal') {
|
|
2291
3701
|
const reset = result.reset;
|
|
2292
3702
|
progress.complete = false;
|
|
2293
|
-
await exclusive(() => mark(local, room.locator, head
|
|
3703
|
+
await exclusive(() => mark(local, room.locator, head, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
|
|
2294
3704
|
return reset === undefined ? 'again' : { at: Date.parse(reset) };
|
|
2295
3705
|
}
|
|
2296
3706
|
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
2297
3707
|
const left = leaving(head, result);
|
|
3708
|
+
const refreshed = async () => ({
|
|
3709
|
+
there: other === undefined ? null : await advance(other, local, authorized, continuing),
|
|
3710
|
+
here: await advance(room, local, authorized, continuing),
|
|
3711
|
+
});
|
|
3712
|
+
const judged = result.kind === 'conflicted' && other !== undefined ? await refreshed() : null;
|
|
3713
|
+
if (judged !== null && (judged.there !== null || judged.here !== null)) {
|
|
3714
|
+
await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, lost));
|
|
3715
|
+
if (other !== undefined && judged.there !== null) {
|
|
3716
|
+
await withhold(judged.there, other, local, progress);
|
|
3717
|
+
}
|
|
3718
|
+
if (judged.here !== null)
|
|
3719
|
+
await withhold(judged.here, room, local, progress);
|
|
3720
|
+
return 'done';
|
|
3721
|
+
}
|
|
2298
3722
|
await exclusive(async () => {
|
|
2299
3723
|
if (!continuing())
|
|
2300
3724
|
return;
|
|
@@ -2302,25 +3726,29 @@ export function composeRuntime(options) {
|
|
|
2302
3726
|
if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
|
|
2303
3727
|
return;
|
|
2304
3728
|
}
|
|
2305
|
-
const
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
});
|
|
2311
|
-
|
|
3729
|
+
const confirmedLayer = await sliceAt(current.record, 'confirmed', [head]);
|
|
3730
|
+
const held = (result.kind === 'expired' &&
|
|
3731
|
+
(await landedWhole(room.locator, head, confirmedLayer))) ||
|
|
3732
|
+
(result.kind === 'conflicted' &&
|
|
3733
|
+
(await landedWhole(room.locator, head, confirmedLayer, result.affected)));
|
|
3734
|
+
publishChangeSet(await settle(local, room.locator, current, current.record, current.queue.filter((batch) => batch.id !== head.id), { leaving: [held ? { ...left, returned: null } : left] }));
|
|
3735
|
+
await unpair(local);
|
|
2312
3736
|
moveTo({ ...status, ...attention() });
|
|
2313
3737
|
});
|
|
2314
3738
|
if (result.kind === 'expired')
|
|
2315
3739
|
continue;
|
|
2316
|
-
const
|
|
2317
|
-
if (
|
|
2318
|
-
await withhold(
|
|
3740
|
+
const { there, here } = judged ?? (await refreshed());
|
|
3741
|
+
if (other !== undefined && there !== null)
|
|
3742
|
+
await withhold(there, other, local, progress);
|
|
3743
|
+
if (here !== null) {
|
|
3744
|
+
await withhold(here, room, local, progress);
|
|
2319
3745
|
return 'done';
|
|
2320
3746
|
}
|
|
2321
3747
|
continue;
|
|
2322
3748
|
}
|
|
2323
|
-
const kept = await
|
|
3749
|
+
const kept = await (partner === null
|
|
3750
|
+
? adopted(room, local, head, result)
|
|
3751
|
+
: adoptedMove(room, partner, local, head, result)).finally(answered);
|
|
2324
3752
|
if (!kept) {
|
|
2325
3753
|
progress.complete = false;
|
|
2326
3754
|
return 'done';
|
|
@@ -2331,44 +3759,67 @@ export function composeRuntime(options) {
|
|
|
2331
3759
|
function pastHorizon(identity) {
|
|
2332
3760
|
return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
|
|
2333
3761
|
}
|
|
2334
|
-
async function discardPending(
|
|
2335
|
-
const { locator } = room;
|
|
3762
|
+
async function discardPending(rooms, local, signal) {
|
|
2336
3763
|
const discarded = await exclusive(async () => {
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
3764
|
+
const held = rooms
|
|
3765
|
+
.filter(({ locator }) => !fences.has(locator))
|
|
3766
|
+
.map((room) => ({
|
|
3767
|
+
room,
|
|
3768
|
+
before: replicas.get(room.locator),
|
|
3769
|
+
queue: queueIn(room.locator),
|
|
3770
|
+
}));
|
|
3771
|
+
if (held.length === 0)
|
|
3772
|
+
return [];
|
|
3773
|
+
const answer = await local.commit({
|
|
3774
|
+
rooms: held.flatMap(({ room, before }) => before === undefined
|
|
3775
|
+
? []
|
|
3776
|
+
: [
|
|
3777
|
+
{
|
|
3778
|
+
locator: room.locator,
|
|
3779
|
+
key: before.record.key,
|
|
3780
|
+
roots: before.record.typedSelection?.roots ?? [],
|
|
3781
|
+
overlay: { set: [], cleared: [...before.overlaid] },
|
|
3782
|
+
},
|
|
3783
|
+
]),
|
|
3784
|
+
discards: held.map(({ room }) => room.locator),
|
|
3785
|
+
});
|
|
3786
|
+
for (const { room, before, queue } of held) {
|
|
3787
|
+
const { locator } = room;
|
|
3788
|
+
lastFenced.set(locator, ++fencings);
|
|
3789
|
+
for (const batch of queue)
|
|
3790
|
+
pendings.delete(pendingKey(locator, batch.id));
|
|
3791
|
+
queuesWithoutReplica.delete(locator);
|
|
3792
|
+
if (before !== undefined) {
|
|
3793
|
+
replicas.set(locator, {
|
|
3794
|
+
...before,
|
|
3795
|
+
queue: [],
|
|
3796
|
+
overlaid: new Set(),
|
|
3797
|
+
count: answer.counts[locator] ?? before.count,
|
|
3798
|
+
});
|
|
3799
|
+
}
|
|
3800
|
+
journal.record(`room discarded ${locator}`);
|
|
2348
3801
|
}
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
if (after !== null)
|
|
2354
|
-
replicas.set(locator, after);
|
|
2355
|
-
journal.record(`room discarded ${locator}`);
|
|
2356
|
-
publishChangeSet(changeSet(locator, before ?? null, after, { queue: true }));
|
|
3802
|
+
publishChangeSet(mergeChangeSets(held.map(({ room }) => changeSet(room.locator, {
|
|
3803
|
+
documents: answer.changed[room.locator] ?? [],
|
|
3804
|
+
queue: true,
|
|
3805
|
+
}))));
|
|
2357
3806
|
moveTo({ ...status, ...attention() });
|
|
2358
|
-
return
|
|
2359
|
-
});
|
|
2360
|
-
if (!discarded)
|
|
2361
|
-
return;
|
|
2362
|
-
const progress = { complete: true, reachable: true };
|
|
2363
|
-
await roomTask(locator, async (live) => {
|
|
2364
|
-
await Promise.race([
|
|
2365
|
-
reload(room, local, progress, () => live() && !signal.aborted),
|
|
2366
|
-
abandonedOn(signal),
|
|
2367
|
-
]);
|
|
3807
|
+
return held.map(({ room }) => room);
|
|
2368
3808
|
});
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
3809
|
+
for (const room of discarded) {
|
|
3810
|
+
if (signal.aborted)
|
|
3811
|
+
return;
|
|
3812
|
+
const progress = { complete: true, reachable: true };
|
|
3813
|
+
await roomTask(room.locator, async (live) => {
|
|
3814
|
+
await Promise.race([
|
|
3815
|
+
reload(room, local, progress, () => live() && !signal.aborted),
|
|
3816
|
+
abandonedOn(signal),
|
|
3817
|
+
]);
|
|
3818
|
+
});
|
|
3819
|
+
if (signal.aborted)
|
|
3820
|
+
return;
|
|
3821
|
+
await rest(progress);
|
|
3822
|
+
}
|
|
2372
3823
|
}
|
|
2373
3824
|
function awaitingAnswer(locator) {
|
|
2374
3825
|
let release = () => { };
|
|
@@ -2382,10 +3833,10 @@ export function composeRuntime(options) {
|
|
|
2382
3833
|
release();
|
|
2383
3834
|
};
|
|
2384
3835
|
}
|
|
2385
|
-
async function retainModel(local, room,
|
|
2386
|
-
const key =
|
|
2387
|
-
const previous = modelOf(room.locator
|
|
2388
|
-
const heldBefore = replicas.get(room.locator)?.
|
|
3836
|
+
async function retainModel(local, room, next) {
|
|
3837
|
+
const key = room.locator;
|
|
3838
|
+
const previous = modelOf(room.locator);
|
|
3839
|
+
const heldBefore = replicas.get(room.locator)?.record;
|
|
2389
3840
|
if (JSON.stringify(previous) === JSON.stringify(next) &&
|
|
2390
3841
|
(heldBefore === undefined ||
|
|
2391
3842
|
(JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) &&
|
|
@@ -2404,32 +3855,47 @@ export function composeRuntime(options) {
|
|
|
2404
3855
|
let changed = null;
|
|
2405
3856
|
if (held !== undefined) {
|
|
2406
3857
|
const declared = {
|
|
2407
|
-
...held.
|
|
3858
|
+
...held.record,
|
|
2408
3859
|
operations: room.operations,
|
|
2409
3860
|
replica: room.replica,
|
|
2410
3861
|
typedSelection: room.typedSelection ?? null,
|
|
2411
|
-
|
|
3862
|
+
model: next,
|
|
2412
3863
|
};
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
3864
|
+
let read = 'staged';
|
|
3865
|
+
if (!sameMembership(declared, held.record)) {
|
|
3866
|
+
const verdict = await validatedCollection(walkOf(local, held.record.key, 'confirmed'), lookupOf(local, held.record.key, 'confirmed'), composed(declared));
|
|
3867
|
+
read =
|
|
3868
|
+
verdict.verdict !== 'refuse'
|
|
3869
|
+
? 'staged'
|
|
3870
|
+
: verdict.rule === 'model-unavailable'
|
|
3871
|
+
? 'deferred'
|
|
3872
|
+
: { kind: 'refused', refusal: verdict };
|
|
3873
|
+
}
|
|
3874
|
+
const record = read === 'staged'
|
|
2417
3875
|
? declared
|
|
2418
|
-
: {
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
const
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
3876
|
+
: { ...declared, typedSelection: held.record.typedSelection };
|
|
3877
|
+
const confirmedLayer = await sliceAt(record, 'confirmed', held.queue, held.overlaid);
|
|
3878
|
+
const replayedLayer = await replayed(record, confirmedLayer, held.queue);
|
|
3879
|
+
const diff = await overlayDiff(record, held, replayedLayer.visible, replayedLayer.touched);
|
|
3880
|
+
const answer = await local.commit({
|
|
3881
|
+
rooms: [
|
|
3882
|
+
{
|
|
3883
|
+
locator: room.locator,
|
|
3884
|
+
key: record.key,
|
|
3885
|
+
record: storedRecord(record),
|
|
3886
|
+
roots: record.typedSelection?.roots ?? [],
|
|
3887
|
+
composition: composed(record),
|
|
3888
|
+
overlay: diff.overlay,
|
|
3889
|
+
},
|
|
3890
|
+
],
|
|
3891
|
+
});
|
|
3892
|
+
const { count, indexed } = committedRoom(room.locator, answer);
|
|
3893
|
+
replicas.set(room.locator, { ...held, record, overlaid: diff.touched, count, indexed });
|
|
3894
|
+
changed = changeSet(room.locator, {
|
|
3895
|
+
documents: answer.changed[room.locator] ?? [],
|
|
3896
|
+
room: true,
|
|
3897
|
+
});
|
|
3898
|
+
if (read === 'staged' || read === 'deferred') {
|
|
2433
3899
|
if (declarationRefusals.delete(room.locator)) {
|
|
2434
3900
|
sourceRefusals.delete(room.locator);
|
|
2435
3901
|
unreadable.delete(room.locator);
|
|
@@ -2453,7 +3919,7 @@ export function composeRuntime(options) {
|
|
|
2453
3919
|
});
|
|
2454
3920
|
}
|
|
2455
3921
|
async function scoped(result, room, local) {
|
|
2456
|
-
if (
|
|
3922
|
+
if (result.scope === 'content' && !result.retryable) {
|
|
2457
3923
|
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
2458
3924
|
await loseRooms([room.locator]);
|
|
2459
3925
|
return;
|
|
@@ -2465,22 +3931,14 @@ export function composeRuntime(options) {
|
|
|
2465
3931
|
});
|
|
2466
3932
|
let scopedAway = room;
|
|
2467
3933
|
if (result.scope === 'model')
|
|
2468
|
-
await retainModel(local, room,
|
|
3934
|
+
await retainModel(local, room, {
|
|
2469
3935
|
status: 'unavailable',
|
|
2470
3936
|
reason: 'not-readable',
|
|
2471
3937
|
});
|
|
2472
3938
|
else if (result.scope !== 'operation') {
|
|
2473
3939
|
unreadable.add(room.locator);
|
|
2474
|
-
scopedAway = {
|
|
2475
|
-
|
|
2476
|
-
replica: { ...room.replica, admission: 'unavailable' },
|
|
2477
|
-
operations: {
|
|
2478
|
-
...room.operations,
|
|
2479
|
-
...(result.scope === 'content' ? { read: [] } : {}),
|
|
2480
|
-
...(result.scope === 'sync' ? { sync: [] } : {}),
|
|
2481
|
-
},
|
|
2482
|
-
};
|
|
2483
|
-
await retainModel(local, scopedAway, 'collection', modelOf(room.locator));
|
|
3940
|
+
scopedAway = { ...room, replica: { ...room.replica, admission: 'unavailable' } };
|
|
3941
|
+
await retainModel(local, scopedAway, modelOf(room.locator));
|
|
2484
3942
|
}
|
|
2485
3943
|
if (!continuing())
|
|
2486
3944
|
return;
|
|
@@ -2490,17 +3948,10 @@ export function composeRuntime(options) {
|
|
|
2490
3948
|
}
|
|
2491
3949
|
function heldRooms(listed) {
|
|
2492
3950
|
return listed.flatMap((room) => {
|
|
2493
|
-
const record = replicas.get(room.locator)?.
|
|
3951
|
+
const record = replicas.get(room.locator)?.record;
|
|
2494
3952
|
if (record === undefined || record.replica.selection !== room.replica.selection)
|
|
2495
3953
|
return [];
|
|
2496
|
-
return [
|
|
2497
|
-
{
|
|
2498
|
-
locator: room.locator,
|
|
2499
|
-
roomVersion: record.roomVersion,
|
|
2500
|
-
documents: record.documents,
|
|
2501
|
-
revisions: record.revisions,
|
|
2502
|
-
},
|
|
2503
|
-
];
|
|
3954
|
+
return [{ locator: room.locator, roomVersion: record.roomVersion }];
|
|
2504
3955
|
});
|
|
2505
3956
|
}
|
|
2506
3957
|
async function synchronize(listed, local, progress, attempt, live) {
|
|
@@ -2528,7 +3979,7 @@ export function composeRuntime(options) {
|
|
|
2528
3979
|
progress.complete = false;
|
|
2529
3980
|
return false;
|
|
2530
3981
|
}
|
|
2531
|
-
const prior = replicas.get(room.locator)?.
|
|
3982
|
+
const prior = replicas.get(room.locator)?.record;
|
|
2532
3983
|
if (prior !== undefined && prior.replica.selection !== room.replica.selection) {
|
|
2533
3984
|
list(prior, false);
|
|
2534
3985
|
emitRooms();
|
|
@@ -2537,7 +3988,7 @@ export function composeRuntime(options) {
|
|
|
2537
3988
|
return false;
|
|
2538
3989
|
}
|
|
2539
3990
|
if (prior !== undefined)
|
|
2540
|
-
await retainModel(local, room,
|
|
3991
|
+
await retainModel(local, room, modelOf(room.locator));
|
|
2541
3992
|
if (discoveryPaused || authorization === undefined)
|
|
2542
3993
|
return false;
|
|
2543
3994
|
return room.replica.admission === 'admitted';
|
|
@@ -2550,10 +4001,8 @@ export function composeRuntime(options) {
|
|
|
2550
4001
|
await withhold(withheld, room, local, progress);
|
|
2551
4002
|
}
|
|
2552
4003
|
function revoked(locator) {
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
2556
|
-
});
|
|
4004
|
+
const model = modelOf(locator);
|
|
4005
|
+
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
2557
4006
|
}
|
|
2558
4007
|
async function loadRoom(room, local, progress, live) {
|
|
2559
4008
|
if (!live() || authorization === undefined)
|
|
@@ -2576,13 +4025,12 @@ export function composeRuntime(options) {
|
|
|
2576
4025
|
if (!live() || authorization === undefined)
|
|
2577
4026
|
return;
|
|
2578
4027
|
const bootstrapped = await pullBootstrap(authorization, room, local, live);
|
|
2579
|
-
if (!live())
|
|
2580
|
-
return;
|
|
2581
4028
|
if (bootstrapped.kind !== 'staged') {
|
|
2582
|
-
|
|
4029
|
+
if (live())
|
|
4030
|
+
await withhold(bootstrapped, room, local, progress);
|
|
2583
4031
|
return;
|
|
2584
4032
|
}
|
|
2585
|
-
await publish(local, room, bootstrapped.
|
|
4033
|
+
await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
|
|
2586
4034
|
await caughtUp(room, local, progress, live);
|
|
2587
4035
|
}
|
|
2588
4036
|
finally {
|
|
@@ -2612,10 +4060,7 @@ export function composeRuntime(options) {
|
|
|
2612
4060
|
journal.record(`connection discover ${result.kind}`);
|
|
2613
4061
|
if (result.kind === 'transport-uncertainty') {
|
|
2614
4062
|
for (const room of rooms.values()) {
|
|
2615
|
-
await retainModel(local, room,
|
|
2616
|
-
if (room.typedSelection !== null) {
|
|
2617
|
-
await retainModel(local, room, 'typed-selection', cached(modelOf(room.locator, 'typed-selection')));
|
|
2618
|
-
}
|
|
4063
|
+
await retainModel(local, room, cached(modelOf(room.locator)));
|
|
2619
4064
|
}
|
|
2620
4065
|
progress.reachable = false;
|
|
2621
4066
|
progress.complete = false;
|
|
@@ -2639,12 +4084,13 @@ export function composeRuntime(options) {
|
|
|
2639
4084
|
}
|
|
2640
4085
|
for (const room of result.rooms) {
|
|
2641
4086
|
if (room.operations === undefined ||
|
|
2642
|
-
!['
|
|
4087
|
+
!['write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
|
|
2643
4088
|
room.operations[operation].every((scope) => typeof scope === 'string')) ||
|
|
2644
4089
|
room.replica === undefined ||
|
|
2645
4090
|
typeof room.replica.selection !== 'string' ||
|
|
2646
4091
|
room.replica.selection.length === 0 ||
|
|
2647
|
-
!['admitted', 'unavailable'].includes(room.replica.admission)
|
|
4092
|
+
!['admitted', 'unavailable'].includes(room.replica.admission) ||
|
|
4093
|
+
!['locked', 'open'].includes(room.replica.offline)) {
|
|
2648
4094
|
journal.record(`invalid model or replica declaration ${room.locator}`);
|
|
2649
4095
|
await refuse('missing-required-guarantee');
|
|
2650
4096
|
notifyReady();
|
|
@@ -2668,11 +4114,12 @@ export function composeRuntime(options) {
|
|
|
2668
4114
|
if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
|
|
2669
4115
|
JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
|
|
2670
4116
|
limits = result.providerLimits;
|
|
2671
|
-
await local.
|
|
4117
|
+
await local.fact('providerLimits', limits);
|
|
4118
|
+
await local.fact('capabilities', capabilities);
|
|
2672
4119
|
}
|
|
2673
4120
|
const signedIn = result.member ?? null;
|
|
2674
4121
|
if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
|
|
2675
|
-
await local.
|
|
4122
|
+
await local.fact('member', signedIn);
|
|
2676
4123
|
member = signedIn;
|
|
2677
4124
|
emitMember();
|
|
2678
4125
|
}
|
|
@@ -2806,6 +4253,7 @@ export function composeRuntime(options) {
|
|
|
2806
4253
|
function closedSocket(revoked, lost) {
|
|
2807
4254
|
const opened = socketOpen;
|
|
2808
4255
|
socketOpen = false;
|
|
4256
|
+
connectionLost();
|
|
2809
4257
|
if (lost) {
|
|
2810
4258
|
reads.lost();
|
|
2811
4259
|
conversations.lost();
|
|
@@ -2875,7 +4323,7 @@ export function composeRuntime(options) {
|
|
|
2875
4323
|
owed.clear();
|
|
2876
4324
|
for (const locator of reloading)
|
|
2877
4325
|
catching.add(locator);
|
|
2878
|
-
|
|
4326
|
+
liveQueries.recover();
|
|
2879
4327
|
return reloading;
|
|
2880
4328
|
}
|
|
2881
4329
|
async function reloadNow(replaced, progress) {
|
|
@@ -2894,12 +4342,9 @@ export function composeRuntime(options) {
|
|
|
2894
4342
|
}
|
|
2895
4343
|
}
|
|
2896
4344
|
async function confirmModels(room, local) {
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
await retainModel(local, room, attached, { ...model, status: 'current' });
|
|
2901
|
-
}
|
|
2902
|
-
}
|
|
4345
|
+
const model = modelOf(room.locator);
|
|
4346
|
+
if (model.status === 'cached')
|
|
4347
|
+
await retainModel(local, room, { ...model, status: 'current' });
|
|
2903
4348
|
}
|
|
2904
4349
|
function submitLater(locator) {
|
|
2905
4350
|
if (localSaves)
|
|
@@ -3042,7 +4487,7 @@ export function composeRuntime(options) {
|
|
|
3042
4487
|
if (synced && !recorded) {
|
|
3043
4488
|
const completedAt = new Date().toISOString();
|
|
3044
4489
|
try {
|
|
3045
|
-
await (await openLocal()).
|
|
4490
|
+
await (await openLocal()).fact('lastCompleted', completedAt);
|
|
3046
4491
|
if (!live())
|
|
3047
4492
|
return;
|
|
3048
4493
|
recorded = true;
|
|
@@ -3082,8 +4527,8 @@ export function composeRuntime(options) {
|
|
|
3082
4527
|
if (room === undefined || room.replica.admission !== 'admitted')
|
|
3083
4528
|
return;
|
|
3084
4529
|
if (held !== undefined &&
|
|
3085
|
-
(held.
|
|
3086
|
-
options.adapter.reached(locator, held.
|
|
4530
|
+
(held.record.replica.selection !== room.replica.selection ||
|
|
4531
|
+
options.adapter.reached(locator, held.record.roomVersion, roomVersion))) {
|
|
3087
4532
|
return;
|
|
3088
4533
|
}
|
|
3089
4534
|
const during = underway?.generation === generation ? underway.progress : null;
|
|
@@ -3130,6 +4575,7 @@ export function composeRuntime(options) {
|
|
|
3130
4575
|
return;
|
|
3131
4576
|
if (event.kind === 'opened') {
|
|
3132
4577
|
socketOpen = true;
|
|
4578
|
+
connectionLive();
|
|
3133
4579
|
generation += 1;
|
|
3134
4580
|
reads.opened();
|
|
3135
4581
|
conversations.reopened();
|
|
@@ -3144,7 +4590,14 @@ export function composeRuntime(options) {
|
|
|
3144
4590
|
void rediscover(event.scope);
|
|
3145
4591
|
else
|
|
3146
4592
|
void applyRoom(event.locator, event.roomVersion);
|
|
3147
|
-
}, receipts);
|
|
4593
|
+
}, receipts, heldRevisions);
|
|
4594
|
+
}
|
|
4595
|
+
async function heldRevisions(locator, paths) {
|
|
4596
|
+
const held = replicas.get(locator);
|
|
4597
|
+
if (held === undefined || paths.length === 0)
|
|
4598
|
+
return new Map();
|
|
4599
|
+
const entries = await (await openLocal()).entries(held.record.key, 'confirmed', paths);
|
|
4600
|
+
return new Map(entries.flatMap(({ path, revision }) => revision === null ? [] : [[path, revision]]));
|
|
3148
4601
|
}
|
|
3149
4602
|
function writesHeld() {
|
|
3150
4603
|
return serviceMode === 'write-halted' || haltedRefusal;
|
|
@@ -3214,8 +4667,12 @@ export function composeRuntime(options) {
|
|
|
3214
4667
|
if (closed)
|
|
3215
4668
|
return;
|
|
3216
4669
|
closed = true;
|
|
3217
|
-
if (
|
|
3218
|
-
clearInterval(
|
|
4670
|
+
if (probing !== null)
|
|
4671
|
+
clearInterval(probing);
|
|
4672
|
+
for (const resolve of shownWaiters.splice(0))
|
|
4673
|
+
resolve();
|
|
4674
|
+
connectionLive();
|
|
4675
|
+
lockListeners.clear();
|
|
3219
4676
|
readinessWatches.clear();
|
|
3220
4677
|
withdrawStream?.();
|
|
3221
4678
|
withdrawStream = undefined;
|
|
@@ -3224,17 +4681,13 @@ export function composeRuntime(options) {
|
|
|
3224
4681
|
withdrawServiceMode?.();
|
|
3225
4682
|
conversations.close();
|
|
3226
4683
|
void recovery.retire();
|
|
3227
|
-
store?.close();
|
|
3228
|
-
void opening?.then((opened) => {
|
|
3229
|
-
opened.close();
|
|
3230
|
-
}, () => { });
|
|
3231
4684
|
roomListeners.clear();
|
|
3232
4685
|
refusalListeners.clear();
|
|
3233
4686
|
storageLossListeners.clear();
|
|
3234
4687
|
statusListeners.clear();
|
|
3235
4688
|
serviceModeListeners.clear();
|
|
3236
4689
|
memberListeners.clear();
|
|
3237
|
-
|
|
4690
|
+
liveQueries.close();
|
|
3238
4691
|
queueSubscriptions.clear();
|
|
3239
4692
|
roomDraftListeners.clear();
|
|
3240
4693
|
presentationListeners.clear();
|
|
@@ -3280,7 +4733,7 @@ export function composeRuntime(options) {
|
|
|
3280
4733
|
const watchers = seedWatchers.get(plan.key) ?? new Set();
|
|
3281
4734
|
seedWatchers.set(plan.key, watchers);
|
|
3282
4735
|
watchers.add(shown);
|
|
3283
|
-
const stop =
|
|
4736
|
+
const stop = liveQueries.watch(plan, identityOf(plan), () => {
|
|
3284
4737
|
const cover = covered(plan);
|
|
3285
4738
|
awaitPreparation(cover);
|
|
3286
4739
|
return seedBehind(plan, cover);
|
|
@@ -3301,66 +4754,53 @@ export function composeRuntime(options) {
|
|
|
3301
4754
|
stop();
|
|
3302
4755
|
};
|
|
3303
4756
|
}
|
|
3304
|
-
async function searched(input, reach) {
|
|
4757
|
+
async function searched(input, reach, locked) {
|
|
3305
4758
|
const asked = searchPlanOf(input);
|
|
3306
|
-
const
|
|
3307
|
-
|
|
4759
|
+
const filter = {
|
|
4760
|
+
...filterOf(asked.documents ?? {}, roomFacts(reach), planReach(reach)),
|
|
4761
|
+
locked,
|
|
4762
|
+
};
|
|
4763
|
+
const unlocking = (filter.rooms ?? [])
|
|
4764
|
+
.filter((room) => locked.includes(room))
|
|
4765
|
+
.map((room) => ({ room, awaits: 'unlock' }));
|
|
3308
4766
|
for (;;) {
|
|
3309
4767
|
const cover = covered(filter);
|
|
3310
4768
|
awaitPreparation(cover);
|
|
3311
|
-
const pending = 'pending' in cover ?
|
|
3312
|
-
const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
|
|
3313
|
-
const held = covered({
|
|
3314
|
-
...filter,
|
|
3315
|
-
rooms: reachedRooms(filter).filter((room) => !awaited.has(room)),
|
|
3316
|
-
});
|
|
3317
|
-
const covering = 'pending' in held ? [] : held.coverage.rooms;
|
|
3318
|
-
const unindexed = covering
|
|
3319
|
-
.filter(({ room }) => !searchIndex.rooms().has(room))
|
|
3320
|
-
.map(({ room }) => room);
|
|
3321
|
-
for (const room of unindexed)
|
|
3322
|
-
pending.push({ room, awaits: 'index' });
|
|
3323
|
-
const listed = conversations.searchable();
|
|
3324
|
-
const named = reach !== null && !reach.conversations
|
|
3325
|
-
? []
|
|
3326
|
-
: (plan.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
|
|
3327
|
-
const waiting = named.filter((one) => !searchIndex.conversations().has(one));
|
|
3328
|
-
for (const conversation of waiting)
|
|
3329
|
-
pending.push({ conversation, awaits: 'index' });
|
|
3330
|
-
if (unindexed.length > 0 || waiting.length > 0)
|
|
3331
|
-
index(unindexed, waiting);
|
|
4769
|
+
const pending = [...unlocking, ...('pending' in cover ? cover.pending : [])];
|
|
3332
4770
|
const complete = pending.length === 0 && !('pending' in cover);
|
|
4771
|
+
const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
|
|
3333
4772
|
const answering = complete
|
|
3334
4773
|
? filter
|
|
3335
|
-
: {
|
|
3336
|
-
...filter,
|
|
3337
|
-
rooms: covering
|
|
3338
|
-
.filter(({ room }) => searchIndex.rooms().has(room))
|
|
3339
|
-
.map(({ room }) => room),
|
|
3340
|
-
};
|
|
4774
|
+
: { ...filter, rooms: reachedRooms(filter).filter((room) => !awaited.has(room)) };
|
|
3341
4775
|
const answered = covered(answering);
|
|
3342
4776
|
if ('pending' in answered)
|
|
3343
4777
|
continue;
|
|
3344
|
-
const
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
const
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
4778
|
+
const listed = conversations.searchable();
|
|
4779
|
+
const named = reach !== null && !reach.conversations
|
|
4780
|
+
? []
|
|
4781
|
+
: (asked.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
|
|
4782
|
+
const hits = await (await openLocal()).search({
|
|
4783
|
+
text: asked.text,
|
|
4784
|
+
limit: capped(asked.limit),
|
|
4785
|
+
after: asked.after,
|
|
4786
|
+
total: asked.total,
|
|
4787
|
+
documents: {
|
|
4788
|
+
plan: answering,
|
|
4789
|
+
rooms: answered.rooms,
|
|
4790
|
+
beneath: asked.collection
|
|
4791
|
+
? answered.rooms.map(({ locator, key }) => ({
|
|
4792
|
+
key,
|
|
4793
|
+
roots: replicas.get(locator)?.record.typedSelection?.roots ?? [],
|
|
4794
|
+
}))
|
|
4795
|
+
: [],
|
|
4796
|
+
},
|
|
4797
|
+
conversations: named,
|
|
4798
|
+
});
|
|
4799
|
+
if (JSON.stringify(covered(answering)) !== JSON.stringify(answered))
|
|
3353
4800
|
continue;
|
|
3354
|
-
const hits = searchIndex.search(plan, selected === null ? null : selected.documents, searchedConversations, answering.rooms === null ? null : new Set(answering.rooms), plan.collection
|
|
3355
|
-
? new Map(answered.coverage.rooms.map(({ room }) => [
|
|
3356
|
-
room,
|
|
3357
|
-
replicas.get(room)?.confirmed.record.typedSelection?.roots ?? [],
|
|
3358
|
-
]))
|
|
3359
|
-
: null);
|
|
3360
4801
|
const coverage = {
|
|
3361
4802
|
rooms: answered.coverage.rooms,
|
|
3362
|
-
conversations:
|
|
3363
|
-
index: searchIndex.generation(),
|
|
4803
|
+
conversations: named,
|
|
3364
4804
|
overlay: answered.coverage.overlay,
|
|
3365
4805
|
};
|
|
3366
4806
|
return complete
|
|
@@ -3374,13 +4814,17 @@ export function composeRuntime(options) {
|
|
|
3374
4814
|
roomDraftListeners.add(listener);
|
|
3375
4815
|
void exclusive(async () => {
|
|
3376
4816
|
await openLocal();
|
|
3377
|
-
if (closed || !roomDraftListeners.has(listener))
|
|
3378
|
-
return;
|
|
3379
|
-
listener.ready = true;
|
|
3380
4817
|
}).then(() => {
|
|
3381
|
-
if (closed || !
|
|
4818
|
+
if (closed || !roomDraftListeners.has(listener))
|
|
3382
4819
|
return;
|
|
3383
|
-
|
|
4820
|
+
const snapshot = snapshotRoomDrafts();
|
|
4821
|
+
draftsShown = draftsShown.then(async () => {
|
|
4822
|
+
const value = await snapshot.catch(() => null);
|
|
4823
|
+
if (value === null || closed || !roomDraftListeners.has(listener))
|
|
4824
|
+
return;
|
|
4825
|
+
listener.ready = true;
|
|
4826
|
+
listener.notify(value);
|
|
4827
|
+
});
|
|
3384
4828
|
}, () => { });
|
|
3385
4829
|
return () => {
|
|
3386
4830
|
roomDraftListeners.delete(listener);
|
|
@@ -3413,7 +4857,7 @@ export function composeRuntime(options) {
|
|
|
3413
4857
|
};
|
|
3414
4858
|
}
|
|
3415
4859
|
if (topic === 'readiness') {
|
|
3416
|
-
return watchReadiness({ target: '', code: 'stored',
|
|
4860
|
+
return watchReadiness({ target: '', code: 'stored', space: null, search: 'unused' }, null, deliver);
|
|
3417
4861
|
}
|
|
3418
4862
|
if (topic === 'rooms') {
|
|
3419
4863
|
roomListeners.add(deliver);
|
|
@@ -3475,14 +4919,14 @@ export function composeRuntime(options) {
|
|
|
3475
4919
|
return listed();
|
|
3476
4920
|
},
|
|
3477
4921
|
query: ((input) => rowsQuery(planned(input, null))),
|
|
3478
|
-
search: (input) => searched(input, null),
|
|
4922
|
+
search: (input) => searched(input, null, []),
|
|
3479
4923
|
types(locator) {
|
|
3480
4924
|
return {
|
|
3481
4925
|
collection: modelTypes(modelOf(locator)),
|
|
3482
4926
|
typedSelection: rooms.get(locator)?.typedSelection === undefined ||
|
|
3483
4927
|
rooms.get(locator)?.typedSelection === null
|
|
3484
4928
|
? null
|
|
3485
|
-
: modelTypes(
|
|
4929
|
+
: modelTypes(builtIn),
|
|
3486
4930
|
};
|
|
3487
4931
|
},
|
|
3488
4932
|
roomDraft: {
|
|
@@ -3495,46 +4939,32 @@ export function composeRuntime(options) {
|
|
|
3495
4939
|
return listed;
|
|
3496
4940
|
});
|
|
3497
4941
|
},
|
|
3498
|
-
read(room, path) {
|
|
4942
|
+
async read(room, path) {
|
|
3499
4943
|
const record = roomDrafts.get(room)?.get(path);
|
|
3500
|
-
return record === undefined ? null :
|
|
4944
|
+
return record === undefined ? null : ((await roomDraftsOf([record]))[0] ?? null);
|
|
3501
4945
|
},
|
|
3502
|
-
write(room, path, revision, input) {
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
const
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
? undefined
|
|
3514
|
-
: await heldFile(parts.file.blob, binaryBase(replicas.get(room)?.visible, path));
|
|
3515
|
-
const record = {
|
|
3516
|
-
locator: room,
|
|
3517
|
-
path,
|
|
3518
|
-
revision: roomDraftRevision(),
|
|
3519
|
-
parts: held(current?.parts, parts, file),
|
|
3520
|
-
returned: current?.returned ?? null,
|
|
3521
|
-
};
|
|
3522
|
-
await local.keepRoomDraft(record);
|
|
3523
|
-
rememberRoomDraft(record);
|
|
3524
|
-
publishChangeSet(changeSet(room, null, null, { drafts: [path] }));
|
|
3525
|
-
return { outcome: 'written', roomDraft: roomDraftOf(record) };
|
|
3526
|
-
});
|
|
4946
|
+
write(room, path, revision, input, next = roomDraftRevision()) {
|
|
4947
|
+
if (typeof next !== 'string' || next === '')
|
|
4948
|
+
return Promise.reject(runtimeError('room-draft-malformed'));
|
|
4949
|
+
return gathered(`room-draft\u0000${room}\u0000${path}`, { revision, input, next }, (earlier, later) => {
|
|
4950
|
+
if (later.revision !== earlier.next)
|
|
4951
|
+
return null;
|
|
4952
|
+
const parts = gatheredParts(earlier.input, later.input);
|
|
4953
|
+
return parts === null
|
|
4954
|
+
? null
|
|
4955
|
+
: { revision: earlier.revision, input: parts, next: later.next };
|
|
4956
|
+
}, (written) => writtenRoomDraft(room, path, written.revision, written.input, written.next));
|
|
3527
4957
|
},
|
|
3528
4958
|
discard(room, path, revision) {
|
|
3529
4959
|
return exclusive(async () => {
|
|
3530
4960
|
const local = await openLocal();
|
|
3531
4961
|
const current = roomDrafts.get(room)?.get(path);
|
|
3532
4962
|
if (current === undefined || current.revision !== revision) {
|
|
3533
|
-
return staleWrite(current === undefined ? null :
|
|
4963
|
+
return staleWrite(current === undefined ? null : ((await roomDraftsOf([current]))[0] ?? null));
|
|
3534
4964
|
}
|
|
3535
4965
|
await local.discardRoomDraft(room, path);
|
|
3536
4966
|
forgetRoomDraft(room, path);
|
|
3537
|
-
publishChangeSet(changeSet(room,
|
|
4967
|
+
publishChangeSet(changeSet(room, { drafts: [path] }));
|
|
3538
4968
|
moveTo({ ...status, ...attention() });
|
|
3539
4969
|
return { outcome: 'written', roomDraft: null };
|
|
3540
4970
|
});
|
|
@@ -3550,13 +4980,13 @@ export function composeRuntime(options) {
|
|
|
3550
4980
|
});
|
|
3551
4981
|
},
|
|
3552
4982
|
write(app, value) {
|
|
3553
|
-
return
|
|
4983
|
+
return exclusive(() => keptPresentation(app, () => structuredClone(value)));
|
|
3554
4984
|
},
|
|
3555
4985
|
edit(app, members) {
|
|
3556
|
-
return
|
|
3557
|
-
...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(
|
|
3558
|
-
...Object.entries(
|
|
3559
|
-
]));
|
|
4986
|
+
return gathered(`presentation\u0000${app}`, structuredClone(members), (earlier, later) => ({ ...earlier, ...later }), (edited) => keptPresentation(app, (held) => Object.fromEntries([
|
|
4987
|
+
...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(edited, member)),
|
|
4988
|
+
...Object.entries(edited).filter(([, value]) => value !== null),
|
|
4989
|
+
])));
|
|
3560
4990
|
},
|
|
3561
4991
|
},
|
|
3562
4992
|
save(room, named, writtenBy) {
|
|
@@ -3568,15 +4998,22 @@ export function composeRuntime(options) {
|
|
|
3568
4998
|
history(locator, path, query) {
|
|
3569
4999
|
return readHistory(locator, path, query);
|
|
3570
5000
|
},
|
|
5001
|
+
resolve(room, path, destination) {
|
|
5002
|
+
return resolveReference(room, path, destination);
|
|
5003
|
+
},
|
|
3571
5004
|
binaryFile: {
|
|
5005
|
+
part: readPart,
|
|
3572
5006
|
read: readBinaryPath,
|
|
3573
5007
|
link: linkBinaryPath,
|
|
3574
|
-
|
|
5008
|
+
show: showBinaryPath,
|
|
5009
|
+
attach: attachBinary,
|
|
3575
5010
|
},
|
|
3576
|
-
call(name, input) {
|
|
3577
|
-
|
|
5011
|
+
async call(name, input) {
|
|
5012
|
+
await ready;
|
|
5013
|
+
const room = calledRooms(name, input).find((one) => locked(one));
|
|
5014
|
+
return room === undefined ? recoveredAction(name, input) : lockedRoom(room);
|
|
3578
5015
|
},
|
|
3579
|
-
reached(locator, roomVersion, path) {
|
|
5016
|
+
async reached(locator, roomVersion, path) {
|
|
3580
5017
|
return path === undefined
|
|
3581
5018
|
? reachedBy(locator, roomVersion)
|
|
3582
5019
|
: writtenAfter(locator, roomVersion, path);
|
|
@@ -3584,14 +5021,16 @@ export function composeRuntime(options) {
|
|
|
3584
5021
|
log() {
|
|
3585
5022
|
return journal.export();
|
|
3586
5023
|
},
|
|
5024
|
+
cleanUp,
|
|
3587
5025
|
persistence() {
|
|
3588
5026
|
return persistence === 'granted';
|
|
3589
5027
|
},
|
|
3590
5028
|
async retry() {
|
|
3591
5029
|
journal.record('retry');
|
|
3592
|
-
|
|
5030
|
+
unpreparable.clear();
|
|
3593
5031
|
queueReadiness();
|
|
3594
|
-
|
|
5032
|
+
if (full)
|
|
5033
|
+
await probed();
|
|
3595
5034
|
},
|
|
3596
5035
|
observe: observing(watch),
|
|
3597
5036
|
async reauthorize() {
|
|
@@ -3613,12 +5052,15 @@ export function composeRuntime(options) {
|
|
|
3613
5052
|
const reachable = {
|
|
3614
5053
|
md,
|
|
3615
5054
|
watch,
|
|
3616
|
-
referrerScope: (referrer) => options.adapter.referrerScope(referrer),
|
|
3617
5055
|
plan: planned,
|
|
3618
5056
|
query: (plan) => rowsQuery(plan),
|
|
3619
5057
|
watchPlan,
|
|
3620
5058
|
search: searched,
|
|
3621
5059
|
watchReadiness,
|
|
5060
|
+
locked,
|
|
5061
|
+
lockedRooms,
|
|
5062
|
+
watchLocks,
|
|
5063
|
+
located,
|
|
3622
5064
|
};
|
|
3623
5065
|
return {
|
|
3624
5066
|
md,
|
|
@@ -3633,6 +5075,11 @@ export function composeRuntime(options) {
|
|
|
3633
5075
|
journal.record(`persistence ${answer}`);
|
|
3634
5076
|
queueReadiness();
|
|
3635
5077
|
},
|
|
3636
|
-
|
|
5078
|
+
shown: screen,
|
|
5079
|
+
within: (reach, port) => {
|
|
5080
|
+
if (full)
|
|
5081
|
+
void probed();
|
|
5082
|
+
return narrowed(reachable, reach, port);
|
|
5083
|
+
},
|
|
3637
5084
|
};
|
|
3638
5085
|
}
|