amalgm 0.1.239 → 0.1.241
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/lib/shared-realtime-tunnel.js +18 -4
- package/package.json +1 -1
- package/runtime/scripts/amalgm-mcp/browser/cookie-jar.js +68 -20
- package/runtime/scripts/amalgm-mcp/project-context/store.js +87 -19
- package/runtime/scripts/amalgm-mcp/server/routes/state.js +13 -0
- package/runtime/scripts/amalgm-mcp/state/attachments.js +40 -12
- package/runtime/scripts/amalgm-mcp/state/db.js +216 -17
- package/runtime/scripts/amalgm-mcp/state/doc-disk.js +550 -0
- package/runtime/scripts/amalgm-mcp/state/docs.js +1101 -301
- package/runtime/scripts/amalgm-mcp/state/mutation-contracts.js +99 -0
- package/runtime/scripts/amalgm-mcp/state/mutations.js +507 -130
- package/runtime/scripts/amalgm-mcp/state/promotions.js +21 -1
- package/runtime/scripts/amalgm-mcp/state/replicas.js +134 -13
- package/runtime/scripts/amalgm-mcp/state/shared-rest.js +14 -8
- package/runtime/scripts/amalgm-mcp/tests/browser-cookie-cloud.test.js +193 -0
- package/runtime/scripts/amalgm-mcp/tests/code-project-stream.test.js +313 -94
- package/runtime/scripts/amalgm-mcp/tests/fixtures/code-project-stream-v2.json +1 -0
- package/runtime/scripts/amalgm-mcp/tests/project-context.test.js +32 -17
- package/runtime/scripts/amalgm-mcp/tests/shared-tunnel-rehydration.test.js +84 -0
- package/runtime/scripts/amalgm-mcp/tests/state-docs.test.js +376 -13
- package/runtime/scripts/amalgm-mcp/tests/state-mutations.test.js +309 -0
- package/runtime/scripts/amalgm-mcp/tests/state-shared-replica.test.js +758 -30
- package/runtime/scripts/amalgm-mcp/tests/workspace-cards-store.test.js +72 -3
- package/runtime/scripts/amalgm-mcp/tests/workspace-cards.test.js +33 -6
- package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint-reducer.test.js +27 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint.test.js +334 -191
- package/runtime/scripts/amalgm-mcp/tests/workspace-durable.test.js +24 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-local-intent.test.js +1999 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-native-file-transaction.test.js +364 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-object-tunnel.test.js +98 -4
- package/runtime/scripts/amalgm-mcp/tests/workspace-objects.test.js +156 -8
- package/runtime/scripts/amalgm-mcp/tests/workspace-provenance.test.js +185 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-publication-invariants.test.js +174 -269
- package/runtime/scripts/amalgm-mcp/tests/workspace-reducer-liveness.test.js +26 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-reducer.test.js +2060 -407
- package/runtime/scripts/amalgm-mcp/tests/workspace-sync-stats.test.js +119 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-transition.test.js +1446 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-tree-cloud.test.js +5319 -3
- package/runtime/scripts/amalgm-mcp/tests/workspace-tree-core.test.js +191 -1
- package/runtime/scripts/amalgm-mcp/tests/workspace-tree-store.test.js +3140 -17
- package/runtime/scripts/amalgm-mcp/tests/workspace-wall.test.js +135 -637
- package/runtime/scripts/amalgm-mcp/workspace/access-store.js +5 -1
- package/runtime/scripts/amalgm-mcp/workspace/card.js +7 -7
- package/runtime/scripts/amalgm-mcp/workspace/cards.js +36 -12
- package/runtime/scripts/amalgm-mcp/workspace/checkpoint.js +436 -173
- package/runtime/scripts/amalgm-mcp/workspace/content.js +32 -0
- package/runtime/scripts/amalgm-mcp/workspace/durable.js +45 -0
- package/runtime/scripts/amalgm-mcp/workspace/git.js +391 -37
- package/runtime/scripts/amalgm-mcp/workspace/intent.js +535 -0
- package/runtime/scripts/amalgm-mcp/workspace/merge.js +89 -6
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/BUILD.md +31 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/darwin-arm64/amalgm-file-transaction +0 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/linux-x64/amalgm-file-transaction +0 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/build.mjs +72 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/client.js +165 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.mod +5 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.sum +2 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/main.go +2201 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/manifest.json +19 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_darwin.go +18 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_linux.go +18 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_darwin.go +33 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_linux.go +47 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/verify.mjs +43 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_darwin.go +114 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_linux.go +84 -0
- package/runtime/scripts/amalgm-mcp/workspace/objects.js +185 -55
- package/runtime/scripts/amalgm-mcp/workspace/project-stream.js +616 -168
- package/runtime/scripts/amalgm-mcp/workspace/provenance.js +240 -0
- package/runtime/scripts/amalgm-mcp/workspace/reducer.js +1979 -570
- package/runtime/scripts/amalgm-mcp/workspace/rest.js +49 -28
- package/runtime/scripts/amalgm-mcp/workspace/store.js +77 -4
- package/runtime/scripts/amalgm-mcp/workspace/sync-stats.js +240 -0
- package/runtime/scripts/amalgm-mcp/workspace/transition.js +1729 -192
- package/runtime/scripts/amalgm-mcp/workspace/tree/floor.js +100 -0
- package/runtime/scripts/amalgm-mcp/workspace/tree/inclusion.js +7 -1
- package/runtime/scripts/amalgm-mcp/workspace/tree/local-state.js +73 -61
- package/runtime/scripts/amalgm-mcp/workspace/tree/observation-holds.js +127 -0
- package/runtime/scripts/amalgm-mcp/workspace/tree/path-state.js +12 -0
- package/runtime/scripts/amalgm-mcp/workspace/tree/reducer.js +51 -6
- package/runtime/scripts/amalgm-mcp/workspace/tree/root-binding.js +125 -0
- package/runtime/scripts/amalgm-mcp/workspace/tree/snapshot.js +79 -8
- package/runtime/scripts/amalgm-mcp/workspace/tree/symlinks.js +76 -0
- package/runtime/scripts/amalgm-mcp/workspace/tree-cloud.js +3480 -165
- package/runtime/scripts/amalgm-mcp/workspace/tree-materialization.js +3766 -0
- package/runtime/scripts/amalgm-mcp/workspace/tree-store.js +899 -71
- package/runtime/scripts/amalgm-mcp/workspace/wall.js +352 -497
|
@@ -1,84 +1,528 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* The one authoritative stream for a code project.
|
|
5
|
+
*
|
|
6
|
+
* Clients publish deltas. Only an officially numbered cloud mutation changes
|
|
7
|
+
* this snapshot. A local outbox row is intent, never authority.
|
|
8
|
+
*/
|
|
9
|
+
|
|
3
10
|
const crypto = require('crypto');
|
|
4
11
|
const fs = require('fs');
|
|
5
12
|
const { appendStateEvent } = require('../state/events');
|
|
6
13
|
const { openLocalDb } = require('../state/db');
|
|
14
|
+
const durable = require('./durable');
|
|
7
15
|
|
|
8
|
-
const STREAM_CONTRACT = 'code-project-stream@
|
|
9
|
-
const WALL_CONTRACT = 'code-project@
|
|
16
|
+
const STREAM_CONTRACT = 'code-project-stream@2';
|
|
17
|
+
const WALL_CONTRACT = 'code-project@7';
|
|
18
|
+
const CARD_CONTRACT = 'repo-card@3';
|
|
19
|
+
const CHECKPOINT_CONTRACT = 'repo-checkpoint@11';
|
|
20
|
+
const BUNDLE_CONTRACT = 'git-bundle-chunks@2';
|
|
21
|
+
const PAYLOAD_CONTRACT = 'opaque-chunks@2';
|
|
10
22
|
const LOCAL_RESOURCE_PREFIX = 'code-project:';
|
|
23
|
+
const SHA = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
24
|
+
const SHA256 = /^[0-9a-f]{64}$/;
|
|
25
|
+
const MUTATION_ID = /^[A-Za-z0-9_-]{16,160}$/;
|
|
26
|
+
const MAX_CHUNKS = 4096;
|
|
27
|
+
const MAX_CHUNK_BYTES = 512 * 1024;
|
|
28
|
+
const MAX_BRANCHES = 4096;
|
|
11
29
|
|
|
12
|
-
function invalid(message, statusCode = 400) {
|
|
13
|
-
|
|
30
|
+
function invalid(message, statusCode = 400, code = null) {
|
|
31
|
+
const error = Object.assign(new Error(message), { statusCode });
|
|
32
|
+
if (code) error.code = code;
|
|
33
|
+
return error;
|
|
14
34
|
}
|
|
15
35
|
|
|
16
|
-
function
|
|
17
|
-
|
|
36
|
+
function exactObject(input, keys, label) {
|
|
37
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
38
|
+
throw invalid(`${label} is invalid`, 409);
|
|
39
|
+
}
|
|
40
|
+
const actual = Object.keys(input).sort();
|
|
41
|
+
const expected = [...keys].sort();
|
|
42
|
+
if (
|
|
43
|
+
actual.length !== expected.length
|
|
44
|
+
|| actual.some((key, index) => key !== expected[index])
|
|
45
|
+
) throw invalid(`${label} has unexpected fields`, 409);
|
|
46
|
+
return input;
|
|
18
47
|
}
|
|
19
48
|
|
|
20
|
-
function
|
|
21
|
-
|
|
49
|
+
function nonEmptyString(value, label) {
|
|
50
|
+
if (typeof value !== 'string' || !value) throw invalid(`${label} is invalid`, 409);
|
|
51
|
+
return value;
|
|
22
52
|
}
|
|
23
53
|
|
|
24
|
-
function
|
|
54
|
+
function branchName(value) {
|
|
55
|
+
const components = typeof value === 'string' ? value.split('/') : [];
|
|
25
56
|
if (
|
|
26
|
-
|
|
27
|
-
||
|
|
28
|
-
||
|
|
29
|
-
||
|
|
30
|
-
||
|
|
31
|
-
||
|
|
32
|
-
||
|
|
33
|
-
||
|
|
34
|
-
||
|
|
35
|
-
||
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
57
|
+
typeof value !== 'string'
|
|
58
|
+
|| value.length < 1
|
|
59
|
+
|| value.length > 1024
|
|
60
|
+
|| value === '@'
|
|
61
|
+
|| value === 'HEAD'
|
|
62
|
+
|| value.startsWith('-')
|
|
63
|
+
|| value.endsWith('.')
|
|
64
|
+
|| value.includes('..')
|
|
65
|
+
|| value.includes('@{')
|
|
66
|
+
|| value.includes('[')
|
|
67
|
+
|| /[\u0000-\u0020\u007f~^:?*\\]/.test(value)
|
|
68
|
+
|| components.some((component) => (
|
|
69
|
+
!component
|
|
70
|
+
|| component.startsWith('.')
|
|
71
|
+
|| component.endsWith('.lock')
|
|
72
|
+
))
|
|
73
|
+
) throw invalid('branch name is invalid', 409);
|
|
74
|
+
return value;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function mutationId(value, label) {
|
|
78
|
+
if (!MUTATION_ID.test(String(value || ''))) throw invalid(`${label} is invalid`, 409);
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function normalizeChunks(input, resourceId, kind, contract) {
|
|
83
|
+
exactObject(
|
|
84
|
+
input,
|
|
85
|
+
['kind', 'contract', 'resourceId', 'checksum', 'bytes', 'chunks'],
|
|
86
|
+
`${kind} locator`,
|
|
87
|
+
);
|
|
88
|
+
if (
|
|
89
|
+
input.kind !== kind
|
|
90
|
+
|| input.contract !== contract
|
|
91
|
+
|| input.resourceId !== resourceId
|
|
92
|
+
|| !SHA256.test(String(input.checksum || ''))
|
|
93
|
+
|| !Number.isSafeInteger(input.bytes)
|
|
94
|
+
|| input.bytes < 1
|
|
95
|
+
|| !Array.isArray(input.chunks)
|
|
96
|
+
|| input.chunks.length < 1
|
|
97
|
+
|| input.chunks.length > MAX_CHUNKS
|
|
98
|
+
) throw invalid(`${kind} locator is invalid`, 409);
|
|
99
|
+
const chunks = input.chunks.map((chunk) => {
|
|
100
|
+
exactObject(chunk, ['sha256', 'bytes'], `${kind} chunk`);
|
|
101
|
+
if (
|
|
102
|
+
!SHA256.test(String(chunk.sha256 || ''))
|
|
103
|
+
|| !Number.isSafeInteger(chunk.bytes)
|
|
104
|
+
|| chunk.bytes < 1
|
|
105
|
+
|| chunk.bytes > MAX_CHUNK_BYTES
|
|
106
|
+
) throw invalid(`${kind} chunk is invalid`, 409);
|
|
107
|
+
return { sha256: chunk.sha256, bytes: chunk.bytes };
|
|
108
|
+
});
|
|
109
|
+
if (chunks.reduce((sum, chunk) => sum + chunk.bytes, 0) !== input.bytes) {
|
|
110
|
+
throw invalid(`${kind} locator byte count is invalid`, 409);
|
|
40
111
|
}
|
|
112
|
+
const checksum = crypto.createHash('sha256').update(Buffer.from(JSON.stringify(chunks))).digest('hex');
|
|
113
|
+
if (checksum !== input.checksum) throw invalid(`${kind} locator checksum is invalid`, 409);
|
|
41
114
|
return {
|
|
42
|
-
|
|
43
|
-
|
|
115
|
+
kind,
|
|
116
|
+
contract,
|
|
117
|
+
resourceId,
|
|
118
|
+
checksum: input.checksum,
|
|
119
|
+
bytes: input.bytes,
|
|
120
|
+
chunks,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function normalizeBundle(input, resourceId, oid) {
|
|
125
|
+
exactObject(
|
|
126
|
+
input,
|
|
127
|
+
['kind', 'contract', 'resourceId', 'oid', 'ref', 'checksum', 'bytes', 'chunks'],
|
|
128
|
+
'bundle locator',
|
|
129
|
+
);
|
|
130
|
+
if (
|
|
131
|
+
input.kind !== 'bundle'
|
|
132
|
+
|| input.contract !== BUNDLE_CONTRACT
|
|
133
|
+
|| input.resourceId !== resourceId
|
|
134
|
+
|| input.oid !== oid
|
|
135
|
+
|| input.ref !== `refs/amalgm/exports/${oid}`
|
|
136
|
+
) throw invalid('bundle locator is invalid', 409);
|
|
137
|
+
const common = normalizeChunks(
|
|
138
|
+
{
|
|
139
|
+
kind: 'bundle',
|
|
140
|
+
contract: BUNDLE_CONTRACT,
|
|
141
|
+
resourceId: input.resourceId,
|
|
142
|
+
checksum: input.checksum,
|
|
143
|
+
bytes: input.bytes,
|
|
144
|
+
chunks: input.chunks,
|
|
145
|
+
},
|
|
146
|
+
resourceId,
|
|
147
|
+
'bundle',
|
|
148
|
+
BUNDLE_CONTRACT,
|
|
149
|
+
);
|
|
150
|
+
return { ...common, oid, ref: input.ref };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function normalizePayload(input, resourceId) {
|
|
154
|
+
return normalizeChunks(input, resourceId, 'chunks', PAYLOAD_CONTRACT);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function normalizeCheckpointValue(input, resourceId, baseOid = null) {
|
|
158
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
159
|
+
throw invalid('checkpoint value is invalid', 409);
|
|
160
|
+
}
|
|
161
|
+
const revision = input.revision;
|
|
162
|
+
if (!(revision === null || SHA256.test(String(revision || '')))) {
|
|
163
|
+
throw invalid('checkpoint revision is invalid', 409);
|
|
164
|
+
}
|
|
165
|
+
if (Object.hasOwn(input, 'sections')) {
|
|
166
|
+
exactObject(input, ['revision', 'sections'], 'checkpoint value');
|
|
167
|
+
exactObject(input.sections, ['index', 'worktree'], 'checkpoint sections');
|
|
168
|
+
const normalizeLayer = (layer) => {
|
|
169
|
+
if (!Array.isArray(input.sections[layer])) {
|
|
170
|
+
throw invalid('checkpoint sections are invalid', 409);
|
|
171
|
+
}
|
|
172
|
+
return input.sections[layer].map((section) => {
|
|
173
|
+
if (Object.hasOwn(section || {}, 'text')) {
|
|
174
|
+
exactObject(section, ['text'], 'checkpoint section');
|
|
175
|
+
if (typeof section.text !== 'string' || section.text.length === 0) {
|
|
176
|
+
throw invalid('checkpoint section is invalid', 409);
|
|
177
|
+
}
|
|
178
|
+
return { text: section.text };
|
|
179
|
+
}
|
|
180
|
+
exactObject(section, ['payload'], 'checkpoint section');
|
|
181
|
+
return { payload: normalizePayload(section.payload, resourceId) };
|
|
182
|
+
});
|
|
183
|
+
};
|
|
184
|
+
const normalized = {
|
|
185
|
+
revision,
|
|
186
|
+
sections: {
|
|
187
|
+
index: normalizeLayer('index'),
|
|
188
|
+
worktree: normalizeLayer('worktree'),
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
if (require('./checkpoint').revisionOfValue(baseOid, normalized) !== revision) {
|
|
192
|
+
throw invalid('checkpoint revision is invalid', 409);
|
|
193
|
+
}
|
|
194
|
+
return normalized;
|
|
195
|
+
}
|
|
196
|
+
exactObject(input, ['revision', 'indexDiff', 'worktreeDiff'], 'checkpoint value');
|
|
197
|
+
if (typeof input.indexDiff !== 'string' || typeof input.worktreeDiff !== 'string') {
|
|
198
|
+
throw invalid('checkpoint value is invalid', 409);
|
|
199
|
+
}
|
|
200
|
+
if (
|
|
201
|
+
baseOid
|
|
202
|
+
&& require('./checkpoint').revisionOfValue(baseOid, input) !== revision
|
|
203
|
+
) throw invalid('checkpoint revision is invalid', 409);
|
|
204
|
+
return {
|
|
205
|
+
revision,
|
|
206
|
+
indexDiff: input.indexDiff,
|
|
207
|
+
worktreeDiff: input.worktreeDiff,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function normalizeHead(input) {
|
|
212
|
+
exactObject(input, ['branch', 'oid', 'detached'], 'card head');
|
|
213
|
+
if (
|
|
214
|
+
!(input.branch === null || branchName(input.branch))
|
|
215
|
+
|| !SHA.test(String(input.oid || ''))
|
|
216
|
+
|| typeof input.detached !== 'boolean'
|
|
217
|
+
|| (input.detached && input.branch !== null)
|
|
218
|
+
|| (!input.detached && !input.branch)
|
|
219
|
+
) throw invalid('card head is invalid', 409);
|
|
220
|
+
return { branch: input.branch, oid: input.oid, detached: input.detached };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function normalizeBranch(input, resourceId) {
|
|
224
|
+
exactObject(input, ['name', 'oid', 'objects'], 'card branch');
|
|
225
|
+
const name = branchName(input.name);
|
|
226
|
+
if (!SHA.test(String(input.oid || ''))) throw invalid('branch oid is invalid', 409);
|
|
227
|
+
return {
|
|
228
|
+
name,
|
|
229
|
+
oid: input.oid,
|
|
230
|
+
objects: normalizeBundle(input.objects, resourceId, input.oid),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function normalizeCheckpoint(input, resourceId, expectedHead = null) {
|
|
235
|
+
exactObject(input, ['checkpoint', 'baseOid', 'branch', 'value'], 'card checkpoint');
|
|
236
|
+
if (
|
|
237
|
+
input.checkpoint !== CHECKPOINT_CONTRACT
|
|
238
|
+
|| !SHA.test(String(input.baseOid || ''))
|
|
239
|
+
|| !(input.branch === null || branchName(input.branch))
|
|
240
|
+
|| (expectedHead && (
|
|
241
|
+
input.baseOid !== expectedHead.oid
|
|
242
|
+
|| input.branch !== expectedHead.branch
|
|
243
|
+
))
|
|
244
|
+
) throw invalid('card checkpoint is invalid', 409);
|
|
245
|
+
return {
|
|
246
|
+
checkpoint: CHECKPOINT_CONTRACT,
|
|
247
|
+
baseOid: input.baseOid,
|
|
248
|
+
branch: input.branch,
|
|
249
|
+
value: normalizeCheckpointValue(input.value, resourceId, input.baseOid),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function normalizeCardSet(input, resourceId) {
|
|
254
|
+
exactObject(input, ['kind', 'card', 'head', 'objects', 'branches', 'checkpoint'], 'card.set');
|
|
255
|
+
if (input.kind !== 'card.set' || input.card !== CARD_CONTRACT) {
|
|
256
|
+
throw invalid('card.set is invalid', 409);
|
|
257
|
+
}
|
|
258
|
+
const head = normalizeHead(input.head);
|
|
259
|
+
const branches = Array.isArray(input.branches)
|
|
260
|
+
? input.branches.map((branch) => normalizeBranch(branch, resourceId))
|
|
261
|
+
: null;
|
|
262
|
+
if (!branches) throw invalid('card branches are invalid', 409);
|
|
263
|
+
if (branches.length > MAX_BRANCHES) throw invalid('card has too many branches', 409);
|
|
264
|
+
const names = new Set();
|
|
265
|
+
for (const branch of branches) {
|
|
266
|
+
if (names.has(branch.name)) throw invalid('card repeats a branch', 409);
|
|
267
|
+
names.add(branch.name);
|
|
268
|
+
}
|
|
269
|
+
if (!head.detached && !branches.some((branch) => (
|
|
270
|
+
branch.name === head.branch && branch.oid === head.oid
|
|
271
|
+
))) throw invalid('card branch map does not contain HEAD', 409);
|
|
272
|
+
return {
|
|
273
|
+
kind: 'card.set',
|
|
274
|
+
card: CARD_CONTRACT,
|
|
275
|
+
head,
|
|
276
|
+
objects: normalizeBundle(input.objects, resourceId, head.oid),
|
|
277
|
+
branches: branches.sort((left, right) => left.name.localeCompare(right.name)),
|
|
278
|
+
checkpoint: normalizeCheckpoint(input.checkpoint, resourceId, head),
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function normalizeCheckpointSet(input, resourceId) {
|
|
283
|
+
exactObject(
|
|
284
|
+
input,
|
|
285
|
+
['kind', 'checkpoint', 'baseOid', 'branch', 'from', 'to'],
|
|
286
|
+
'checkpoint.set',
|
|
287
|
+
);
|
|
288
|
+
if (
|
|
289
|
+
input.kind !== 'checkpoint.set'
|
|
290
|
+
|| input.checkpoint !== CHECKPOINT_CONTRACT
|
|
291
|
+
|| !SHA.test(String(input.baseOid || ''))
|
|
292
|
+
|| !(input.branch === null || branchName(input.branch))
|
|
293
|
+
) throw invalid('checkpoint.set is invalid', 409);
|
|
294
|
+
return {
|
|
295
|
+
kind: 'checkpoint.set',
|
|
296
|
+
checkpoint: CHECKPOINT_CONTRACT,
|
|
297
|
+
baseOid: input.baseOid,
|
|
298
|
+
branch: input.branch,
|
|
299
|
+
from: normalizeCheckpointValue(input.from, resourceId, input.baseOid),
|
|
300
|
+
to: normalizeCheckpointValue(input.to, resourceId, input.baseOid),
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function normalizeDelta(input, resourceId) {
|
|
305
|
+
let normalized;
|
|
306
|
+
if (input?.kind === 'card.set') normalized = normalizeCardSet(input, resourceId);
|
|
307
|
+
else if (input?.kind === 'checkpoint.set') normalized = normalizeCheckpointSet(input, resourceId);
|
|
308
|
+
else throw invalid('code project delta is invalid', 409);
|
|
309
|
+
const chunks = new Map();
|
|
310
|
+
const visit = (value) => {
|
|
311
|
+
if (!value || typeof value !== 'object') return;
|
|
312
|
+
if (Array.isArray(value)) {
|
|
313
|
+
value.forEach(visit);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (Array.isArray(value.chunks)) {
|
|
317
|
+
value.chunks.forEach((chunk) => {
|
|
318
|
+
const previous = chunks.get(chunk.sha256);
|
|
319
|
+
if (previous !== undefined && previous !== chunk.bytes) {
|
|
320
|
+
throw invalid('one code chunk claims two sizes', 409);
|
|
321
|
+
}
|
|
322
|
+
chunks.set(chunk.sha256, chunk.bytes);
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
Object.values(value).forEach(visit);
|
|
326
|
+
};
|
|
327
|
+
visit(normalized);
|
|
328
|
+
if (chunks.size > MAX_CHUNKS) throw invalid('code publication has too many chunks', 409);
|
|
329
|
+
return normalized;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function normalizeOperation(input, resourceId) {
|
|
333
|
+
exactObject(input, ['kind', 'project', 'delta'], 'code.publish operation');
|
|
334
|
+
if (input.kind !== 'code.publish' || input.project !== resourceId) {
|
|
335
|
+
throw invalid('code.publish operation is invalid', 409);
|
|
336
|
+
}
|
|
337
|
+
return {
|
|
338
|
+
kind: 'code.publish',
|
|
339
|
+
project: resourceId,
|
|
340
|
+
delta: normalizeDelta(input.delta, resourceId),
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function stampCard(delta, stamp) {
|
|
345
|
+
return {
|
|
346
|
+
card: CARD_CONTRACT,
|
|
347
|
+
head: delta.head,
|
|
348
|
+
objects: delta.objects,
|
|
349
|
+
branches: delta.branches,
|
|
350
|
+
...stamp,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function stampCheckpoint(checkpoint, stamp) {
|
|
355
|
+
return { ...checkpoint, ...stamp };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function sameValue(left, right) {
|
|
359
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function fold(snapshotInput, operationInput, stampInput) {
|
|
363
|
+
const snapshot = normalizeSnapshot(snapshotInput, operationInput?.project, true);
|
|
364
|
+
const operation = normalizeOperation(operationInput, snapshot.project);
|
|
365
|
+
const version = Number(stampInput?.version);
|
|
366
|
+
const machine = nonEmptyString(stampInput?.machine, 'mutation machine');
|
|
367
|
+
const officialMutationId = mutationId(stampInput?.mutationId, 'mutation id');
|
|
368
|
+
if (!Number.isSafeInteger(version) || version < 1) {
|
|
369
|
+
throw invalid('mutation version is invalid', 409);
|
|
370
|
+
}
|
|
371
|
+
// Snapshot writes precede cursor writes. Replaying after a crash is silent.
|
|
372
|
+
if (snapshot.version >= version) return snapshot;
|
|
373
|
+
if (version !== snapshot.version + 1) {
|
|
374
|
+
throw invalid(
|
|
375
|
+
`code project gap: expected ${snapshot.version + 1}, received ${version}`,
|
|
376
|
+
409,
|
|
377
|
+
'shared_mutation_gap',
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
const stamp = { machine, mutationId: officialMutationId, version };
|
|
381
|
+
if (operation.delta.kind === 'card.set') {
|
|
382
|
+
return {
|
|
383
|
+
schemaVersion: 2,
|
|
384
|
+
project: snapshot.project,
|
|
385
|
+
version,
|
|
386
|
+
card: stampCard(operation.delta, stamp),
|
|
387
|
+
checkpoint: stampCheckpoint(operation.delta.checkpoint, stamp),
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
const delta = operation.delta;
|
|
391
|
+
if (
|
|
392
|
+
!snapshot.card
|
|
393
|
+
|| snapshot.card.head.oid !== delta.baseOid
|
|
394
|
+
|| snapshot.card.head.branch !== delta.branch
|
|
395
|
+
|| !snapshot.checkpoint
|
|
396
|
+
|| snapshot.checkpoint.baseOid !== delta.baseOid
|
|
397
|
+
|| snapshot.checkpoint.branch !== delta.branch
|
|
398
|
+
|| !sameValue(snapshot.checkpoint.value, delta.from)
|
|
399
|
+
) throw invalid('code_project_stale_base', 409, 'code_project_stale_base');
|
|
400
|
+
return {
|
|
401
|
+
...snapshot,
|
|
402
|
+
version,
|
|
403
|
+
checkpoint: stampCheckpoint({
|
|
404
|
+
checkpoint: CHECKPOINT_CONTRACT,
|
|
405
|
+
baseOid: delta.baseOid,
|
|
406
|
+
branch: delta.branch,
|
|
407
|
+
value: delta.to,
|
|
408
|
+
}, stamp),
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function emptySnapshot(resourceId) {
|
|
413
|
+
return {
|
|
414
|
+
schemaVersion: 2,
|
|
415
|
+
project: resourceId,
|
|
416
|
+
version: 0,
|
|
417
|
+
card: null,
|
|
418
|
+
checkpoint: null,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function normalizeStampedCard(input, resourceId, snapshotVersion) {
|
|
423
|
+
exactObject(
|
|
424
|
+
input,
|
|
425
|
+
['card', 'head', 'objects', 'branches', 'machine', 'mutationId', 'version'],
|
|
426
|
+
'official card',
|
|
427
|
+
);
|
|
428
|
+
const version = Number(input.version);
|
|
429
|
+
if (
|
|
430
|
+
input.card !== CARD_CONTRACT
|
|
431
|
+
|| !Number.isSafeInteger(version)
|
|
432
|
+
|| version < 1
|
|
433
|
+
|| version > snapshotVersion
|
|
434
|
+
) throw invalid('official card stamp is invalid', 409);
|
|
435
|
+
const delta = normalizeCardSet({
|
|
436
|
+
kind: 'card.set',
|
|
437
|
+
card: input.card,
|
|
438
|
+
head: input.head,
|
|
439
|
+
objects: input.objects,
|
|
440
|
+
branches: input.branches,
|
|
441
|
+
checkpoint: {
|
|
442
|
+
checkpoint: CHECKPOINT_CONTRACT,
|
|
443
|
+
baseOid: input.head.oid,
|
|
444
|
+
branch: input.head.branch,
|
|
445
|
+
value: { revision: null, indexDiff: '', worktreeDiff: '' },
|
|
446
|
+
},
|
|
447
|
+
}, resourceId);
|
|
448
|
+
return {
|
|
449
|
+
card: CARD_CONTRACT,
|
|
450
|
+
head: delta.head,
|
|
451
|
+
objects: delta.objects,
|
|
452
|
+
branches: delta.branches,
|
|
453
|
+
machine: nonEmptyString(input.machine, 'official card machine'),
|
|
454
|
+
mutationId: mutationId(input.mutationId, 'official card mutation'),
|
|
455
|
+
version,
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function normalizeStampedCheckpoint(input, resourceId, snapshotVersion) {
|
|
460
|
+
exactObject(
|
|
461
|
+
input,
|
|
462
|
+
['checkpoint', 'baseOid', 'branch', 'value', 'machine', 'mutationId', 'version'],
|
|
463
|
+
'official checkpoint',
|
|
464
|
+
);
|
|
465
|
+
const version = Number(input.version);
|
|
466
|
+
if (
|
|
467
|
+
!Number.isSafeInteger(version)
|
|
468
|
+
|| version < 1
|
|
469
|
+
|| version > snapshotVersion
|
|
470
|
+
) throw invalid('official checkpoint stamp is invalid', 409);
|
|
471
|
+
return {
|
|
472
|
+
...normalizeCheckpoint({
|
|
473
|
+
checkpoint: input.checkpoint,
|
|
474
|
+
baseOid: input.baseOid,
|
|
475
|
+
branch: input.branch,
|
|
476
|
+
value: input.value,
|
|
477
|
+
}, resourceId),
|
|
478
|
+
machine: nonEmptyString(input.machine, 'official checkpoint machine'),
|
|
479
|
+
mutationId: mutationId(input.mutationId, 'official checkpoint mutation'),
|
|
480
|
+
version,
|
|
44
481
|
};
|
|
45
482
|
}
|
|
46
483
|
|
|
47
|
-
function normalizeSnapshot(input,
|
|
484
|
+
function normalizeSnapshot(input, resourceId = null, official = false) {
|
|
485
|
+
exactObject(input, ['schemaVersion', 'project', 'version', 'card', 'checkpoint'], 'code project snapshot');
|
|
48
486
|
if (
|
|
49
|
-
|
|
50
|
-
|| input.schemaVersion !== 1
|
|
487
|
+
input.schemaVersion !== 2
|
|
51
488
|
|| typeof input.project !== 'string'
|
|
52
489
|
|| !input.project
|
|
490
|
+
|| !require('./tree/identity').isUuid(input.project)
|
|
491
|
+
|| (resourceId && input.project !== resourceId)
|
|
53
492
|
|| !Number.isSafeInteger(Number(input.version))
|
|
54
493
|
|| Number(input.version) < 0
|
|
55
|
-
|| !Array.isArray(input.records)
|
|
56
|
-
|| (project && input.project !== project)
|
|
57
494
|
) throw invalid('code project snapshot is invalid', 409);
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
throw invalid('code project record is newer than its snapshot', 409);
|
|
63
|
-
}
|
|
64
|
-
if (machines.has(normalized.machine)) {
|
|
65
|
-
throw invalid('code project snapshot repeats a machine', 409);
|
|
495
|
+
const version = Number(input.version);
|
|
496
|
+
if (version === 0) {
|
|
497
|
+
if (input.card !== null || input.checkpoint !== null) {
|
|
498
|
+
throw invalid('empty code project snapshot is invalid', 409);
|
|
66
499
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
500
|
+
return emptySnapshot(input.project);
|
|
501
|
+
}
|
|
502
|
+
if (!official || !input.card || !input.checkpoint) {
|
|
503
|
+
throw invalid('official code project snapshot is incomplete', 409);
|
|
504
|
+
}
|
|
505
|
+
const card = normalizeStampedCard(input.card, input.project, version);
|
|
506
|
+
const checkpoint = normalizeStampedCheckpoint(input.checkpoint, input.project, version);
|
|
507
|
+
if (
|
|
508
|
+
checkpoint.version !== version
|
|
509
|
+
|| card.version > checkpoint.version
|
|
510
|
+
|| checkpoint.baseOid !== card.head.oid
|
|
511
|
+
|| checkpoint.branch !== card.head.branch
|
|
512
|
+
) throw invalid('official checkpoint does not match the card', 409);
|
|
70
513
|
return {
|
|
71
|
-
schemaVersion:
|
|
514
|
+
schemaVersion: 2,
|
|
72
515
|
project: input.project,
|
|
73
|
-
version
|
|
74
|
-
|
|
516
|
+
version,
|
|
517
|
+
card,
|
|
518
|
+
checkpoint,
|
|
75
519
|
};
|
|
76
520
|
}
|
|
77
521
|
|
|
78
|
-
function readSnapshot(file,
|
|
79
|
-
if (!fs.existsSync(file) || fs.statSync(file).size === 0) return emptySnapshot(
|
|
522
|
+
function readSnapshot(file, resourceId) {
|
|
523
|
+
if (!fs.existsSync(file) || fs.statSync(file).size === 0) return emptySnapshot(resourceId);
|
|
80
524
|
try {
|
|
81
|
-
return normalizeSnapshot(JSON.parse(fs.readFileSync(file, 'utf8')),
|
|
525
|
+
return normalizeSnapshot(JSON.parse(fs.readFileSync(file, 'utf8')), resourceId, true);
|
|
82
526
|
} catch (error) {
|
|
83
527
|
if (error?.statusCode) throw error;
|
|
84
528
|
throw invalid('code project snapshot is not JSON', 409);
|
|
@@ -86,114 +530,47 @@ function readSnapshot(file, project) {
|
|
|
86
530
|
}
|
|
87
531
|
|
|
88
532
|
function writeSnapshot(file, snapshot) {
|
|
89
|
-
|
|
90
|
-
fs.writeFileSync(file, `${JSON.stringify(snapshot)}\n`);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function applyRecord(snapshot, record, version = null) {
|
|
94
|
-
const next = normalizeRecord(
|
|
95
|
-
version ? { ...record, version } : record,
|
|
96
|
-
snapshot.project,
|
|
97
|
-
false,
|
|
98
|
-
);
|
|
99
|
-
const records = new Map(snapshot.records.map((entry) => [entry.machine, entry]));
|
|
100
|
-
records.set(next.machine, next);
|
|
101
|
-
return {
|
|
102
|
-
...snapshot,
|
|
103
|
-
version: Number.isSafeInteger(version)
|
|
104
|
-
? Math.max(snapshot.version, version)
|
|
105
|
-
: snapshot.version,
|
|
106
|
-
records: [...records.values()].sort((left, right) => (
|
|
107
|
-
left.machine.localeCompare(right.machine)
|
|
108
|
-
)),
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function records(project) {
|
|
113
|
-
const snapshot = readSnapshot(project.file, project.repoKey);
|
|
114
|
-
let changed = false;
|
|
115
|
-
const sealed = snapshot.records.map((record) => {
|
|
116
|
-
if (record.version) return record;
|
|
117
|
-
const mutation = require('../state/mutations').getMutation(
|
|
118
|
-
`resource:${project.resourceId}`,
|
|
119
|
-
record.mutationId,
|
|
120
|
-
);
|
|
121
|
-
const version = mutation?.officialCloudVersion;
|
|
122
|
-
if (!Number.isSafeInteger(version) || version < 1) return record;
|
|
123
|
-
changed = true;
|
|
124
|
-
return { ...record, version };
|
|
125
|
-
});
|
|
126
|
-
if (changed) writeSnapshot(project.file, {
|
|
127
|
-
...snapshot,
|
|
128
|
-
version: Math.max(snapshot.version, ...sealed.map((record) => record.version || 0)),
|
|
129
|
-
records: sealed,
|
|
130
|
-
});
|
|
131
|
-
return sealed;
|
|
533
|
+
durable.writeJson(file, normalizeSnapshot(snapshot, snapshot.project, true));
|
|
132
534
|
}
|
|
133
535
|
|
|
134
|
-
function publish(project,
|
|
536
|
+
function publish(project, deltaInput, mutationIdInput) {
|
|
135
537
|
const replica = require('../state/replicas').getReplicaByResourceId(project.resourceId);
|
|
136
538
|
if (!replica || replica.contract !== STREAM_CONTRACT) {
|
|
137
539
|
throw invalid('code project stream is not attached', 409);
|
|
138
540
|
}
|
|
541
|
+
const operation = normalizeOperation({
|
|
542
|
+
kind: 'code.publish',
|
|
543
|
+
project: project.resourceId,
|
|
544
|
+
delta: deltaInput,
|
|
545
|
+
}, project.resourceId);
|
|
546
|
+
const publicationMutationId = mutationId(mutationIdInput, 'publication mutation');
|
|
139
547
|
return require('../state/mutations').commitLocalMutation({
|
|
140
548
|
channelId: `resource:${project.resourceId}`,
|
|
141
|
-
localResource:
|
|
549
|
+
localResource: `${LOCAL_RESOURCE_PREFIX}${project.resourceId}`,
|
|
142
550
|
sharedResourceId: project.resourceId,
|
|
143
|
-
mutationId:
|
|
551
|
+
mutationId: publicationMutationId,
|
|
144
552
|
deviceId: require('./identity').machineId(),
|
|
145
553
|
authorityEpoch: replica.authorityEpoch,
|
|
146
554
|
baseCloudVersion: replica.appliedVersion,
|
|
147
555
|
contract: STREAM_CONTRACT,
|
|
148
|
-
schemaVersion:
|
|
556
|
+
schemaVersion: 2,
|
|
149
557
|
operationKind: 'code.publish',
|
|
150
|
-
operation
|
|
558
|
+
operation,
|
|
151
559
|
}, {
|
|
152
|
-
|
|
153
|
-
writeSnapshot(
|
|
154
|
-
project.file,
|
|
155
|
-
applyRecord(readSnapshot(project.file, project.repoKey), record),
|
|
156
|
-
);
|
|
157
|
-
},
|
|
560
|
+
// Outbound intent deliberately does not mutate the official snapshot.
|
|
158
561
|
event: {
|
|
159
|
-
resource:
|
|
562
|
+
resource: `${LOCAL_RESOURCE_PREFIX}${project.resourceId}`,
|
|
160
563
|
op: 'update',
|
|
161
564
|
id: project.file,
|
|
162
565
|
sharedResourceId: project.resourceId,
|
|
163
|
-
value:
|
|
164
|
-
source: 'code-project:
|
|
566
|
+
value: operation.delta,
|
|
567
|
+
source: 'code-project:outbox',
|
|
165
568
|
},
|
|
166
569
|
});
|
|
167
570
|
}
|
|
168
571
|
|
|
169
|
-
function seal(project, mutationId, version) {
|
|
170
|
-
if (!Number.isSafeInteger(Number(version)) || Number(version) < 1) return false;
|
|
171
|
-
const snapshot = readSnapshot(project.file, project.repoKey);
|
|
172
|
-
let changed = false;
|
|
173
|
-
const records = snapshot.records.map((record) => {
|
|
174
|
-
if (record.mutationId !== mutationId || record.version === Number(version)) return record;
|
|
175
|
-
changed = true;
|
|
176
|
-
return { ...record, version: Number(version) };
|
|
177
|
-
});
|
|
178
|
-
if (!changed) return false;
|
|
179
|
-
writeSnapshot(project.file, {
|
|
180
|
-
...snapshot,
|
|
181
|
-
version: Math.max(snapshot.version, Number(version)),
|
|
182
|
-
records,
|
|
183
|
-
});
|
|
184
|
-
appendStateEvent({
|
|
185
|
-
resource: localResource(project.resourceId),
|
|
186
|
-
op: 'update',
|
|
187
|
-
id: project.file,
|
|
188
|
-
sharedResourceId: project.resourceId,
|
|
189
|
-
resourceVersion: Number(version),
|
|
190
|
-
source: 'code-project:ack',
|
|
191
|
-
});
|
|
192
|
-
return true;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
572
|
function decodeSnapshot(input) {
|
|
196
|
-
if (input?.contract !== STREAM_CONTRACT || Number(input?.schemaVersion) !==
|
|
573
|
+
if (input?.contract !== STREAM_CONTRACT || Number(input?.schemaVersion) !== 2) {
|
|
197
574
|
throw invalid('unsupported code project stream', 409);
|
|
198
575
|
}
|
|
199
576
|
const bytes = Buffer.from(String(input.snapshotBase64 || ''), 'base64');
|
|
@@ -207,72 +584,122 @@ function decodeSnapshot(input) {
|
|
|
207
584
|
} catch {
|
|
208
585
|
throw invalid('code project snapshot must be JSON', 409);
|
|
209
586
|
}
|
|
210
|
-
const snapshot = normalizeSnapshot(parsed,
|
|
587
|
+
const snapshot = normalizeSnapshot(parsed, input.resourceId, true);
|
|
211
588
|
if (snapshot.version !== Number(input.snapshotVersion)) {
|
|
212
589
|
throw invalid('code project snapshot version mismatch', 409);
|
|
213
590
|
}
|
|
214
|
-
return {
|
|
591
|
+
return { snapshot };
|
|
215
592
|
}
|
|
216
593
|
|
|
217
594
|
function installSnapshot(file, input, options = {}) {
|
|
218
595
|
const database = options.database || openLocalDb();
|
|
219
|
-
const { snapshot } = decodeSnapshot(input);
|
|
220
596
|
const replicas = require('../state/replicas');
|
|
597
|
+
const envelope = replicas.validateSnapshotEnvelope(input, {
|
|
598
|
+
contract: STREAM_CONTRACT,
|
|
599
|
+
schemaVersion: 2,
|
|
600
|
+
});
|
|
601
|
+
const { snapshot } = decodeSnapshot(input);
|
|
221
602
|
const existing = replicas.getReplicaByResourceId(input.resourceId, { database });
|
|
222
603
|
if (existing && existing.localPath !== file) {
|
|
223
604
|
throw invalid('code project stream is bound to another local file', 409);
|
|
224
605
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
606
|
+
const current = fs.existsSync(file)
|
|
607
|
+
? readSnapshot(file, input.resourceId)
|
|
608
|
+
: emptySnapshot(input.resourceId);
|
|
609
|
+
if (snapshot.version < current.version) {
|
|
610
|
+
const checked = existing
|
|
611
|
+
? replicas.advanceReplicaSnapshot({
|
|
612
|
+
resourceId: envelope.resourceId,
|
|
613
|
+
snapshotVersion: envelope.snapshotVersion,
|
|
614
|
+
snapshotChecksum: input.snapshotChecksum,
|
|
615
|
+
authorityEpoch: envelope.authorityEpoch,
|
|
616
|
+
}, { database })
|
|
617
|
+
: existing;
|
|
618
|
+
return { replica: checked, duplicate: true, ignored: true };
|
|
619
|
+
}
|
|
620
|
+
if (snapshot.version === current.version) {
|
|
621
|
+
if (JSON.stringify(snapshot) !== JSON.stringify(current)) {
|
|
622
|
+
throw invalid(
|
|
623
|
+
'equal code project snapshot versions contain different state',
|
|
624
|
+
409,
|
|
625
|
+
'code_project_snapshot_mismatch',
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
if (existing) {
|
|
629
|
+
const replica = replicas.advanceReplicaSnapshot({
|
|
630
|
+
resourceId: input.resourceId,
|
|
631
|
+
snapshotVersion: envelope.snapshotVersion,
|
|
632
|
+
snapshotChecksum: input.snapshotChecksum,
|
|
633
|
+
authorityEpoch: envelope.authorityEpoch,
|
|
634
|
+
}, { database });
|
|
635
|
+
completeInstalledSnapshot(input.resourceId, snapshot.version, database);
|
|
636
|
+
return { replica, duplicate: true };
|
|
233
637
|
}
|
|
234
638
|
}
|
|
235
|
-
writeSnapshot(file,
|
|
639
|
+
writeSnapshot(file, snapshot);
|
|
236
640
|
const registered = existing
|
|
237
641
|
? {
|
|
238
642
|
replica: replicas.advanceReplicaSnapshot({
|
|
239
643
|
resourceId: input.resourceId,
|
|
240
|
-
snapshotVersion:
|
|
644
|
+
snapshotVersion: envelope.snapshotVersion,
|
|
241
645
|
snapshotChecksum: input.snapshotChecksum,
|
|
242
|
-
authorityEpoch:
|
|
646
|
+
authorityEpoch: envelope.authorityEpoch,
|
|
243
647
|
}, { database }),
|
|
244
648
|
}
|
|
245
649
|
: replicas.registerReplica({
|
|
246
650
|
resourceId: input.resourceId,
|
|
247
|
-
localResource:
|
|
651
|
+
localResource: `${LOCAL_RESOURCE_PREFIX}${input.resourceId}`,
|
|
248
652
|
localPath: file,
|
|
249
653
|
contract: STREAM_CONTRACT,
|
|
250
|
-
schemaVersion:
|
|
251
|
-
authorityEpoch:
|
|
252
|
-
appliedVersion:
|
|
253
|
-
snapshotVersion:
|
|
654
|
+
schemaVersion: 2,
|
|
655
|
+
authorityEpoch: envelope.authorityEpoch,
|
|
656
|
+
appliedVersion: envelope.snapshotVersion,
|
|
657
|
+
snapshotVersion: envelope.snapshotVersion,
|
|
254
658
|
snapshotChecksum: input.snapshotChecksum,
|
|
255
659
|
}, { database });
|
|
660
|
+
completeInstalledSnapshot(input.resourceId, snapshot.version, database);
|
|
256
661
|
appendStateEvent({
|
|
257
|
-
resource:
|
|
662
|
+
resource: `${LOCAL_RESOURCE_PREFIX}${input.resourceId}`,
|
|
258
663
|
op: 'update',
|
|
259
664
|
id: file,
|
|
260
665
|
sharedResourceId: input.resourceId,
|
|
261
|
-
resourceVersion:
|
|
666
|
+
resourceVersion: envelope.snapshotVersion,
|
|
262
667
|
source: 'code-project:snapshot',
|
|
263
668
|
});
|
|
264
669
|
return registered;
|
|
265
670
|
}
|
|
266
671
|
|
|
672
|
+
function completeInstalledSnapshot(resourceId, through, database) {
|
|
673
|
+
require('../state/mutations').completeOfficialSnapshotFold(
|
|
674
|
+
resourceId,
|
|
675
|
+
through,
|
|
676
|
+
{ database },
|
|
677
|
+
);
|
|
678
|
+
const project = require('./wall').projectForResource(resourceId);
|
|
679
|
+
if (!project) return;
|
|
680
|
+
const intents = require('./intent');
|
|
681
|
+
const held = intents.readIntent(project);
|
|
682
|
+
const acknowledgement = held
|
|
683
|
+
? intents.officialAcknowledgement(project, held)
|
|
684
|
+
: null;
|
|
685
|
+
if (acknowledgement && acknowledgement.version <= through) {
|
|
686
|
+
intents.markPublicationFolded(
|
|
687
|
+
project,
|
|
688
|
+
acknowledgement.mutationId,
|
|
689
|
+
acknowledgement.version,
|
|
690
|
+
held.publication.numberedAt,
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
267
695
|
function applyCloudMutation(input, options = {}) {
|
|
268
696
|
const envelope = input?.envelope || {};
|
|
269
|
-
const record = envelope.operation?.record;
|
|
270
697
|
if (
|
|
271
698
|
envelope.contract !== STREAM_CONTRACT
|
|
699
|
+
|| Number(envelope.schemaVersion) !== 2
|
|
272
700
|
|| envelope.operationKind !== 'code.publish'
|
|
273
|
-
|| envelope.operation?.kind !== 'code.publish'
|
|
274
|
-
|| record?.mutationId !== envelope.mutationId
|
|
275
701
|
) throw invalid('invalid code project cloud mutation', 409);
|
|
702
|
+
const operation = normalizeOperation(envelope.operation, envelope.resourceId);
|
|
276
703
|
const database = options.database || openLocalDb();
|
|
277
704
|
const replica = require('../state/replicas').getReplicaByResourceId(
|
|
278
705
|
envelope.resourceId,
|
|
@@ -283,41 +710,62 @@ function applyCloudMutation(input, options = {}) {
|
|
|
283
710
|
}
|
|
284
711
|
return require('../state/mutations').ingestCloudMutation({
|
|
285
712
|
...input,
|
|
286
|
-
localResource:
|
|
713
|
+
localResource: `${LOCAL_RESOURCE_PREFIX}${envelope.resourceId}`,
|
|
287
714
|
}, {
|
|
288
715
|
database,
|
|
716
|
+
requireOfficialMaterialization: true,
|
|
289
717
|
materialize() {
|
|
290
|
-
|
|
291
|
-
replica.localPath,
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
718
|
+
const next = fold(
|
|
719
|
+
readSnapshot(replica.localPath, envelope.resourceId),
|
|
720
|
+
operation,
|
|
721
|
+
{
|
|
722
|
+
machine: envelope.deviceId,
|
|
723
|
+
mutationId: envelope.mutationId,
|
|
724
|
+
version: Number(input.version),
|
|
725
|
+
},
|
|
297
726
|
);
|
|
727
|
+
writeSnapshot(replica.localPath, next);
|
|
728
|
+
const project = require('./wall').projectForResource(envelope.resourceId);
|
|
729
|
+
if (project) {
|
|
730
|
+
require('./intent').markPublicationFolded(
|
|
731
|
+
project,
|
|
732
|
+
envelope.mutationId,
|
|
733
|
+
Number(input.version),
|
|
734
|
+
input.committedAt,
|
|
735
|
+
);
|
|
736
|
+
}
|
|
298
737
|
},
|
|
299
738
|
event: {
|
|
300
|
-
resource:
|
|
739
|
+
resource: `${LOCAL_RESOURCE_PREFIX}${envelope.resourceId}`,
|
|
301
740
|
op: 'update',
|
|
302
741
|
id: replica.localPath,
|
|
303
742
|
sharedResourceId: envelope.resourceId,
|
|
304
743
|
resourceVersion: Number(input.version),
|
|
305
|
-
value:
|
|
744
|
+
value: operation.delta,
|
|
306
745
|
source: 'code-project:cloud',
|
|
307
746
|
},
|
|
308
747
|
});
|
|
309
748
|
}
|
|
310
749
|
|
|
311
750
|
module.exports = {
|
|
751
|
+
BUNDLE_CONTRACT,
|
|
752
|
+
CARD_CONTRACT,
|
|
753
|
+
CHECKPOINT_CONTRACT,
|
|
312
754
|
LOCAL_RESOURCE_PREFIX,
|
|
755
|
+
PAYLOAD_CONTRACT,
|
|
313
756
|
STREAM_CONTRACT,
|
|
314
757
|
WALL_CONTRACT,
|
|
315
|
-
_test: {
|
|
758
|
+
_test: {
|
|
759
|
+
emptySnapshot,
|
|
760
|
+
fold,
|
|
761
|
+
normalizeDelta,
|
|
762
|
+
normalizeOperation,
|
|
763
|
+
normalizeSnapshot,
|
|
764
|
+
},
|
|
316
765
|
applyCloudMutation,
|
|
766
|
+
emptySnapshot,
|
|
317
767
|
installSnapshot,
|
|
318
768
|
publish,
|
|
319
769
|
readSnapshot,
|
|
320
|
-
records,
|
|
321
|
-
seal,
|
|
322
770
|
writeSnapshot,
|
|
323
771
|
};
|