amalgm 0.1.220 → 0.1.234

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.
@@ -0,0 +1,323 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const fs = require('fs');
5
+ const { appendStateEvent } = require('../state/events');
6
+ const { openLocalDb } = require('../state/db');
7
+
8
+ const STREAM_CONTRACT = 'code-project-stream@1';
9
+ const WALL_CONTRACT = 'code-project@6';
10
+ const LOCAL_RESOURCE_PREFIX = 'code-project:';
11
+
12
+ function invalid(message, statusCode = 400) {
13
+ return Object.assign(new Error(message), { statusCode });
14
+ }
15
+
16
+ function emptySnapshot(project) {
17
+ return { schemaVersion: 1, project, version: 0, records: [] };
18
+ }
19
+
20
+ function localResource(resourceId) {
21
+ return `${LOCAL_RESOURCE_PREFIX}${resourceId}`;
22
+ }
23
+
24
+ function normalizeRecord(input, project, official = false) {
25
+ if (
26
+ !input
27
+ || input.wall !== WALL_CONTRACT
28
+ || input.project !== project
29
+ || typeof input.machine !== 'string'
30
+ || !input.machine
31
+ || typeof input.mutationId !== 'string'
32
+ || !input.mutationId
33
+ || typeof input.publishedAt !== 'string'
34
+ || !Number.isFinite(Date.parse(input.publishedAt))
35
+ || !Array.isArray(input.cards)
36
+ ) throw invalid('code project record is invalid', 409);
37
+ const version = Number(input.version);
38
+ if (official && (!Number.isSafeInteger(version) || version < 1)) {
39
+ throw invalid('code project record has no official version', 409);
40
+ }
41
+ return {
42
+ ...input,
43
+ ...(Number.isSafeInteger(version) && version > 0 ? { version } : {}),
44
+ };
45
+ }
46
+
47
+ function normalizeSnapshot(input, project = null, official = false) {
48
+ if (
49
+ !input
50
+ || input.schemaVersion !== 1
51
+ || typeof input.project !== 'string'
52
+ || !input.project
53
+ || !Number.isSafeInteger(Number(input.version))
54
+ || Number(input.version) < 0
55
+ || !Array.isArray(input.records)
56
+ || (project && input.project !== project)
57
+ ) throw invalid('code project snapshot is invalid', 409);
58
+ const machines = new Set();
59
+ const records = input.records.map((record) => {
60
+ const normalized = normalizeRecord(record, input.project, official);
61
+ if (official && normalized.version > Number(input.version)) {
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);
66
+ }
67
+ machines.add(normalized.machine);
68
+ return normalized;
69
+ });
70
+ return {
71
+ schemaVersion: 1,
72
+ project: input.project,
73
+ version: Number(input.version),
74
+ records,
75
+ };
76
+ }
77
+
78
+ function readSnapshot(file, project) {
79
+ if (!fs.existsSync(file) || fs.statSync(file).size === 0) return emptySnapshot(project);
80
+ try {
81
+ return normalizeSnapshot(JSON.parse(fs.readFileSync(file, 'utf8')), project);
82
+ } catch (error) {
83
+ if (error?.statusCode) throw error;
84
+ throw invalid('code project snapshot is not JSON', 409);
85
+ }
86
+ }
87
+
88
+ function writeSnapshot(file, snapshot) {
89
+ fs.mkdirSync(require('path').dirname(file), { recursive: true });
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;
132
+ }
133
+
134
+ function publish(project, record) {
135
+ const replica = require('../state/replicas').getReplicaByResourceId(project.resourceId);
136
+ if (!replica || replica.contract !== STREAM_CONTRACT) {
137
+ throw invalid('code project stream is not attached', 409);
138
+ }
139
+ return require('../state/mutations').commitLocalMutation({
140
+ channelId: `resource:${project.resourceId}`,
141
+ localResource: localResource(project.resourceId),
142
+ sharedResourceId: project.resourceId,
143
+ mutationId: record.mutationId,
144
+ deviceId: require('./identity').machineId(),
145
+ authorityEpoch: replica.authorityEpoch,
146
+ baseCloudVersion: replica.appliedVersion,
147
+ contract: STREAM_CONTRACT,
148
+ schemaVersion: 1,
149
+ operationKind: 'code.publish',
150
+ operation: { kind: 'code.publish', record },
151
+ }, {
152
+ materialize() {
153
+ writeSnapshot(
154
+ project.file,
155
+ applyRecord(readSnapshot(project.file, project.repoKey), record),
156
+ );
157
+ },
158
+ event: {
159
+ resource: localResource(project.resourceId),
160
+ op: 'update',
161
+ id: project.file,
162
+ sharedResourceId: project.resourceId,
163
+ value: record,
164
+ source: 'code-project:local',
165
+ },
166
+ });
167
+ }
168
+
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
+ function decodeSnapshot(input) {
196
+ if (input?.contract !== STREAM_CONTRACT || Number(input?.schemaVersion) !== 1) {
197
+ throw invalid('unsupported code project stream', 409);
198
+ }
199
+ const bytes = Buffer.from(String(input.snapshotBase64 || ''), 'base64');
200
+ if (
201
+ !bytes.length
202
+ || crypto.createHash('sha256').update(bytes).digest('hex') !== input.snapshotChecksum
203
+ ) throw invalid('code project snapshot checksum mismatch', 409);
204
+ let parsed;
205
+ try {
206
+ parsed = JSON.parse(bytes.toString('utf8'));
207
+ } catch {
208
+ throw invalid('code project snapshot must be JSON', 409);
209
+ }
210
+ const snapshot = normalizeSnapshot(parsed, null, true);
211
+ if (snapshot.version !== Number(input.snapshotVersion)) {
212
+ throw invalid('code project snapshot version mismatch', 409);
213
+ }
214
+ return { bytes, snapshot };
215
+ }
216
+
217
+ function installSnapshot(file, input, options = {}) {
218
+ const database = options.database || openLocalDb();
219
+ const { snapshot } = decodeSnapshot(input);
220
+ const replicas = require('../state/replicas');
221
+ const existing = replicas.getReplicaByResourceId(input.resourceId, { database });
222
+ if (existing && existing.localPath !== file) {
223
+ throw invalid('code project stream is bound to another local file', 409);
224
+ }
225
+ let installed = snapshot;
226
+ if (existing) {
227
+ for (const mutation of require('../state/mutations').listPendingCloudMutations(
228
+ input.resourceId,
229
+ { database },
230
+ )) {
231
+ if (mutation.contract !== STREAM_CONTRACT || mutation.operationKind !== 'code.publish') continue;
232
+ installed = applyRecord(installed, mutation.operation?.record);
233
+ }
234
+ }
235
+ writeSnapshot(file, installed);
236
+ const registered = existing
237
+ ? {
238
+ replica: replicas.advanceReplicaSnapshot({
239
+ resourceId: input.resourceId,
240
+ snapshotVersion: input.snapshotVersion,
241
+ snapshotChecksum: input.snapshotChecksum,
242
+ authorityEpoch: input.authorityEpoch,
243
+ }, { database }),
244
+ }
245
+ : replicas.registerReplica({
246
+ resourceId: input.resourceId,
247
+ localResource: localResource(input.resourceId),
248
+ localPath: file,
249
+ contract: STREAM_CONTRACT,
250
+ schemaVersion: 1,
251
+ authorityEpoch: input.authorityEpoch,
252
+ appliedVersion: input.snapshotVersion,
253
+ snapshotVersion: input.snapshotVersion,
254
+ snapshotChecksum: input.snapshotChecksum,
255
+ }, { database });
256
+ appendStateEvent({
257
+ resource: localResource(input.resourceId),
258
+ op: 'update',
259
+ id: file,
260
+ sharedResourceId: input.resourceId,
261
+ resourceVersion: Number(input.snapshotVersion),
262
+ source: 'code-project:snapshot',
263
+ });
264
+ return registered;
265
+ }
266
+
267
+ function applyCloudMutation(input, options = {}) {
268
+ const envelope = input?.envelope || {};
269
+ const record = envelope.operation?.record;
270
+ if (
271
+ envelope.contract !== STREAM_CONTRACT
272
+ || envelope.operationKind !== 'code.publish'
273
+ || envelope.operation?.kind !== 'code.publish'
274
+ || record?.mutationId !== envelope.mutationId
275
+ ) throw invalid('invalid code project cloud mutation', 409);
276
+ const database = options.database || openLocalDb();
277
+ const replica = require('../state/replicas').getReplicaByResourceId(
278
+ envelope.resourceId,
279
+ { database },
280
+ );
281
+ if (!replica || replica.contract !== STREAM_CONTRACT) {
282
+ throw invalid('unknown code project stream', 404);
283
+ }
284
+ return require('../state/mutations').ingestCloudMutation({
285
+ ...input,
286
+ localResource: localResource(envelope.resourceId),
287
+ }, {
288
+ database,
289
+ materialize() {
290
+ writeSnapshot(
291
+ replica.localPath,
292
+ applyRecord(
293
+ readSnapshot(replica.localPath, record.project),
294
+ record,
295
+ Number(input.version),
296
+ ),
297
+ );
298
+ },
299
+ event: {
300
+ resource: localResource(envelope.resourceId),
301
+ op: 'update',
302
+ id: replica.localPath,
303
+ sharedResourceId: envelope.resourceId,
304
+ resourceVersion: Number(input.version),
305
+ value: record,
306
+ source: 'code-project:cloud',
307
+ },
308
+ });
309
+ }
310
+
311
+ module.exports = {
312
+ LOCAL_RESOURCE_PREFIX,
313
+ STREAM_CONTRACT,
314
+ WALL_CONTRACT,
315
+ _test: { applyRecord, emptySnapshot, normalizeSnapshot },
316
+ applyCloudMutation,
317
+ installSnapshot,
318
+ publish,
319
+ readSnapshot,
320
+ records,
321
+ seal,
322
+ writeSnapshot,
323
+ };