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