@agentshouse/mdruntime 0.1.0-cefbc8bb8a1c0 → 0.1.0-cf04ede9fb796
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 +2 -2
- package/dist/acceptance.d.ts +6 -0
- package/dist/acceptance.d.ts.map +1 -0
- package/dist/acceptance.js +389 -0
- package/dist/actions.d.ts +20 -0
- package/dist/actions.d.ts.map +1 -0
- package/dist/actions.js +251 -0
- package/dist/adapter.d.ts +86 -67
- package/dist/adapter.d.ts.map +1 -1
- package/dist/attached-model.d.ts +3 -10
- package/dist/attached-model.d.ts.map +1 -1
- package/dist/attached-model.js +21 -56
- package/dist/bootstrap.d.ts +10 -9
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +211 -81
- package/dist/cell.d.ts +6 -0
- package/dist/cell.d.ts.map +1 -0
- package/dist/cell.js +9 -0
- package/dist/chain.d.ts +2 -1
- package/dist/chain.d.ts.map +1 -1
- package/dist/change.d.ts +1 -0
- package/dist/change.d.ts.map +1 -1
- package/dist/chat-types.d.ts +5 -0
- package/dist/chat-types.d.ts.map +1 -0
- package/dist/chat-types.js +13 -0
- package/dist/client.d.ts +21 -6
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +3 -1
- package/dist/collection.d.ts +2 -4
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +1 -17
- package/dist/compile.d.ts +6 -6
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +263 -319
- package/dist/connection.d.ts +5 -143
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +102 -5311
- package/dist/context.d.ts +2 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +7 -0
- package/dist/conversation-live.d.ts +136 -0
- package/dist/conversation-live.d.ts.map +1 -0
- package/dist/conversation-live.js +248 -0
- package/dist/conversation-row.d.ts +33 -0
- package/dist/conversation-row.d.ts.map +1 -0
- package/dist/conversation-row.js +53 -0
- package/dist/conversation.d.ts +41 -45
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +859 -870
- package/dist/discovery.d.ts +10 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/discovery.js +175 -0
- package/dist/durability.d.ts +15 -1
- package/dist/durability.d.ts.map +1 -1
- package/dist/entry.d.ts +2 -2
- package/dist/entry.d.ts.map +1 -1
- package/dist/entry.js +17 -7
- package/dist/figures.d.ts +49 -0
- package/dist/figures.d.ts.map +1 -0
- package/dist/figures.js +197 -0
- package/dist/holdings.d.ts +29 -0
- package/dist/holdings.d.ts.map +1 -0
- package/dist/holdings.js +95 -0
- package/dist/host.d.ts +4 -7
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +23 -30
- 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 +25 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/install.d.ts +56 -0
- package/dist/install.d.ts.map +1 -0
- package/dist/install.js +556 -0
- package/dist/job.d.ts +33 -0
- package/dist/job.d.ts.map +1 -0
- package/dist/job.js +54 -0
- package/dist/lifecycle.d.ts +7 -0
- package/dist/lifecycle.d.ts.map +1 -0
- package/dist/lifecycle.js +268 -0
- package/dist/listing.d.ts +53 -0
- package/dist/listing.d.ts.map +1 -0
- package/dist/listing.js +176 -0
- package/dist/live.d.ts +5 -4
- package/dist/live.d.ts.map +1 -1
- package/dist/live.js +63 -22
- package/dist/locks.d.ts +9 -0
- package/dist/locks.d.ts.map +1 -0
- package/dist/locks.js +61 -0
- package/dist/log.d.ts +1 -0
- package/dist/log.d.ts.map +1 -1
- package/dist/lost.d.ts +5 -0
- package/dist/lost.d.ts.map +1 -0
- package/dist/lost.js +61 -0
- package/dist/memory.d.ts +11 -4
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +172 -82
- package/dist/messages.d.ts +27 -3
- package/dist/messages.d.ts.map +1 -1
- package/dist/messages.js +97 -19
- package/dist/model.d.ts +2 -10
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +0 -18
- package/dist/models.d.ts +14 -0
- package/dist/models.d.ts.map +1 -0
- package/dist/models.js +195 -0
- package/dist/observation.d.ts +2 -2
- package/dist/observation.d.ts.map +1 -1
- package/dist/observation.js +7 -2
- package/dist/opening.d.ts +4 -0
- package/dist/opening.d.ts.map +1 -0
- package/dist/opening.js +57 -0
- package/dist/outcome.d.ts +4 -4
- package/dist/outcome.d.ts.map +1 -1
- package/dist/outcome.js +3 -0
- package/dist/overlay.d.ts +1 -0
- package/dist/overlay.d.ts.map +1 -1
- package/dist/overlay.js +11 -0
- package/dist/parts.d.ts +60 -0
- package/dist/parts.d.ts.map +1 -0
- package/dist/parts.js +126 -0
- package/dist/paths.d.ts +1 -0
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +22 -0
- package/dist/preparation.d.ts +20 -0
- package/dist/preparation.d.ts.map +1 -0
- package/dist/preparation.js +226 -0
- package/dist/queries.d.ts +16 -0
- package/dist/queries.d.ts.map +1 -0
- package/dist/queries.js +272 -0
- package/dist/query.d.ts +14 -209
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +3 -505
- package/dist/questions.d.ts +34 -0
- package/dist/questions.d.ts.map +1 -0
- package/dist/questions.js +122 -0
- package/dist/reach.d.ts +15 -13
- package/dist/reach.d.ts.map +1 -1
- package/dist/reach.js +183 -137
- package/dist/react.d.ts +10 -2
- package/dist/react.d.ts.map +1 -1
- package/dist/read.d.ts +7 -0
- package/dist/read.d.ts.map +1 -0
- package/dist/read.js +17 -0
- package/dist/readiness.d.ts +2 -16
- package/dist/readiness.d.ts.map +1 -1
- package/dist/readiness.js +4 -10
- package/dist/reads.d.ts +36 -10
- package/dist/reads.d.ts.map +1 -1
- package/dist/reads.js +158 -78
- package/dist/recovery.d.ts.map +1 -1
- package/dist/recovery.js +9 -1
- package/dist/refresh.d.ts +7 -5
- package/dist/refresh.d.ts.map +1 -1
- package/dist/refresh.js +33 -5
- package/dist/refusal.d.ts +5 -0
- package/dist/refusal.d.ts.map +1 -0
- package/dist/refusal.js +56 -0
- package/dist/replica.d.ts +9 -9
- package/dist/replica.d.ts.map +1 -1
- package/dist/replica.js +52 -20
- package/dist/room-draft.d.ts +4 -3
- package/dist/room-draft.d.ts.map +1 -1
- package/dist/room-draft.js +65 -52
- package/dist/room-drafts.d.ts +24 -0
- package/dist/room-drafts.d.ts.map +1 -0
- package/dist/room-drafts.js +216 -0
- package/dist/rows.d.ts +14 -21
- package/dist/rows.d.ts.map +1 -1
- package/dist/rows.js +48 -184
- package/dist/runtime.d.ts +43 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +1 -0
- package/dist/save.d.ts +5 -1
- package/dist/save.d.ts.map +1 -1
- package/dist/save.js +19 -13
- package/dist/saves.d.ts +70 -0
- package/dist/saves.d.ts.map +1 -0
- package/dist/saves.js +1008 -0
- package/dist/search.d.ts +12 -63
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +9 -52
- package/dist/seed.d.ts +11 -2
- package/dist/seed.d.ts.map +1 -1
- package/dist/seed.js +71 -0
- package/dist/shared.d.ts +1 -2
- package/dist/shared.d.ts.map +1 -1
- package/dist/shared.js +9 -15
- package/dist/shown.d.ts +31 -0
- package/dist/shown.d.ts.map +1 -0
- package/dist/shown.js +295 -0
- package/dist/socket/admission.d.ts +7 -0
- package/dist/socket/admission.d.ts.map +1 -0
- package/dist/socket/admission.js +39 -0
- package/dist/socket/answers.d.ts +6 -0
- package/dist/socket/answers.d.ts.map +1 -0
- package/dist/socket/answers.js +20 -0
- package/dist/socket/lanes.d.ts +20 -0
- package/dist/socket/lanes.d.ts.map +1 -0
- package/dist/socket/lanes.js +77 -0
- package/dist/socket/reads.d.ts +10 -0
- package/dist/socket/reads.d.ts.map +1 -0
- package/dist/socket/reads.js +31 -0
- package/dist/socket/rooms.d.ts +44 -0
- package/dist/socket/rooms.d.ts.map +1 -0
- package/dist/socket/rooms.js +90 -0
- package/dist/socket/session.d.ts +39 -0
- package/dist/socket/session.d.ts.map +1 -0
- package/dist/socket/session.js +23 -0
- package/dist/socket.d.ts +70 -0
- package/dist/socket.d.ts.map +1 -0
- package/dist/socket.js +175 -0
- package/dist/source.d.ts +61 -0
- package/dist/source.d.ts.map +1 -0
- package/dist/source.js +81 -0
- package/dist/status.d.ts +0 -6
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +0 -6
- package/dist/store/fences.d.ts +16 -0
- package/dist/store/fences.d.ts.map +1 -0
- package/dist/store/fences.js +26 -0
- package/dist/store/installs.d.ts +30 -0
- package/dist/store/installs.d.ts.map +1 -0
- package/dist/store/installs.js +34 -0
- package/dist/store/readiness.d.ts +10 -0
- package/dist/store/readiness.d.ts.map +1 -0
- package/dist/store/readiness.js +52 -0
- package/dist/store/records.d.ts +212 -0
- package/dist/store/records.d.ts.map +1 -0
- package/dist/store/records.js +431 -0
- package/dist/store/room-held.d.ts +8 -0
- package/dist/store/room-held.d.ts.map +1 -0
- package/dist/store/room-held.js +17 -0
- package/dist/store/space.d.ts +11 -0
- package/dist/store/space.d.ts.map +1 -0
- package/dist/store/space.js +43 -0
- package/dist/store/watchers.d.ts +17 -0
- package/dist/store/watchers.d.ts.map +1 -0
- package/dist/store/watchers.js +57 -0
- package/dist/store/writes.d.ts +24 -0
- package/dist/store/writes.d.ts.map +1 -0
- package/dist/store/writes.js +125 -0
- package/dist/store.d.ts +107 -157
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +143 -395
- package/dist/stored.d.ts +58 -37
- package/dist/stored.d.ts.map +1 -1
- package/dist/stored.js +276 -207
- package/dist/surface.d.ts +88 -0
- package/dist/surface.d.ts.map +1 -0
- package/dist/surface.js +223 -0
- package/dist/sync.d.ts +19 -0
- package/dist/sync.d.ts.map +1 -0
- package/dist/sync.js +532 -0
- package/dist/tables.d.ts +120 -55
- package/dist/tables.d.ts.map +1 -1
- package/dist/tables.js +212 -93
- package/dist/tallies.d.ts +18 -0
- package/dist/tallies.d.ts.map +1 -0
- package/dist/tallies.js +129 -0
- package/dist/transfer.d.ts +0 -5
- package/dist/transfer.d.ts.map +1 -1
- package/dist/validation.d.ts +3 -4
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +42 -100
- package/dist/windows.d.ts +1 -12
- package/dist/windows.d.ts.map +1 -1
- package/dist/windows.js +2 -75
- package/dist/wire.d.ts +17 -5
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +7 -1
- package/package.json +2 -2
- package/dist/fence.d.ts +0 -6
- package/dist/fence.d.ts.map +0 -1
- package/dist/fence.js +0 -10
- package/dist/fulltext.d.ts +0 -27
- package/dist/fulltext.d.ts.map +0 -1
- package/dist/fulltext.js +0 -370
- package/dist/words.d.ts +0 -19
- package/dist/words.d.ts.map +0 -1
- package/dist/words.js +0 -156
package/dist/saves.js
ADDED
|
@@ -0,0 +1,1008 @@
|
|
|
1
|
+
import { parseFrontmatter } from '@agentshouse/mdmodel';
|
|
2
|
+
import { binaryRecord } from './binary.js';
|
|
3
|
+
import { changeSet, mergeChangeSets } from './change-set.js';
|
|
4
|
+
import { leaving as leavingIn, partnerOf, queuedFile, } from './change.js';
|
|
5
|
+
import { available, composedOf } from './collection.js';
|
|
6
|
+
import { runtimeError } from './failure.js';
|
|
7
|
+
import { attachedFile } from './file.js';
|
|
8
|
+
import { roomDraftRevision } from './identity.js';
|
|
9
|
+
import { confirmedAgain } from './install.js';
|
|
10
|
+
import { compositionFor } from './listing.js';
|
|
11
|
+
import { absentBinary, attachAnswer, binaryAnswer, binaryLinkAnswer, failure, OFFLINE, UNCERTAIN, } from './outcome.js';
|
|
12
|
+
import { sameBinary } from './overlay.js';
|
|
13
|
+
import { ancestors } from './paths.js';
|
|
14
|
+
import { connectionNow, prepare } from './preparation.js';
|
|
15
|
+
import { entryAt, landed, movedBy, overlayOf, replaying, slicing, slicingWith, standing, storedRecord, touchedBy, } from './replica.js';
|
|
16
|
+
import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, moveTarget, readSavedParts, returnedBy, returnedInto, withdrawnDrafts, } from './room-draft.js';
|
|
17
|
+
import { refuse } from './refusal.js';
|
|
18
|
+
import { visibleOf } from './room-drafts.js';
|
|
19
|
+
import { documentKey } from './rows.js';
|
|
20
|
+
import { buildBatch, buildMove, joined, limiting, queuedDocuments, requeuing, } from './save.js';
|
|
21
|
+
import { pendingShown } from './shown.js';
|
|
22
|
+
import { paced, ROW_WEIGHT, stepped } from './turn.js';
|
|
23
|
+
import { validatedNeighbourhood } from './validation.js';
|
|
24
|
+
export function withdrawable(states) {
|
|
25
|
+
return (batch) => {
|
|
26
|
+
const state = states.get(batch.id);
|
|
27
|
+
return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export async function sliceAt(store, record, view, queue, extra = [], moved = []) {
|
|
31
|
+
const local = await store.open();
|
|
32
|
+
const needed = new Set([...touchedBy(queue), ...extra]);
|
|
33
|
+
const movedPaths = [...new Set([...movedBy(queue, record.locator), ...moved])];
|
|
34
|
+
if (movedPaths.length > 0) {
|
|
35
|
+
for (const { path } of await local.referrers(record.key, view, movedPaths)) {
|
|
36
|
+
needed.add(path);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const paths = [...needed];
|
|
40
|
+
return stepped(slicing(await local.entries(record.key, view, paths), paths));
|
|
41
|
+
}
|
|
42
|
+
export async function landedWhole(store, locator, batch, confirmedLayer, current) {
|
|
43
|
+
if (!landed(batch, confirmedLayer, current))
|
|
44
|
+
return false;
|
|
45
|
+
const partner = partnerOf(batch, locator);
|
|
46
|
+
if (partner === null)
|
|
47
|
+
return true;
|
|
48
|
+
const other = await store.replica(partner);
|
|
49
|
+
const half = other?.queue.find((one) => one.id === batch.id);
|
|
50
|
+
if (other === undefined || half === undefined)
|
|
51
|
+
return false;
|
|
52
|
+
return landed(half, await sliceAt(store, other.record, 'confirmed', [half]));
|
|
53
|
+
}
|
|
54
|
+
async function carriedBack(store, locator, unlanded, written) {
|
|
55
|
+
const back = new Map();
|
|
56
|
+
const taken = [];
|
|
57
|
+
for (const batch of unlanded) {
|
|
58
|
+
const between = batch.between;
|
|
59
|
+
if (between === undefined)
|
|
60
|
+
continue;
|
|
61
|
+
const source = between.source.room;
|
|
62
|
+
const destination = between.destination.room;
|
|
63
|
+
const local = await store.open();
|
|
64
|
+
const arrived = (await local.pending(destination, batch.id)).state?.drafts ?? [];
|
|
65
|
+
for (const { path: to } of arrived) {
|
|
66
|
+
const record = await store.draft(destination, to);
|
|
67
|
+
const path = between.moves.find((move) => move.to === to)?.path;
|
|
68
|
+
if (record === undefined || path === undefined)
|
|
69
|
+
continue;
|
|
70
|
+
const [into, key] = source === locator ? [written, path] : [back, documentKey(source, path)];
|
|
71
|
+
const existing = into.get(key) ?? (await store.draft(source, path));
|
|
72
|
+
taken.push({ locator: destination, path: to });
|
|
73
|
+
into.set(key, {
|
|
74
|
+
locator: source,
|
|
75
|
+
path,
|
|
76
|
+
revision: roomDraftRevision(),
|
|
77
|
+
parts: returnedInto(existing?.parts, record.parts),
|
|
78
|
+
returned: existing?.returned ?? null,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const records = [...back.values()];
|
|
83
|
+
const elsewhere = taken.filter((one) => one.locator !== locator);
|
|
84
|
+
const rooms = new Set([...records, ...elsewhere].map((one) => one.locator));
|
|
85
|
+
return {
|
|
86
|
+
taken: taken.filter((one) => one.locator === locator).map(({ path }) => path),
|
|
87
|
+
writes: [...rooms].map((room) => ({
|
|
88
|
+
locator: room,
|
|
89
|
+
roomDrafts: records.filter((record) => record.locator === room),
|
|
90
|
+
consumedRoomDrafts: elsewhere.filter((one) => one.locator === room).map(({ path }) => path),
|
|
91
|
+
})),
|
|
92
|
+
changes: [...rooms].map((room) => changeSet(room, {
|
|
93
|
+
drafts: [...records, ...elsewhere]
|
|
94
|
+
.filter((one) => one.locator === room)
|
|
95
|
+
.map(({ path }) => path),
|
|
96
|
+
})),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
export function preparedAfter(runtime, locator, answer) {
|
|
100
|
+
if (answer.indexed[locator] === false)
|
|
101
|
+
prepare(runtime, [locator]);
|
|
102
|
+
}
|
|
103
|
+
export function replayed(store, record, confirmedLayer, queue) {
|
|
104
|
+
return stepped(replaying(confirmedLayer, queue, record.locator, store.composed(record)));
|
|
105
|
+
}
|
|
106
|
+
export async function overlayDiff(record, before, visible, touched) {
|
|
107
|
+
const kept = before !== undefined && before.record.key === record.key ? before.overlaid : new Set();
|
|
108
|
+
return {
|
|
109
|
+
overlay: {
|
|
110
|
+
set: await stepped(overlayOf(visible, touched)),
|
|
111
|
+
cleared: [...kept].filter((path) => !touched.has(path)),
|
|
112
|
+
},
|
|
113
|
+
touched,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
export async function settle(runtime, local, locator, work, record, queue, settling = {}) {
|
|
117
|
+
return together(runtime, local, [
|
|
118
|
+
await settlement(runtime, local, locator, work, record, queue, settling),
|
|
119
|
+
]);
|
|
120
|
+
}
|
|
121
|
+
async function unpairedIn(store, locator, batch) {
|
|
122
|
+
const { state } = await (await store.open()).pending(locator, batch.id);
|
|
123
|
+
if (state === undefined)
|
|
124
|
+
return false;
|
|
125
|
+
return !leavingIn(batch, locator) || withdrawable(new Map([[batch.id, state]]))(batch);
|
|
126
|
+
}
|
|
127
|
+
export async function together(runtime, local, settlements) {
|
|
128
|
+
const settled = new Set(settlements.map(({ locator }) => locator));
|
|
129
|
+
const all = [...settlements];
|
|
130
|
+
for (let wave = settlements; wave.length > 0;) {
|
|
131
|
+
const partnered = new Map();
|
|
132
|
+
for (const { locator, left } of wave) {
|
|
133
|
+
for (const batch of left) {
|
|
134
|
+
const partner = partnerOf(batch, locator);
|
|
135
|
+
if (partner === null ||
|
|
136
|
+
settled.has(partner) ||
|
|
137
|
+
!(await unpairedIn(runtime.store, partner, batch))) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
partnered.set(partner, [...(partnered.get(partner) ?? []), batch]);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const partners = [];
|
|
144
|
+
for (const [partner, batches] of partnered) {
|
|
145
|
+
settled.add(partner);
|
|
146
|
+
partners.push(await unpairing(runtime, local, partner, batches));
|
|
147
|
+
}
|
|
148
|
+
all.push(...partners);
|
|
149
|
+
wave = partners;
|
|
150
|
+
}
|
|
151
|
+
const answer = await local.commit({
|
|
152
|
+
rooms: all.flatMap(({ commit }) => commit.rooms),
|
|
153
|
+
writes: all.flatMap(({ commit }) => commit.writes),
|
|
154
|
+
});
|
|
155
|
+
return mergeChangeSets(all.map((one) => one.apply(answer)));
|
|
156
|
+
}
|
|
157
|
+
export async function settlement(runtime, local, locator, work, record, queue, settling = {}) {
|
|
158
|
+
const { store, journal, localSaves } = runtime;
|
|
159
|
+
const before = work.replica;
|
|
160
|
+
const view = settling.view ?? 'confirmed';
|
|
161
|
+
const previousQueue = work.queue;
|
|
162
|
+
const overrides = settling.changes;
|
|
163
|
+
const read = await sliceAt(store, record, view, queue, [
|
|
164
|
+
...(before !== undefined && before.record.key === record.key ? before.overlaid : []),
|
|
165
|
+
...(overrides?.set.map(({ path }) => path) ?? []),
|
|
166
|
+
...(overrides?.removed ?? []),
|
|
167
|
+
]);
|
|
168
|
+
const confirmedLayer = overrides === undefined
|
|
169
|
+
? read
|
|
170
|
+
: await stepped(slicingWith(read, overrides.set, overrides.removed));
|
|
171
|
+
const composition = store.composed(record);
|
|
172
|
+
const shown = queue.filter((batch) => {
|
|
173
|
+
const held = store.landed.of(locator, batch.id);
|
|
174
|
+
return (held !== undefined &&
|
|
175
|
+
Object.entries(held).some(([path, revision]) => confirmedLayer.revisions[path] !== revision));
|
|
176
|
+
});
|
|
177
|
+
const unshown = queue.filter((batch) => !shown.includes(batch));
|
|
178
|
+
const { withdrawn, kept } = await stepped(standing(confirmedLayer, unshown, withdrawable(work.states), localSaves, locator, composition));
|
|
179
|
+
const founded = kept.filter((batch) => !unshown.includes(batch));
|
|
180
|
+
const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
|
|
181
|
+
const unqueued = shown.map((batch) => ({ batch, landed: true }));
|
|
182
|
+
for (const batch of withdrawn) {
|
|
183
|
+
unqueued.push({ batch, landed: await landedWhole(store, locator, batch, confirmedLayer) });
|
|
184
|
+
}
|
|
185
|
+
const returning = [
|
|
186
|
+
...[...unqueued].reverse().map(({ batch, landed: held }) => ({
|
|
187
|
+
batch,
|
|
188
|
+
returned: localSaves || held ? null : returnedBy('withdrawn'),
|
|
189
|
+
})),
|
|
190
|
+
...(settling.leaving ?? []),
|
|
191
|
+
];
|
|
192
|
+
const written = new Map();
|
|
193
|
+
for (const { batch, returned: reason } of returning) {
|
|
194
|
+
if (reason === null || batch.between?.destination.room === locator)
|
|
195
|
+
continue;
|
|
196
|
+
for (const saved of work.states.get(batch.id)?.drafts ?? []) {
|
|
197
|
+
const existing = written.get(saved.path) ?? (await store.draft(locator, saved.path));
|
|
198
|
+
written.set(saved.path, {
|
|
199
|
+
locator,
|
|
200
|
+
path: saved.path,
|
|
201
|
+
revision: roomDraftRevision(),
|
|
202
|
+
parts: returnedInto(existing?.parts, saved.parts),
|
|
203
|
+
returned: reason,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const carried = await carriedBack(store, locator, [
|
|
208
|
+
...[...unqueued].reverse().flatMap(({ batch, landed: held }) => (held ? [] : [batch])),
|
|
209
|
+
...(settling.leaving ?? []).flatMap(({ batch, returned }) => returned === null ? [] : [batch]),
|
|
210
|
+
], written);
|
|
211
|
+
const replayedLayer = await replayed(store, record, confirmedLayer, kept);
|
|
212
|
+
const { overlay } = await overlayDiff(record, before, replayedLayer.visible, replayedLayer.touched);
|
|
213
|
+
const commit = {
|
|
214
|
+
rooms: [
|
|
215
|
+
{
|
|
216
|
+
locator,
|
|
217
|
+
key: record.key,
|
|
218
|
+
record: storedRecord(record),
|
|
219
|
+
roots: record.typedSelection?.roots ?? [],
|
|
220
|
+
composed: composedOf(composition),
|
|
221
|
+
...settling.commit,
|
|
222
|
+
...(overrides === undefined
|
|
223
|
+
? {}
|
|
224
|
+
: { confirmed: { set: overrides.set, removed: overrides.removed } }),
|
|
225
|
+
overlay,
|
|
226
|
+
},
|
|
227
|
+
],
|
|
228
|
+
writes: [
|
|
229
|
+
{
|
|
230
|
+
locator,
|
|
231
|
+
queue: kept,
|
|
232
|
+
stored: previousQueue,
|
|
233
|
+
founded,
|
|
234
|
+
roomDrafts: [...written.values()],
|
|
235
|
+
consumedRoomDrafts: carried.taken,
|
|
236
|
+
},
|
|
237
|
+
...carried.writes,
|
|
238
|
+
],
|
|
239
|
+
};
|
|
240
|
+
const staying = new Set(kept.map((batch) => batch.id));
|
|
241
|
+
return {
|
|
242
|
+
locator,
|
|
243
|
+
left: previousQueue.filter((batch) => !staying.has(batch.id)),
|
|
244
|
+
commit,
|
|
245
|
+
apply: (answer) => {
|
|
246
|
+
for (const batch of previousQueue) {
|
|
247
|
+
if (!staying.has(batch.id))
|
|
248
|
+
store.landed.drop(locator, batch.id);
|
|
249
|
+
}
|
|
250
|
+
const drafts = [...written.keys(), ...carried.taken];
|
|
251
|
+
for (const { batch, landed: held } of unqueued) {
|
|
252
|
+
const outcome = localSaves ? 'dropped' : held ? 'landed' : 'withdrawn';
|
|
253
|
+
journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
|
|
254
|
+
}
|
|
255
|
+
const changed = answer.changed[locator];
|
|
256
|
+
if (settling.commit?.published !== undefined ||
|
|
257
|
+
settling.commit?.refreshed === true ||
|
|
258
|
+
overrides !== undefined) {
|
|
259
|
+
confirmedAgain(store, locator);
|
|
260
|
+
}
|
|
261
|
+
preparedAfter(runtime, locator, answer);
|
|
262
|
+
return mergeChangeSets([
|
|
263
|
+
changeSet(locator, {
|
|
264
|
+
documents: changed === undefined ? [] : changed,
|
|
265
|
+
room: settling.room,
|
|
266
|
+
queue: queueChanged,
|
|
267
|
+
drafts,
|
|
268
|
+
}),
|
|
269
|
+
...carried.changes,
|
|
270
|
+
]);
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
export async function unpairing(runtime, local, locator, batches) {
|
|
275
|
+
const { store, journal, localSaves, submitLater } = runtime;
|
|
276
|
+
const ids = new Set(batches.map(({ id }) => id));
|
|
277
|
+
const leaving = batches.map((batch) => ({
|
|
278
|
+
batch,
|
|
279
|
+
returned: !localSaves && leavingIn(batch, locator) ? returnedBy('withdrawn') : null,
|
|
280
|
+
}));
|
|
281
|
+
const outcome = localSaves ? 'dropped' : 'unpaired';
|
|
282
|
+
const work = await store.work(locator);
|
|
283
|
+
const current = work.replica;
|
|
284
|
+
if (current !== undefined) {
|
|
285
|
+
const settled = await settlement(runtime, local, locator, work, current.record, current.queue.filter((one) => !ids.has(one.id)), { leaving });
|
|
286
|
+
return {
|
|
287
|
+
...settled,
|
|
288
|
+
apply(answer) {
|
|
289
|
+
for (const batch of batches)
|
|
290
|
+
journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
|
|
291
|
+
const set = settled.apply(answer);
|
|
292
|
+
submitLater(locator);
|
|
293
|
+
return set;
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
const queue = work.queue.filter((one) => !ids.has(one.id));
|
|
298
|
+
const written = new Map();
|
|
299
|
+
for (const { batch, returned } of leaving) {
|
|
300
|
+
if (returned === null)
|
|
301
|
+
continue;
|
|
302
|
+
for (const saved of work.states.get(batch.id)?.drafts ?? []) {
|
|
303
|
+
written.set(saved.path, {
|
|
304
|
+
locator,
|
|
305
|
+
path: saved.path,
|
|
306
|
+
revision: roomDraftRevision(),
|
|
307
|
+
parts: returnedInto((written.get(saved.path) ?? (await store.draft(locator, saved.path)))?.parts, saved.parts),
|
|
308
|
+
returned,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
const carried = await carriedBack(store, locator, leaving.flatMap(({ batch, returned }) => (returned === null ? [] : [batch])), written);
|
|
313
|
+
return {
|
|
314
|
+
locator,
|
|
315
|
+
left: batches,
|
|
316
|
+
commit: {
|
|
317
|
+
rooms: [],
|
|
318
|
+
writes: [
|
|
319
|
+
{
|
|
320
|
+
locator,
|
|
321
|
+
queue,
|
|
322
|
+
stored: work.queue,
|
|
323
|
+
roomDrafts: [...written.values()],
|
|
324
|
+
consumedRoomDrafts: carried.taken,
|
|
325
|
+
},
|
|
326
|
+
...carried.writes,
|
|
327
|
+
],
|
|
328
|
+
},
|
|
329
|
+
apply() {
|
|
330
|
+
for (const batch of batches)
|
|
331
|
+
journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
|
|
332
|
+
submitLater(locator);
|
|
333
|
+
return mergeChangeSets([
|
|
334
|
+
changeSet(locator, {
|
|
335
|
+
queue: true,
|
|
336
|
+
drafts: [...written.keys(), ...carried.taken],
|
|
337
|
+
}),
|
|
338
|
+
...carried.changes,
|
|
339
|
+
]);
|
|
340
|
+
},
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
async function savedRoom(store, locator) {
|
|
344
|
+
const local = await store.open();
|
|
345
|
+
const replica = await store.replica(locator);
|
|
346
|
+
if (replica === undefined || store.fences.has(locator)) {
|
|
347
|
+
throw runtimeError('unknown-room', `Unknown Room ${locator}`);
|
|
348
|
+
}
|
|
349
|
+
return { local, replica };
|
|
350
|
+
}
|
|
351
|
+
export async function saveRoomDrafts(runtime, locator, named, writtenBy) {
|
|
352
|
+
const { store } = runtime;
|
|
353
|
+
const target = await savedRoom(store, locator);
|
|
354
|
+
const drafts = new Map((await store.drafts(locator, named.map(({ path }) => path))).map((record) => [record.path, record]));
|
|
355
|
+
const saved = [];
|
|
356
|
+
for (const { path, revision } of named) {
|
|
357
|
+
const record = drafts.get(path);
|
|
358
|
+
if (record === undefined || record.revision !== revision) {
|
|
359
|
+
return { outcome: 'refused', code: 'stale', retryable: false, file: path };
|
|
360
|
+
}
|
|
361
|
+
if (!saved.some((draft) => draft.path === path))
|
|
362
|
+
saved.push({ path, parts: record.parts });
|
|
363
|
+
}
|
|
364
|
+
return committed(runtime, locator, target, saved, new Set(saved.map(({ path }) => path)), writtenBy);
|
|
365
|
+
}
|
|
366
|
+
export async function saveParts(runtime, locator, named, writtenBy) {
|
|
367
|
+
const { store } = runtime;
|
|
368
|
+
const read = readSavedParts(named);
|
|
369
|
+
const target = await savedRoom(store, locator);
|
|
370
|
+
const saved = [];
|
|
371
|
+
const files = [];
|
|
372
|
+
const filed = read.filter(({ parts }) => parts.file !== undefined).map(({ path }) => path);
|
|
373
|
+
const visible = filed.length === 0 ? undefined : await visibleOf(store, locator, filed);
|
|
374
|
+
for (const { path, parts } of read) {
|
|
375
|
+
const file = parts.file === undefined
|
|
376
|
+
? undefined
|
|
377
|
+
: await heldFile(parts.file.blob, parts.file.base === undefined ? binaryBase(visible, path) : parts.file.base);
|
|
378
|
+
if (parts.file !== undefined && file !== undefined) {
|
|
379
|
+
files.push({ key: file.localId, blob: parts.file.blob });
|
|
380
|
+
}
|
|
381
|
+
saved.push({ path, parts: held(undefined, parts, file) });
|
|
382
|
+
}
|
|
383
|
+
return committed(runtime, locator, target, saved, new Set(), writtenBy, files);
|
|
384
|
+
}
|
|
385
|
+
async function carriedDrafts(store, locator, saved, consumed) {
|
|
386
|
+
if (!saved.some(({ parts }) => parts.move !== undefined))
|
|
387
|
+
return new Map();
|
|
388
|
+
const holding = new Map((await store.drafts(locator)).map((record) => [record.path, record]));
|
|
389
|
+
return new Map(saved.flatMap(({ path, parts }) => {
|
|
390
|
+
const record = holding.get(path);
|
|
391
|
+
if (parts.move === undefined || record === undefined || consumed.has(path))
|
|
392
|
+
return [];
|
|
393
|
+
const to = moveTarget(parts.move);
|
|
394
|
+
if (to === path || holding.has(to))
|
|
395
|
+
return [];
|
|
396
|
+
return [[path, { ...record, path: to, revision: roomDraftRevision() }]];
|
|
397
|
+
}));
|
|
398
|
+
}
|
|
399
|
+
async function savingIn(store, replica, changes, extra, leaving = []) {
|
|
400
|
+
const record = replica.record;
|
|
401
|
+
const local = await store.open();
|
|
402
|
+
const paths = [
|
|
403
|
+
...extra,
|
|
404
|
+
...changes.flatMap((change) => change.kind === 'move' ? [change.path, change.to] : [change.path]),
|
|
405
|
+
];
|
|
406
|
+
const moved = [
|
|
407
|
+
...leaving,
|
|
408
|
+
...changes.flatMap((change) => (change.kind === 'move' ? [change.path] : [])),
|
|
409
|
+
];
|
|
410
|
+
const removed = changes.flatMap((change) => (change.kind === 'remove' ? [change.path] : []));
|
|
411
|
+
const referring = removed.length === 0
|
|
412
|
+
? []
|
|
413
|
+
: (await local.referrers(record.key, 'confirmed', removed)).map(({ path }) => path);
|
|
414
|
+
const confirmedLayer = await sliceAt(store, record, 'confirmed', replica.queue, [...paths, ...paths.flatMap(ancestors), ...referring], moved);
|
|
415
|
+
const { visible } = await replayed(store, record, confirmedLayer, replica.queue);
|
|
416
|
+
const composition = store.composed(record);
|
|
417
|
+
return {
|
|
418
|
+
record,
|
|
419
|
+
confirmed: confirmedLayer,
|
|
420
|
+
visible,
|
|
421
|
+
folders: new Set(await local.folders(record.key, 'visible', paths)),
|
|
422
|
+
*visibleOver(queue) {
|
|
423
|
+
return (yield* replaying(confirmedLayer, queue, record.locator, composition)).visible;
|
|
424
|
+
},
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
async function unmodeledIn(runtime, record) {
|
|
428
|
+
const composition = compositionFor(runtime, record);
|
|
429
|
+
if (available(composition))
|
|
430
|
+
return false;
|
|
431
|
+
const local = await runtime.store.open();
|
|
432
|
+
const roots = record.typedSelection?.roots ?? [];
|
|
433
|
+
return local.typedWithin(record.key, 'visible', roots, false);
|
|
434
|
+
}
|
|
435
|
+
function textDiffers(left, right) {
|
|
436
|
+
return ((left?.content ?? undefined) !== (right?.content ?? undefined) ||
|
|
437
|
+
!sameBinary(left?.binary ?? undefined, right?.binary ?? undefined));
|
|
438
|
+
}
|
|
439
|
+
function textOf(entry) {
|
|
440
|
+
if (entry === null)
|
|
441
|
+
return undefined;
|
|
442
|
+
return entry.binary !== null ? null : entry.content;
|
|
443
|
+
}
|
|
444
|
+
function lookupOf(local, key, view) {
|
|
445
|
+
return async (paths) => {
|
|
446
|
+
if (paths.length === 0)
|
|
447
|
+
return new Map();
|
|
448
|
+
return new Map((await local.typed(key, view, paths)).map(({ path, type, binary }) => [
|
|
449
|
+
path,
|
|
450
|
+
{ type: type ?? undefined, binary },
|
|
451
|
+
]));
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
function declaredOf(entry) {
|
|
455
|
+
if (entry.binary !== null)
|
|
456
|
+
return { type: undefined, binary: true };
|
|
457
|
+
const read = parseFrontmatter(entry.content ?? '');
|
|
458
|
+
return { type: read.ok ? (read.frontmatter.type ?? 'any') : undefined, binary: false };
|
|
459
|
+
}
|
|
460
|
+
function overriddenLookup(lookup, overrides) {
|
|
461
|
+
return async (paths) => {
|
|
462
|
+
const known = new Map(await lookup(paths.filter((path) => !overrides.has(path))));
|
|
463
|
+
for (const path of paths) {
|
|
464
|
+
const entry = overrides.get(path);
|
|
465
|
+
if (entry === undefined)
|
|
466
|
+
continue;
|
|
467
|
+
if (entry === null)
|
|
468
|
+
known.delete(path);
|
|
469
|
+
else
|
|
470
|
+
known.set(path, declaredOf(entry));
|
|
471
|
+
}
|
|
472
|
+
return known;
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
function validatorOf(runtime, record, confirmedLayer) {
|
|
476
|
+
return async (resulting, touched) => {
|
|
477
|
+
const local = await runtime.store.open();
|
|
478
|
+
const changes = [];
|
|
479
|
+
const overrides = new Map();
|
|
480
|
+
const pace = paced();
|
|
481
|
+
for (const path of new Set([...confirmedLayer.held, ...touched])) {
|
|
482
|
+
await pace(ROW_WEIGHT);
|
|
483
|
+
const was = entryAt(confirmedLayer, path);
|
|
484
|
+
const now = entryAt(resulting, path);
|
|
485
|
+
if (!textDiffers(was, now))
|
|
486
|
+
continue;
|
|
487
|
+
overrides.set(path, now);
|
|
488
|
+
if (textOf(was) !== textOf(now))
|
|
489
|
+
changes.push({ path, was: textOf(was), now: textOf(now) });
|
|
490
|
+
}
|
|
491
|
+
return validatedNeighbourhood(changes, compositionFor(runtime, record), {
|
|
492
|
+
lookup: overriddenLookup(lookupOf(local, record.key, 'confirmed'), overrides),
|
|
493
|
+
texts: textsOf(local, record.key, 'confirmed'),
|
|
494
|
+
referrers: referrersOf(local, record.key, 'confirmed'),
|
|
495
|
+
});
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
function textsOf(local, key, view) {
|
|
499
|
+
return async (paths) => new Map((await local.entries(key, view, paths)).map(({ path, content, binary }) => [
|
|
500
|
+
path,
|
|
501
|
+
binary !== null ? null : content,
|
|
502
|
+
]));
|
|
503
|
+
}
|
|
504
|
+
function referrersOf(local, key, view) {
|
|
505
|
+
return async (targets) => [
|
|
506
|
+
...new Set((await local.referrers(key, view, targets)).map(({ path }) => path)),
|
|
507
|
+
];
|
|
508
|
+
}
|
|
509
|
+
async function sideOf(runtime, locator, replica, saving) {
|
|
510
|
+
const record = replica.record;
|
|
511
|
+
return {
|
|
512
|
+
room: locator,
|
|
513
|
+
handle: record.handle,
|
|
514
|
+
baseRoomVersion: record.roomVersion,
|
|
515
|
+
composition: compositionFor(runtime, record),
|
|
516
|
+
operations: record.operations,
|
|
517
|
+
providerOwnedPaths: record.providerOwnedPaths,
|
|
518
|
+
authoredPaths: record.authoredPaths,
|
|
519
|
+
visible: saving.visible,
|
|
520
|
+
folders: saving.folders,
|
|
521
|
+
queued: queuedDocuments(replica.queue),
|
|
522
|
+
unmodeled: await unmodeledIn(runtime, record),
|
|
523
|
+
validate: validatorOf(runtime, record, saving.confirmed),
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
function movedInto(locator, saved) {
|
|
527
|
+
const into = saved.find(({ parts }) => parts.move?.room !== undefined && parts.move.room !== locator);
|
|
528
|
+
return into?.parts.move?.room ?? null;
|
|
529
|
+
}
|
|
530
|
+
async function saveCommit(store, local, files, rooms) {
|
|
531
|
+
const commits = [];
|
|
532
|
+
for (const { locator, saving, before, queue } of rooms) {
|
|
533
|
+
const replayedLayer = await replayed(store, saving.record, saving.confirmed, queue);
|
|
534
|
+
const diff = await overlayDiff(saving.record, before, replayedLayer.visible, replayedLayer.touched);
|
|
535
|
+
commits.push({
|
|
536
|
+
locator,
|
|
537
|
+
key: saving.record.key,
|
|
538
|
+
roots: saving.record.typedSelection?.roots ?? [],
|
|
539
|
+
composed: composedOf(store.composed(saving.record)),
|
|
540
|
+
overlay: diff.overlay,
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
try {
|
|
544
|
+
return await local.commit({
|
|
545
|
+
rooms: commits,
|
|
546
|
+
writes: rooms.map(({ write }) => write),
|
|
547
|
+
files,
|
|
548
|
+
whole: true,
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
catch {
|
|
552
|
+
return null;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
async function committedMove(runtime, locator, destination, { local, replica }, saved, consumed, writtenBy, files) {
|
|
556
|
+
const { store, localSaves } = runtime;
|
|
557
|
+
const mixed = saved.find(({ parts }) => Object.keys(parts).length !== 1 || parts.move?.room !== destination);
|
|
558
|
+
if (mixed !== undefined) {
|
|
559
|
+
return { outcome: 'refused', code: 'mixed-room-move', retryable: false, file: mixed.path };
|
|
560
|
+
}
|
|
561
|
+
const moves = saved.map(({ path, parts }) => ({ path, to: moveTarget(parts.move) }));
|
|
562
|
+
const sourceSaving = await savingIn(store, replica, moves.map(({ path }) => ({ kind: 'remove', path })), saved.map(({ path }) => path), moves.map(({ path }) => path));
|
|
563
|
+
const changed = saved.flatMap(({ path, parts }) => changedParts(path, parts, sourceSaving.visible));
|
|
564
|
+
if (changed.length > 0)
|
|
565
|
+
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
566
|
+
const target = await store.replica(destination);
|
|
567
|
+
if (target === undefined || store.fences.has(destination)) {
|
|
568
|
+
throw runtimeError('unknown-room', `Unknown Room ${destination}`);
|
|
569
|
+
}
|
|
570
|
+
const arriving = await savingIn(store, target, [], moves.map(({ to }) => to));
|
|
571
|
+
const built = await buildMove({
|
|
572
|
+
source: await sideOf(runtime, locator, replica, sourceSaving),
|
|
573
|
+
destination: await sideOf(runtime, destination, target, arriving),
|
|
574
|
+
moves,
|
|
575
|
+
providerLimits: (await local.facts()).providerLimits,
|
|
576
|
+
pathRule: runtime.pathRule,
|
|
577
|
+
local: localSaves,
|
|
578
|
+
...(writtenBy === undefined ? {} : { writtenBy }),
|
|
579
|
+
});
|
|
580
|
+
if (built.kind === 'refused')
|
|
581
|
+
return { outcome: 'refused', ...built.refusal };
|
|
582
|
+
const id = built.leaving.id;
|
|
583
|
+
const leavingDrafts = new Map((await store.drafts(locator, saved.map(({ path }) => path))).map((record) => [record.path, record]));
|
|
584
|
+
const arrivingDrafts = new Set((await store.drafts(destination, moves.map(({ to }) => to))).map(({ path }) => path));
|
|
585
|
+
const carrying = saved.flatMap(({ path, parts }) => {
|
|
586
|
+
const record = leavingDrafts.get(path);
|
|
587
|
+
const to = moveTarget(parts.move);
|
|
588
|
+
if (record === undefined || consumed.has(path) || arrivingDrafts.has(to)) {
|
|
589
|
+
return [];
|
|
590
|
+
}
|
|
591
|
+
const moved = {
|
|
592
|
+
...record,
|
|
593
|
+
locator: destination,
|
|
594
|
+
path: to,
|
|
595
|
+
revision: roomDraftRevision(),
|
|
596
|
+
};
|
|
597
|
+
return [{ from: path, record: moved }];
|
|
598
|
+
});
|
|
599
|
+
const carried = carrying.map(({ record }) => record);
|
|
600
|
+
const marked = carried.map(({ path }) => ({ path, parts: {} }));
|
|
601
|
+
const left = [...consumed, ...carrying.map(({ from }) => from)];
|
|
602
|
+
const leavingQueue = [...replica.queue, built.leaving];
|
|
603
|
+
const arrivingQueue = [...target.queue, built.arriving];
|
|
604
|
+
const answer = await saveCommit(store, local, files, [
|
|
605
|
+
{
|
|
606
|
+
locator,
|
|
607
|
+
saving: sourceSaving,
|
|
608
|
+
before: replica,
|
|
609
|
+
queue: leavingQueue,
|
|
610
|
+
write: {
|
|
611
|
+
locator,
|
|
612
|
+
queue: leavingQueue,
|
|
613
|
+
stored: replica.queue,
|
|
614
|
+
saved: new Map([[id, saved]]),
|
|
615
|
+
consumedRoomDrafts: left,
|
|
616
|
+
},
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
locator: destination,
|
|
620
|
+
saving: arriving,
|
|
621
|
+
before: target,
|
|
622
|
+
queue: arrivingQueue,
|
|
623
|
+
write: {
|
|
624
|
+
locator: destination,
|
|
625
|
+
queue: arrivingQueue,
|
|
626
|
+
stored: target.queue,
|
|
627
|
+
saved: new Map([[id, marked]]),
|
|
628
|
+
roomDrafts: carried,
|
|
629
|
+
},
|
|
630
|
+
},
|
|
631
|
+
]);
|
|
632
|
+
if (answer === null)
|
|
633
|
+
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
634
|
+
if (answer.fenced !== null) {
|
|
635
|
+
throw runtimeError('unknown-room', `Unknown Room ${answer.fenced}`);
|
|
636
|
+
}
|
|
637
|
+
if (store.fences.has(locator) || store.fences.has(destination)) {
|
|
638
|
+
throw runtimeError('unknown-room', `Unknown Room ${locator}`);
|
|
639
|
+
}
|
|
640
|
+
preparedAfter(runtime, locator, answer);
|
|
641
|
+
preparedAfter(runtime, destination, answer);
|
|
642
|
+
const shownNow = store.changed(mergeChangeSets([
|
|
643
|
+
changeSet(locator, {
|
|
644
|
+
documents: answer.changed[locator] ?? [],
|
|
645
|
+
queue: true,
|
|
646
|
+
drafts: left,
|
|
647
|
+
}),
|
|
648
|
+
changeSet(destination, {
|
|
649
|
+
documents: answer.changed[destination] ?? [],
|
|
650
|
+
queue: true,
|
|
651
|
+
drafts: carried.map(({ path }) => path),
|
|
652
|
+
}),
|
|
653
|
+
]));
|
|
654
|
+
await Promise.all([shownNow, queuedSave(runtime, locator)]);
|
|
655
|
+
return { outcome: 'saved', identifier: id };
|
|
656
|
+
}
|
|
657
|
+
function queuedSave(runtime, locator) {
|
|
658
|
+
const shownNow = pendingShown(runtime);
|
|
659
|
+
runtime.submitLater(locator);
|
|
660
|
+
return shownNow;
|
|
661
|
+
}
|
|
662
|
+
async function committed(runtime, locator, target, saved, consumed, writtenBy, files = []) {
|
|
663
|
+
const { store, localSaves } = runtime;
|
|
664
|
+
if (store.space.full())
|
|
665
|
+
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
666
|
+
const into = movedInto(locator, saved);
|
|
667
|
+
if (into !== null) {
|
|
668
|
+
return committedMove(runtime, locator, into, target, saved, consumed, writtenBy, files);
|
|
669
|
+
}
|
|
670
|
+
const { local, replica } = target;
|
|
671
|
+
const { providerLimits } = await local.facts();
|
|
672
|
+
const parts = saved.map(({ path, parts: held }) => changesOf(path, held));
|
|
673
|
+
const changes = parts.flatMap(({ changes: made }) => made);
|
|
674
|
+
const saving = await savingIn(store, replica, changes, [
|
|
675
|
+
...saved.map(({ path }) => path),
|
|
676
|
+
...parts.flatMap(({ bases }) => bases.map(({ path }) => path)),
|
|
677
|
+
]);
|
|
678
|
+
const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, held, saving.visible));
|
|
679
|
+
if (changed.length > 0)
|
|
680
|
+
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
681
|
+
const requeue = await stepped(requeuing(replica.queue, changes, saving.visible, withdrawable(replica.states), saving.visibleOver));
|
|
682
|
+
if (requeue.kind === 'refused')
|
|
683
|
+
return { outcome: 'refused', ...requeue.refusal };
|
|
684
|
+
const foreign = [...requeue.withdrawn.keys()].some((origin) => {
|
|
685
|
+
const writer = replica.queue.find((batch) => batch.id === origin)?.writtenBy;
|
|
686
|
+
return writer?.app !== writtenBy?.app || writer?.release !== writtenBy?.release;
|
|
687
|
+
});
|
|
688
|
+
if (foreign) {
|
|
689
|
+
return {
|
|
690
|
+
outcome: 'refused',
|
|
691
|
+
code: 'unknown-revision',
|
|
692
|
+
retryable: false,
|
|
693
|
+
file: [...requeue.queued].at(-1),
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
const withdrawing = requeue.withdrawn.size > 0;
|
|
697
|
+
const input = {
|
|
698
|
+
local: localSaves,
|
|
699
|
+
baseRoomVersion: replica.record.roomVersion,
|
|
700
|
+
composition: compositionFor(runtime, replica.record),
|
|
701
|
+
operations: replica.record.operations,
|
|
702
|
+
providerOwnedPaths: replica.record.providerOwnedPaths,
|
|
703
|
+
authoredPaths: replica.record.authoredPaths,
|
|
704
|
+
providerLimits,
|
|
705
|
+
pathRule: runtime.pathRule,
|
|
706
|
+
handle: replica.record.handle,
|
|
707
|
+
visible: saving.visible,
|
|
708
|
+
folders: saving.folders,
|
|
709
|
+
changes,
|
|
710
|
+
bases: parts.flatMap(({ bases }) => bases),
|
|
711
|
+
queued: queuedDocuments(replica.queue),
|
|
712
|
+
writtenBy,
|
|
713
|
+
unmodeled: await unmodeledIn(runtime, replica.record),
|
|
714
|
+
validate: validatorOf(runtime, replica.record, saving.confirmed),
|
|
715
|
+
};
|
|
716
|
+
const checked = withdrawing
|
|
717
|
+
? await buildBatch({ ...input, queued: new Set([...input.queued, ...requeue.queued]) })
|
|
718
|
+
: null;
|
|
719
|
+
if (checked?.kind === 'refused')
|
|
720
|
+
return { outcome: 'refused', ...checked.refusal };
|
|
721
|
+
const owners = [...requeue.owners].flatMap((origin) => {
|
|
722
|
+
const batch = requeue.withdrawn.get(origin);
|
|
723
|
+
return batch === undefined || batch === null ? [] : [{ origin, batch }];
|
|
724
|
+
});
|
|
725
|
+
if (owners.length > 1) {
|
|
726
|
+
return {
|
|
727
|
+
outcome: 'refused',
|
|
728
|
+
code: 'unknown-revision',
|
|
729
|
+
retryable: false,
|
|
730
|
+
file: [...requeue.queued].at(-1),
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
const built = !withdrawing
|
|
734
|
+
? await buildBatch(input)
|
|
735
|
+
: requeue.changes.length === 0
|
|
736
|
+
? null
|
|
737
|
+
: await buildBatch({
|
|
738
|
+
...input,
|
|
739
|
+
visible: await stepped(saving.visibleOver(requeue.queue)),
|
|
740
|
+
changes: requeue.changes,
|
|
741
|
+
queued: queuedDocuments(requeue.queue),
|
|
742
|
+
});
|
|
743
|
+
if (built?.kind === 'refused')
|
|
744
|
+
return { outcome: 'refused', ...built.refusal };
|
|
745
|
+
const [owner] = owners;
|
|
746
|
+
const join = built === null || owner === undefined ? null : joined(requeue.queue, owner.batch, built.batch);
|
|
747
|
+
if (join?.kind === 'refused')
|
|
748
|
+
return { outcome: 'refused', ...join.refusal };
|
|
749
|
+
const withdrawn = new Map(requeue.withdrawn);
|
|
750
|
+
if (join !== null && owner !== undefined)
|
|
751
|
+
withdrawn.set(owner.origin, join.batch);
|
|
752
|
+
const drafted = new Map();
|
|
753
|
+
for (const [origin, replaced] of withdrawn) {
|
|
754
|
+
if (replaced === null)
|
|
755
|
+
continue;
|
|
756
|
+
const carried = withdrawnDrafts(replica.states.get(origin)?.drafts ?? [], changes);
|
|
757
|
+
drafted.set(replaced.id, replaced === join?.batch
|
|
758
|
+
? [
|
|
759
|
+
...carried.map((held) => joinedDraft(held, saved)),
|
|
760
|
+
...saved.filter((draft) => !carried.some((held) => held.path === draft.path)),
|
|
761
|
+
]
|
|
762
|
+
: carried);
|
|
763
|
+
}
|
|
764
|
+
if (built !== null && join === null)
|
|
765
|
+
drafted.set(built.batch.id, saved);
|
|
766
|
+
const batches = join !== null ? join.queue : built === null ? requeue.queue : [...requeue.queue, built.batch];
|
|
767
|
+
for (const replaced of withdrawn.values()) {
|
|
768
|
+
if (replaced === null || !batches.includes(replaced))
|
|
769
|
+
continue;
|
|
770
|
+
const limited = await stepped(limiting(batches, replaced, providerLimits, saving.visibleOver));
|
|
771
|
+
if (limited !== null)
|
|
772
|
+
return { outcome: 'refused', ...limited };
|
|
773
|
+
}
|
|
774
|
+
const carried = await carriedDrafts(store, locator, saved, consumed);
|
|
775
|
+
const left = [...consumed, ...carried.keys()];
|
|
776
|
+
const arrived = [...carried.values()];
|
|
777
|
+
const answer = await saveCommit(store, local, files, [
|
|
778
|
+
{
|
|
779
|
+
locator,
|
|
780
|
+
saving,
|
|
781
|
+
before: replica,
|
|
782
|
+
queue: batches,
|
|
783
|
+
write: {
|
|
784
|
+
locator,
|
|
785
|
+
queue: batches,
|
|
786
|
+
stored: replica.queue,
|
|
787
|
+
saved: drafted,
|
|
788
|
+
consumedRoomDrafts: left,
|
|
789
|
+
roomDrafts: arrived,
|
|
790
|
+
},
|
|
791
|
+
},
|
|
792
|
+
]);
|
|
793
|
+
if (answer === null)
|
|
794
|
+
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
795
|
+
if (answer.fenced !== null) {
|
|
796
|
+
throw runtimeError('unknown-room', `Unknown Room ${answer.fenced}`);
|
|
797
|
+
}
|
|
798
|
+
if (store.fences.has(locator))
|
|
799
|
+
throw runtimeError('unknown-room', `Unknown Room ${locator}`);
|
|
800
|
+
preparedAfter(runtime, locator, answer);
|
|
801
|
+
const shownNow = store.changed(changeSet(locator, {
|
|
802
|
+
documents: answer.changed[locator] ?? [],
|
|
803
|
+
queue: true,
|
|
804
|
+
drafts: [...left, ...arrived.map(({ path }) => path)],
|
|
805
|
+
}));
|
|
806
|
+
await Promise.all([shownNow, queuedSave(runtime, locator)]);
|
|
807
|
+
const identifier = join?.batch.id ??
|
|
808
|
+
built?.batch.id ??
|
|
809
|
+
[...requeue.withdrawn].map(([origin, replaced]) => replaced?.id ?? origin).at(-1);
|
|
810
|
+
return { outcome: 'saved', identifier };
|
|
811
|
+
}
|
|
812
|
+
async function queuedFacts(store, locator, path, localId) {
|
|
813
|
+
for (const batch of (await store.replica(locator))?.queue ?? []) {
|
|
814
|
+
const change = batch.changes[queuedFile(batch, path, localId)];
|
|
815
|
+
if (change?.kind === 'binary')
|
|
816
|
+
return change;
|
|
817
|
+
}
|
|
818
|
+
const drafted = (await store.draft(locator, path))?.parts.file;
|
|
819
|
+
return drafted?.localId === localId ? drafted : null;
|
|
820
|
+
}
|
|
821
|
+
export function batchFile(store, batch) {
|
|
822
|
+
return async (localId) => {
|
|
823
|
+
const change = batch.changes.find((held) => held.kind === 'binary' && held.localId === localId);
|
|
824
|
+
return (await store.open()).file(localId, change?.kind === 'binary' ? change.mediaType : '');
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
async function transferBinary(runtime, request, transfer, answer) {
|
|
828
|
+
const { store, socket, journal } = runtime;
|
|
829
|
+
const { session } = socket;
|
|
830
|
+
if (socket.closed() || session.now().refusal !== null)
|
|
831
|
+
return UNCERTAIN;
|
|
832
|
+
await socket.firstDiscovery;
|
|
833
|
+
if (!session.now().roomsNamed)
|
|
834
|
+
return UNCERTAIN;
|
|
835
|
+
try {
|
|
836
|
+
await socket.authorize();
|
|
837
|
+
}
|
|
838
|
+
catch {
|
|
839
|
+
return UNCERTAIN;
|
|
840
|
+
}
|
|
841
|
+
const { closed, authorization, refusal } = session.now();
|
|
842
|
+
if (closed || authorization === undefined || refusal !== null)
|
|
843
|
+
return UNCERTAIN;
|
|
844
|
+
const since = store.fences.count();
|
|
845
|
+
journal.record(`transfer binary ${request.locator} ${request.path}`);
|
|
846
|
+
try {
|
|
847
|
+
const result = await transfer(authorization);
|
|
848
|
+
const settled = result;
|
|
849
|
+
journal.record(`outcome binary ${settled.kind}`);
|
|
850
|
+
if (store.fences.since(request.locator, since))
|
|
851
|
+
return failure({ kind: 'room-not-readable', locator: request.locator });
|
|
852
|
+
if (settled.kind === 'scoped-refusal') {
|
|
853
|
+
const room = await store.record(settled.locator);
|
|
854
|
+
if (room !== undefined)
|
|
855
|
+
await runtime.scoped(settled, room, await store.open());
|
|
856
|
+
}
|
|
857
|
+
if (settled.kind === 'revoked-authorization')
|
|
858
|
+
await refuse(runtime, 'revoked-authorization');
|
|
859
|
+
return answer(result);
|
|
860
|
+
}
|
|
861
|
+
catch {
|
|
862
|
+
journal.record('outcome binary transport-uncertainty');
|
|
863
|
+
return UNCERTAIN;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
function fetchBinary(runtime, request, name) {
|
|
867
|
+
const { store, socket } = runtime;
|
|
868
|
+
return transferBinary(runtime, request, (authorization) => store.opened.fetch(name, () => socket.binary(authorization, request)), binaryAnswer);
|
|
869
|
+
}
|
|
870
|
+
async function readBinary(runtime, request, name) {
|
|
871
|
+
if (runtime.socket.closed())
|
|
872
|
+
return UNCERTAIN;
|
|
873
|
+
const held = await runtime.store.opened.find(name);
|
|
874
|
+
if (held !== null)
|
|
875
|
+
return held;
|
|
876
|
+
return fetchBinary(runtime, request, name);
|
|
877
|
+
}
|
|
878
|
+
async function binaryPath(store, locator, path, revision) {
|
|
879
|
+
if (revision !== undefined)
|
|
880
|
+
return { kind: 'path', locator, path, revision };
|
|
881
|
+
const replica = await store.replica(locator);
|
|
882
|
+
const local = await store.open();
|
|
883
|
+
const [held] = replica === undefined ? [] : await local.entries(replica.record.key, 'visible', [path]);
|
|
884
|
+
const attached = held === undefined ? store.attached.of(locator, path) : undefined;
|
|
885
|
+
if (attached !== undefined)
|
|
886
|
+
return { kind: 'path', locator, path, revision: attached.revision };
|
|
887
|
+
if (replica === undefined || held?.binary === undefined || held.binary === null)
|
|
888
|
+
return null;
|
|
889
|
+
if (held.binary.state === 'pending')
|
|
890
|
+
return { kind: 'pending', localId: held.binary.localId };
|
|
891
|
+
const record = binaryRecord(path, held.binary, held.revision ?? undefined);
|
|
892
|
+
if (record.state !== 'accepted')
|
|
893
|
+
return null;
|
|
894
|
+
const queued = replica.queue.flatMap((batch) => batch.changes);
|
|
895
|
+
const sources = queued.flatMap((change) => (change.kind === 'move' ? [change.path] : []));
|
|
896
|
+
const confirmedLayer = await stepped(slicing(await local.entries(replica.record.key, 'confirmed', [path, ...sources]), [
|
|
897
|
+
path,
|
|
898
|
+
...sources,
|
|
899
|
+
]));
|
|
900
|
+
const holds = (at) => {
|
|
901
|
+
const kept = confirmedLayer.binaries[at];
|
|
902
|
+
return (kept?.state === 'accepted' &&
|
|
903
|
+
kept.sha256 === record.sha256 &&
|
|
904
|
+
confirmedLayer.revisions[at] === record.revision);
|
|
905
|
+
};
|
|
906
|
+
let confirmedAt = path;
|
|
907
|
+
for (let index = queued.length - 1; index >= 0 && !holds(confirmedAt); index -= 1) {
|
|
908
|
+
const change = queued[index];
|
|
909
|
+
if (change.kind === 'move' && change.to === confirmedAt)
|
|
910
|
+
confirmedAt = change.path;
|
|
911
|
+
}
|
|
912
|
+
return {
|
|
913
|
+
kind: 'path',
|
|
914
|
+
locator,
|
|
915
|
+
path: confirmedAt,
|
|
916
|
+
revision: record.revision,
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
async function askedBinary({ store, socket }, locator, path, revision) {
|
|
920
|
+
await socket.ready;
|
|
921
|
+
const drafted = (await store.draft(locator, path))?.parts.file;
|
|
922
|
+
return drafted !== undefined && drafted.localId === revision
|
|
923
|
+
? { kind: 'pending', localId: revision }
|
|
924
|
+
: binaryPath(store, locator, path, revision);
|
|
925
|
+
}
|
|
926
|
+
async function pendingBinary(store, locator, path, localId) {
|
|
927
|
+
const facts = await queuedFacts(store, locator, path, localId);
|
|
928
|
+
return facts === null
|
|
929
|
+
? absentBinary(path)
|
|
930
|
+
: {
|
|
931
|
+
kind: 'binary',
|
|
932
|
+
file: localId,
|
|
933
|
+
byteLength: facts.byteLength,
|
|
934
|
+
mediaType: facts.mediaType,
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
async function readBinaryPath(runtime, locator, path, revision) {
|
|
938
|
+
const asked = await askedBinary(runtime, locator, path, revision);
|
|
939
|
+
if (asked === null)
|
|
940
|
+
return absentBinary(path);
|
|
941
|
+
if (asked.kind === 'pending')
|
|
942
|
+
return pendingBinary(runtime.store, locator, path, asked.localId);
|
|
943
|
+
return readBinary(runtime, asked, { locator, path: asked.path, revision: asked.revision });
|
|
944
|
+
}
|
|
945
|
+
async function heldMediaType(store, locator, path) {
|
|
946
|
+
const replica = await store.record(locator);
|
|
947
|
+
const [held] = replica === undefined ? [] : await (await store.open()).entries(replica.key, 'visible', [path]);
|
|
948
|
+
return held === undefined ? store.attached.of(locator, path)?.mediaType : held.binary?.mediaType;
|
|
949
|
+
}
|
|
950
|
+
function linkOf(runtime, asked, held) {
|
|
951
|
+
return transferBinary(runtime, asked, (authorization) => runtime.socket.link(authorization, asked), (result) => binaryLinkAnswer(result, held));
|
|
952
|
+
}
|
|
953
|
+
async function linkBinaryPath(runtime, locator, path, revision) {
|
|
954
|
+
const { store, socket } = runtime;
|
|
955
|
+
await socket.ready;
|
|
956
|
+
const asked = await binaryPath(store, locator, path, revision);
|
|
957
|
+
if (asked === null || asked.kind === 'pending')
|
|
958
|
+
return absentBinary(path);
|
|
959
|
+
return linkOf(runtime, asked, revision === undefined ? await heldMediaType(store, locator, path) : undefined);
|
|
960
|
+
}
|
|
961
|
+
async function showBinaryPath(runtime, locator, path, revision) {
|
|
962
|
+
const { store } = runtime;
|
|
963
|
+
const asked = await askedBinary(runtime, locator, path, revision);
|
|
964
|
+
if (asked === null)
|
|
965
|
+
return absentBinary(path);
|
|
966
|
+
if (asked.kind === 'pending')
|
|
967
|
+
return pendingBinary(store, locator, path, asked.localId);
|
|
968
|
+
if (runtime.socket.closed())
|
|
969
|
+
return UNCERTAIN;
|
|
970
|
+
const name = { locator, path: asked.path, revision: asked.revision };
|
|
971
|
+
const kept = await store.opened.find(name);
|
|
972
|
+
if (kept !== null)
|
|
973
|
+
return kept;
|
|
974
|
+
const mediaType = await heldMediaType(store, locator, path);
|
|
975
|
+
if (mediaType?.startsWith('image/') === true)
|
|
976
|
+
return fetchBinary(runtime, asked, name);
|
|
977
|
+
return linkOf(runtime, asked, revision === undefined ? mediaType : undefined);
|
|
978
|
+
}
|
|
979
|
+
async function attachBinary(runtime, locator, file) {
|
|
980
|
+
const { store, socket } = runtime;
|
|
981
|
+
await socket.ready;
|
|
982
|
+
await socket.firstDiscovery;
|
|
983
|
+
if (socket.closed() ||
|
|
984
|
+
!socket.session.now().roomsNamed ||
|
|
985
|
+
(await connectionNow(runtime)) === 'unreachable' ||
|
|
986
|
+
socket.session.now().refusal !== null)
|
|
987
|
+
return OFFLINE;
|
|
988
|
+
const request = await attachedFile(locator, file);
|
|
989
|
+
return transferBinary(runtime, { locator, path: file.name }, async (held) => {
|
|
990
|
+
const result = await socket.attach(held, request);
|
|
991
|
+
if (result.kind === 'attached') {
|
|
992
|
+
store.attached.hold(locator, result.path, {
|
|
993
|
+
revision: result.revision,
|
|
994
|
+
mediaType: request.mediaType,
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
return result;
|
|
998
|
+
}, attachAnswer);
|
|
999
|
+
}
|
|
1000
|
+
export function binaryFileSurface(runtime) {
|
|
1001
|
+
return {
|
|
1002
|
+
part: runtime.store.part,
|
|
1003
|
+
read: (room, path, revision) => readBinaryPath(runtime, room, path, revision),
|
|
1004
|
+
link: (room, path, revision) => linkBinaryPath(runtime, room, path, revision),
|
|
1005
|
+
show: (room, path, revision) => showBinaryPath(runtime, room, path, revision),
|
|
1006
|
+
attach: (room, file) => attachBinary(runtime, room, file),
|
|
1007
|
+
};
|
|
1008
|
+
}
|