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
|
@@ -16,6 +16,8 @@ const { cachedStatement, openLocalDb } = require('./db');
|
|
|
16
16
|
const { insertStateEvent, publishStateEvent } = require('./events');
|
|
17
17
|
const {
|
|
18
18
|
diagnosticOperation,
|
|
19
|
+
mutationNeedsOfficialMaterialization,
|
|
20
|
+
mutationNeedsOrderedEcho,
|
|
19
21
|
protectOperation,
|
|
20
22
|
revealOperation,
|
|
21
23
|
} = require('./mutation-contracts');
|
|
@@ -174,10 +176,12 @@ function normalizeJournalRow(row, options = {}) {
|
|
|
174
176
|
state: row.mutation_state,
|
|
175
177
|
localRevision: row.local_revision == null ? null : Number(row.local_revision),
|
|
176
178
|
materializedAt: row.materialized_at || null,
|
|
179
|
+
officialProjectedAt: row.official_projected_at || null,
|
|
177
180
|
eventSeq: row.event_seq == null ? null : Number(row.event_seq),
|
|
178
181
|
attemptCount: Number(row.attempt_count || 0),
|
|
179
182
|
nextAttemptAt: row.next_attempt_at || null,
|
|
180
183
|
lastError: row.last_error || null,
|
|
184
|
+
failureCode: row.failure_code || null,
|
|
181
185
|
cloudCommittedAt: row.cloud_committed_at || null,
|
|
182
186
|
createdAt: row.created_at,
|
|
183
187
|
updatedAt: row.updated_at,
|
|
@@ -224,66 +228,67 @@ function sameCloudEnvelope(existing, mutation) {
|
|
|
224
228
|
&& JSON.stringify(existingOperation) === mutation.operationJson;
|
|
225
229
|
}
|
|
226
230
|
|
|
227
|
-
function
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
if (!sameMutation(existing, mutation)) {
|
|
236
|
-
throw invalid(
|
|
237
|
-
`mutationId ${mutation.mutationId} was reused with different content`,
|
|
238
|
-
409,
|
|
239
|
-
);
|
|
240
|
-
}
|
|
241
|
-
return { mutation: normalizeJournalRow(existing), duplicate: true };
|
|
231
|
+
function journalNormalizedMutation(database, mutation, persist = null) {
|
|
232
|
+
const existing = getMutationRow(database, mutation.channelId, mutation.mutationId);
|
|
233
|
+
if (existing) {
|
|
234
|
+
if (!sameMutation(existing, mutation)) {
|
|
235
|
+
throw invalid(
|
|
236
|
+
`mutationId ${mutation.mutationId} was reused with different content`,
|
|
237
|
+
409,
|
|
238
|
+
);
|
|
242
239
|
}
|
|
240
|
+
return { mutation: normalizeJournalRow(existing), duplicate: true };
|
|
241
|
+
}
|
|
243
242
|
|
|
244
|
-
|
|
243
|
+
if (persist) persist(database, mutation);
|
|
244
|
+
|
|
245
|
+
const info = cachedStatement(database, `
|
|
246
|
+
INSERT INTO mutation_journal (
|
|
247
|
+
channel_id,
|
|
248
|
+
local_resource,
|
|
249
|
+
shared_resource_id,
|
|
250
|
+
mutation_id,
|
|
251
|
+
device_id,
|
|
252
|
+
authority_epoch,
|
|
253
|
+
base_cloud_version,
|
|
254
|
+
contract,
|
|
255
|
+
schema_version,
|
|
256
|
+
operation_kind,
|
|
257
|
+
operation_json,
|
|
258
|
+
mutation_origin,
|
|
259
|
+
mutation_state,
|
|
260
|
+
local_revision
|
|
261
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'local', 'saving-local', ?)
|
|
262
|
+
`).run(
|
|
263
|
+
mutation.channelId,
|
|
264
|
+
mutation.localResource,
|
|
265
|
+
mutation.sharedResourceId,
|
|
266
|
+
mutation.mutationId,
|
|
267
|
+
mutation.deviceId,
|
|
268
|
+
mutation.authorityEpoch,
|
|
269
|
+
mutation.baseCloudVersion,
|
|
270
|
+
mutation.contract,
|
|
271
|
+
mutation.schemaVersion,
|
|
272
|
+
mutation.operationKind,
|
|
273
|
+
JSON.stringify(protectOperation(mutation.contract, mutation.operation)),
|
|
274
|
+
mutation.localRevision,
|
|
275
|
+
);
|
|
245
276
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
contract,
|
|
256
|
-
schema_version,
|
|
257
|
-
operation_kind,
|
|
258
|
-
operation_json,
|
|
259
|
-
mutation_origin,
|
|
260
|
-
mutation_state,
|
|
261
|
-
local_revision
|
|
262
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'local', 'saving-local', ?)
|
|
263
|
-
`).run(
|
|
264
|
-
mutation.channelId,
|
|
265
|
-
mutation.localResource,
|
|
266
|
-
mutation.sharedResourceId,
|
|
267
|
-
mutation.mutationId,
|
|
268
|
-
mutation.deviceId,
|
|
269
|
-
mutation.authorityEpoch,
|
|
270
|
-
mutation.baseCloudVersion,
|
|
271
|
-
mutation.contract,
|
|
272
|
-
mutation.schemaVersion,
|
|
273
|
-
mutation.operationKind,
|
|
274
|
-
JSON.stringify(protectOperation(mutation.contract, mutation.operation)),
|
|
275
|
-
mutation.localRevision,
|
|
276
|
-
);
|
|
277
|
+
const row = cachedStatement(database, 'SELECT * FROM mutation_journal WHERE journal_id = ?')
|
|
278
|
+
.get(Number(info.lastInsertRowid));
|
|
279
|
+
trace('journal', {
|
|
280
|
+
mutationId: mutation.mutationId,
|
|
281
|
+
channel: mutation.channelId,
|
|
282
|
+
shared: mutation.sharedResourceId,
|
|
283
|
+
});
|
|
284
|
+
return { mutation: normalizeJournalRow(row), duplicate: false };
|
|
285
|
+
}
|
|
277
286
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
shared: mutation.sharedResourceId,
|
|
284
|
-
});
|
|
285
|
-
return { mutation: normalizeJournalRow(row), duplicate: false };
|
|
286
|
-
})();
|
|
287
|
+
function journalMutation(input, options = {}) {
|
|
288
|
+
const mutation = normalizeMutationInput(input);
|
|
289
|
+
const database = options.database || openLocalDb();
|
|
290
|
+
const persist = typeof options.persist === 'function' ? options.persist : null;
|
|
291
|
+
return database.transaction(() => journalNormalizedMutation(database, mutation, persist))();
|
|
287
292
|
}
|
|
288
293
|
|
|
289
294
|
function recordMaterializationError(database, channelId, mutationId, error) {
|
|
@@ -307,7 +312,7 @@ function advanceReplicaCursor(database, sharedResourceId) {
|
|
|
307
312
|
WHERE shared_resource_id = ?
|
|
308
313
|
AND official_cloud_version = ?
|
|
309
314
|
AND mutation_state = 'synced'
|
|
310
|
-
AND
|
|
315
|
+
AND official_projected_at IS NOT NULL
|
|
311
316
|
LIMIT 1
|
|
312
317
|
`);
|
|
313
318
|
const update = cachedStatement(database, `
|
|
@@ -332,61 +337,168 @@ function advanceReplicaCursor(database, sharedResourceId) {
|
|
|
332
337
|
return replica ? Number(replica.applied_version) : null;
|
|
333
338
|
}
|
|
334
339
|
|
|
340
|
+
function completeMaterializedInTransaction(database, channelId, mutationId, eventInput) {
|
|
341
|
+
const row = getMutationRow(database, channelId, mutationId);
|
|
342
|
+
if (!row) throw invalid(`unknown mutation ${mutationId}`, 404);
|
|
343
|
+
if (row.event_seq != null) {
|
|
344
|
+
return { mutation: normalizeJournalRow(row), duplicate: true, event: null };
|
|
345
|
+
}
|
|
346
|
+
if (row.mutation_state !== 'saving-local') {
|
|
347
|
+
throw invalid(`mutation ${mutationId} cannot materialize from ${row.mutation_state}`, 409);
|
|
348
|
+
}
|
|
349
|
+
if (!eventInput || eventInput.resource !== row.local_resource) {
|
|
350
|
+
throw invalid('materialized event resource must match the journal localResource');
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const event = insertStateEvent(database, {
|
|
354
|
+
...eventInput,
|
|
355
|
+
mutationId: row.mutation_id,
|
|
356
|
+
sharedResourceId: row.shared_resource_id,
|
|
357
|
+
});
|
|
358
|
+
const now = new Date().toISOString();
|
|
359
|
+
const nextState = row.mutation_origin === 'cloud' ? 'synced' : 'saved-local';
|
|
360
|
+
const officialProjectedAt = row.mutation_origin === 'cloud' ? now : null;
|
|
361
|
+
cachedStatement(database, `
|
|
362
|
+
UPDATE mutation_journal
|
|
363
|
+
SET mutation_state = ?,
|
|
364
|
+
materialized_at = ?,
|
|
365
|
+
official_projected_at = ?,
|
|
366
|
+
event_seq = ?,
|
|
367
|
+
last_error = NULL,
|
|
368
|
+
failure_code = NULL,
|
|
369
|
+
updated_at = ?
|
|
370
|
+
WHERE journal_id = ?
|
|
371
|
+
`).run(nextState, now, officialProjectedAt, event.seq, now, row.journal_id);
|
|
372
|
+
|
|
373
|
+
if (row.mutation_origin === 'cloud') {
|
|
374
|
+
advanceReplicaCursor(database, row.shared_resource_id);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
trace('materialized', {
|
|
378
|
+
mutationId: row.mutation_id,
|
|
379
|
+
origin: row.mutation_origin,
|
|
380
|
+
state: nextState,
|
|
381
|
+
eventSeq: event.seq,
|
|
382
|
+
});
|
|
383
|
+
return {
|
|
384
|
+
mutation: normalizeJournalRow(
|
|
385
|
+
cachedStatement(database, 'SELECT * FROM mutation_journal WHERE journal_id = ?')
|
|
386
|
+
.get(row.journal_id),
|
|
387
|
+
),
|
|
388
|
+
duplicate: false,
|
|
389
|
+
event,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
335
393
|
function completeMaterializedMutation(channelIdInput, mutationIdInput, eventInput, options = {}) {
|
|
336
394
|
const channelId = cleanText(channelIdInput, 'channelId');
|
|
337
395
|
const mutationId = cleanText(mutationIdInput, 'mutationId', 160);
|
|
338
396
|
const database = options.database || openLocalDb();
|
|
339
|
-
|
|
397
|
+
const result = database.transaction(() => {
|
|
398
|
+
const completed = completeMaterializedInTransaction(
|
|
399
|
+
database,
|
|
400
|
+
channelId,
|
|
401
|
+
mutationId,
|
|
402
|
+
eventInput,
|
|
403
|
+
);
|
|
404
|
+
if (typeof options.finalize === 'function') {
|
|
405
|
+
options.finalize(database, completed);
|
|
406
|
+
}
|
|
407
|
+
return completed;
|
|
408
|
+
})();
|
|
409
|
+
if (result.event) publishStateEvent(result.event);
|
|
410
|
+
return result;
|
|
411
|
+
}
|
|
340
412
|
|
|
413
|
+
/**
|
|
414
|
+
* A code-project outbox event is not the official materialization. When that
|
|
415
|
+
* same mutation returns in the numbered cloud tail, project it once more,
|
|
416
|
+
* then move the replica cursor. Repeating this after a crash is safe because
|
|
417
|
+
* the contract materializer folds by official snapshot version.
|
|
418
|
+
*/
|
|
419
|
+
function completeOfficialMaterialization(
|
|
420
|
+
channelIdInput,
|
|
421
|
+
mutationIdInput,
|
|
422
|
+
eventInput,
|
|
423
|
+
options = {},
|
|
424
|
+
) {
|
|
425
|
+
const channelId = cleanText(channelIdInput, 'channelId');
|
|
426
|
+
const mutationId = cleanText(mutationIdInput, 'mutationId', 160);
|
|
427
|
+
const database = options.database || openLocalDb();
|
|
428
|
+
let event = null;
|
|
341
429
|
const result = database.transaction(() => {
|
|
430
|
+
const finalize = (completed) => {
|
|
431
|
+
if (typeof options.finalize === 'function') {
|
|
432
|
+
options.finalize(database, completed);
|
|
433
|
+
}
|
|
434
|
+
return completed;
|
|
435
|
+
};
|
|
342
436
|
const row = getMutationRow(database, channelId, mutationId);
|
|
343
437
|
if (!row) throw invalid(`unknown mutation ${mutationId}`, 404);
|
|
344
|
-
if (
|
|
345
|
-
|
|
438
|
+
if (
|
|
439
|
+
row.mutation_origin !== 'local'
|
|
440
|
+
|| row.mutation_state !== 'synced'
|
|
441
|
+
|| row.official_cloud_version == null
|
|
442
|
+
) throw invalid(`mutation ${mutationId} has no official projection`, 409);
|
|
443
|
+
const replica = cachedStatement(
|
|
444
|
+
database,
|
|
445
|
+
'SELECT applied_version FROM shared_resource_replicas WHERE resource_id = ?',
|
|
446
|
+
).get(row.shared_resource_id);
|
|
447
|
+
const appliedVersion = Number(replica?.applied_version);
|
|
448
|
+
const officialVersion = Number(row.official_cloud_version);
|
|
449
|
+
if (row.official_projected_at != null || appliedVersion >= officialVersion) {
|
|
450
|
+
if (row.official_projected_at == null) {
|
|
451
|
+
const now = new Date().toISOString();
|
|
452
|
+
cachedStatement(database, `
|
|
453
|
+
UPDATE mutation_journal
|
|
454
|
+
SET official_projected_at = ?,
|
|
455
|
+
updated_at = ?
|
|
456
|
+
WHERE journal_id = ?
|
|
457
|
+
`).run(now, now, row.journal_id);
|
|
458
|
+
}
|
|
459
|
+
advanceReplicaCursor(database, row.shared_resource_id);
|
|
460
|
+
return finalize({
|
|
461
|
+
mutation: normalizeJournalRow(
|
|
462
|
+
cachedStatement(database, 'SELECT * FROM mutation_journal WHERE journal_id = ?')
|
|
463
|
+
.get(row.journal_id),
|
|
464
|
+
),
|
|
465
|
+
duplicate: true,
|
|
466
|
+
});
|
|
346
467
|
}
|
|
347
|
-
if (
|
|
348
|
-
|
|
468
|
+
if (officialVersion !== appliedVersion + 1) {
|
|
469
|
+
const error = invalid(
|
|
470
|
+
`shared mutation gap for ${row.shared_resource_id}: expected ${appliedVersion + 1}, received ${officialVersion}`,
|
|
471
|
+
409,
|
|
472
|
+
);
|
|
473
|
+
error.code = 'shared_mutation_gap';
|
|
474
|
+
error.expectedVersion = appliedVersion + 1;
|
|
475
|
+
error.receivedVersion = officialVersion;
|
|
476
|
+
throw error;
|
|
349
477
|
}
|
|
350
478
|
if (!eventInput || eventInput.resource !== row.local_resource) {
|
|
351
479
|
throw invalid('materialized event resource must match the journal localResource');
|
|
352
480
|
}
|
|
353
|
-
|
|
354
481
|
event = insertStateEvent(database, {
|
|
355
482
|
...eventInput,
|
|
356
483
|
mutationId: row.mutation_id,
|
|
357
484
|
sharedResourceId: row.shared_resource_id,
|
|
358
485
|
});
|
|
359
486
|
const now = new Date().toISOString();
|
|
360
|
-
const nextState = row.mutation_origin === 'cloud' ? 'synced' : 'saved-local';
|
|
361
487
|
cachedStatement(database, `
|
|
362
488
|
UPDATE mutation_journal
|
|
363
|
-
SET
|
|
364
|
-
materialized_at = ?,
|
|
365
|
-
event_seq = ?,
|
|
366
|
-
last_error = NULL,
|
|
489
|
+
SET official_projected_at = ?,
|
|
367
490
|
updated_at = ?
|
|
368
491
|
WHERE journal_id = ?
|
|
369
|
-
`).run(
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
advanceReplicaCursor(database, row.shared_resource_id);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
trace('materialized', {
|
|
376
|
-
mutationId: row.mutation_id,
|
|
377
|
-
origin: row.mutation_origin,
|
|
378
|
-
state: nextState,
|
|
379
|
-
eventSeq: event.seq,
|
|
380
|
-
});
|
|
381
|
-
return {
|
|
492
|
+
`).run(now, now, row.journal_id);
|
|
493
|
+
advanceReplicaCursor(database, row.shared_resource_id);
|
|
494
|
+
return finalize({
|
|
382
495
|
mutation: normalizeJournalRow(
|
|
383
496
|
cachedStatement(database, 'SELECT * FROM mutation_journal WHERE journal_id = ?')
|
|
384
497
|
.get(row.journal_id),
|
|
385
498
|
),
|
|
386
499
|
duplicate: false,
|
|
387
|
-
};
|
|
500
|
+
});
|
|
388
501
|
})();
|
|
389
|
-
|
|
390
502
|
if (event) publishStateEvent(event);
|
|
391
503
|
return { ...result, event };
|
|
392
504
|
}
|
|
@@ -411,6 +523,7 @@ function journalCloudMutation(input, options = {}) {
|
|
|
411
523
|
const version = positiveInteger(input?.version, 'version');
|
|
412
524
|
const committedAt = cleanText(input?.committedAt, 'committedAt', 64);
|
|
413
525
|
const database = options.database || openLocalDb();
|
|
526
|
+
const persist = typeof options.persist === 'function' ? options.persist : null;
|
|
414
527
|
|
|
415
528
|
return database.transaction(() => {
|
|
416
529
|
const replica = cachedStatement(
|
|
@@ -440,22 +553,60 @@ function journalCloudMutation(input, options = {}) {
|
|
|
440
553
|
throw invalid(`mutationId ${mutation.mutationId} was reused with different content`, 409);
|
|
441
554
|
}
|
|
442
555
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
556
|
+
if (existing.mutation_origin === 'local') {
|
|
557
|
+
const appliedVersion = Number(replica.applied_version);
|
|
558
|
+
if (version > appliedVersion && version !== appliedVersion + 1) {
|
|
559
|
+
const error = invalid(
|
|
560
|
+
`shared mutation gap for ${sharedResourceId}: expected ${appliedVersion + 1}, received ${version}`,
|
|
561
|
+
409,
|
|
562
|
+
);
|
|
563
|
+
error.code = 'shared_mutation_gap';
|
|
564
|
+
error.expectedVersion = appliedVersion + 1;
|
|
565
|
+
error.receivedVersion = version;
|
|
566
|
+
throw error;
|
|
567
|
+
}
|
|
568
|
+
if (existing.official_cloud_version == null) {
|
|
569
|
+
const now = new Date().toISOString();
|
|
570
|
+
cachedStatement(database, `
|
|
571
|
+
UPDATE mutation_journal
|
|
572
|
+
SET mutation_state = 'synced',
|
|
573
|
+
official_cloud_version = ?,
|
|
574
|
+
cloud_committed_at = ?,
|
|
575
|
+
next_attempt_at = NULL,
|
|
576
|
+
last_error = NULL,
|
|
577
|
+
failure_code = NULL,
|
|
578
|
+
updated_at = ?
|
|
579
|
+
WHERE journal_id = ?
|
|
580
|
+
`).run(version, committedAt, now, existing.journal_id);
|
|
581
|
+
}
|
|
582
|
+
if (!mutationNeedsOrderedEcho(existing.contract)) {
|
|
583
|
+
const now = new Date().toISOString();
|
|
584
|
+
cachedStatement(database, `
|
|
585
|
+
UPDATE mutation_journal
|
|
586
|
+
SET official_projected_at = COALESCE(official_projected_at, ?),
|
|
587
|
+
updated_at = ?
|
|
588
|
+
WHERE journal_id = ?
|
|
589
|
+
`).run(now, now, existing.journal_id);
|
|
590
|
+
advanceReplicaCursor(database, sharedResourceId);
|
|
591
|
+
const reconciled = cachedStatement(
|
|
592
|
+
database,
|
|
593
|
+
'SELECT * FROM mutation_journal WHERE journal_id = ?',
|
|
594
|
+
).get(existing.journal_id);
|
|
595
|
+
return {
|
|
596
|
+
mutation: normalizeJournalRow(reconciled),
|
|
597
|
+
duplicate: true,
|
|
598
|
+
alreadyApplied: true,
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
if (appliedVersion >= version && existing.official_projected_at == null) {
|
|
602
|
+
const now = new Date().toISOString();
|
|
603
|
+
cachedStatement(database, `
|
|
604
|
+
UPDATE mutation_journal
|
|
605
|
+
SET official_projected_at = ?,
|
|
606
|
+
updated_at = ?
|
|
607
|
+
WHERE journal_id = ?
|
|
608
|
+
`).run(now, now, existing.journal_id);
|
|
609
|
+
}
|
|
459
610
|
const reconciled = cachedStatement(
|
|
460
611
|
database,
|
|
461
612
|
'SELECT * FROM mutation_journal WHERE journal_id = ?',
|
|
@@ -463,14 +614,15 @@ function journalCloudMutation(input, options = {}) {
|
|
|
463
614
|
return {
|
|
464
615
|
mutation: normalizeJournalRow(reconciled),
|
|
465
616
|
duplicate: true,
|
|
466
|
-
alreadyApplied:
|
|
617
|
+
alreadyApplied: appliedVersion >= version,
|
|
618
|
+
officialMaterialization: appliedVersion < version,
|
|
467
619
|
};
|
|
468
620
|
}
|
|
469
621
|
|
|
470
622
|
return {
|
|
471
623
|
mutation: normalizeJournalRow(existing),
|
|
472
624
|
duplicate: true,
|
|
473
|
-
alreadyApplied: existing.
|
|
625
|
+
alreadyApplied: existing.official_projected_at != null,
|
|
474
626
|
};
|
|
475
627
|
}
|
|
476
628
|
|
|
@@ -490,6 +642,7 @@ function journalCloudMutation(input, options = {}) {
|
|
|
490
642
|
}
|
|
491
643
|
|
|
492
644
|
const now = new Date().toISOString();
|
|
645
|
+
if (persist) persist(database, mutation);
|
|
493
646
|
const info = cachedStatement(database, `
|
|
494
647
|
INSERT INTO mutation_journal (
|
|
495
648
|
channel_id,
|
|
@@ -541,7 +694,10 @@ function journalCloudMutation(input, options = {}) {
|
|
|
541
694
|
|
|
542
695
|
function ingestCloudMutation(input, options = {}) {
|
|
543
696
|
const database = options.database || openLocalDb();
|
|
544
|
-
const started = journalCloudMutation(input, {
|
|
697
|
+
const started = journalCloudMutation(input, {
|
|
698
|
+
database,
|
|
699
|
+
persist: options.persist,
|
|
700
|
+
});
|
|
545
701
|
if (started.alreadyApplied) return { ...started, event: null };
|
|
546
702
|
|
|
547
703
|
try {
|
|
@@ -558,17 +714,56 @@ function ingestCloudMutation(input, options = {}) {
|
|
|
558
714
|
const eventInput = typeof options.event === 'function'
|
|
559
715
|
? options.event(started.mutation)
|
|
560
716
|
: options.event;
|
|
561
|
-
const completed =
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
717
|
+
const completed = started.officialMaterialization
|
|
718
|
+
? completeOfficialMaterialization(
|
|
719
|
+
started.mutation.channelId,
|
|
720
|
+
started.mutation.mutationId,
|
|
721
|
+
eventInput,
|
|
722
|
+
{ database, finalize: options.finalize },
|
|
723
|
+
)
|
|
724
|
+
: completeMaterializedMutation(
|
|
725
|
+
started.mutation.channelId,
|
|
726
|
+
started.mutation.mutationId,
|
|
727
|
+
eventInput,
|
|
728
|
+
{ database, finalize: options.finalize },
|
|
729
|
+
);
|
|
567
730
|
return { ...completed, duplicate: started.duplicate || completed.duplicate };
|
|
568
731
|
}
|
|
569
732
|
|
|
570
733
|
function commitLocalMutation(input, options = {}) {
|
|
571
734
|
const database = options.database || openLocalDb();
|
|
735
|
+
if (mutationNeedsOfficialMaterialization(input?.contract)) {
|
|
736
|
+
if (typeof options.materialize === 'function') {
|
|
737
|
+
throw invalid('code publication cannot materialize official state before cloud order', 409);
|
|
738
|
+
}
|
|
739
|
+
const mutation = normalizeMutationInput(input);
|
|
740
|
+
const persist = typeof options.persist === 'function' ? options.persist : null;
|
|
741
|
+
const result = database.transaction(() => {
|
|
742
|
+
const started = journalNormalizedMutation(database, mutation, persist);
|
|
743
|
+
if (started.duplicate && started.mutation.state !== 'saving-local') {
|
|
744
|
+
return { ...started, event: null };
|
|
745
|
+
}
|
|
746
|
+
const eventInput = typeof options.event === 'function'
|
|
747
|
+
? options.event(started.mutation)
|
|
748
|
+
: options.event;
|
|
749
|
+
const completed = completeMaterializedInTransaction(
|
|
750
|
+
database,
|
|
751
|
+
started.mutation.channelId,
|
|
752
|
+
started.mutation.mutationId,
|
|
753
|
+
eventInput,
|
|
754
|
+
);
|
|
755
|
+
if (typeof options.finalize === 'function') {
|
|
756
|
+
options.finalize(database, completed);
|
|
757
|
+
}
|
|
758
|
+
return {
|
|
759
|
+
...completed,
|
|
760
|
+
duplicate: started.duplicate || completed.duplicate,
|
|
761
|
+
};
|
|
762
|
+
})();
|
|
763
|
+
if (result.event) publishStateEvent(result.event);
|
|
764
|
+
return result;
|
|
765
|
+
}
|
|
766
|
+
|
|
572
767
|
const started = journalMutation(input, { database, persist: options.persist });
|
|
573
768
|
if (started.duplicate && started.mutation.state !== 'saving-local') {
|
|
574
769
|
return { ...started, event: null };
|
|
@@ -595,7 +790,7 @@ function commitLocalMutation(input, options = {}) {
|
|
|
595
790
|
started.mutation.channelId,
|
|
596
791
|
started.mutation.mutationId,
|
|
597
792
|
eventInput,
|
|
598
|
-
{ database },
|
|
793
|
+
{ database, finalize: options.finalize },
|
|
599
794
|
);
|
|
600
795
|
return {
|
|
601
796
|
...completed,
|
|
@@ -723,10 +918,10 @@ function acknowledgeCloudMutation(sharedResourceIdInput, mutationIdInput, acknow
|
|
|
723
918
|
cloud_committed_at = ?,
|
|
724
919
|
next_attempt_at = NULL,
|
|
725
920
|
last_error = NULL,
|
|
921
|
+
failure_code = NULL,
|
|
726
922
|
updated_at = ?
|
|
727
923
|
WHERE journal_id = ?
|
|
728
924
|
`).run(version, committedAt, now, row.journal_id);
|
|
729
|
-
advanceReplicaCursor(database, sharedResourceId);
|
|
730
925
|
event = insertStateEvent(database, {
|
|
731
926
|
resource: 'shared-mutations',
|
|
732
927
|
op: 'ack',
|
|
@@ -746,6 +941,17 @@ function acknowledgeCloudMutation(sharedResourceIdInput, mutationIdInput, acknow
|
|
|
746
941
|
};
|
|
747
942
|
})();
|
|
748
943
|
if (event) publishStateEvent(event);
|
|
944
|
+
if (result.mutation?.contract === require('./mutation-contracts').CODE_PROJECT_CONTRACT) {
|
|
945
|
+
const project = require('../workspace/wall').projectForResource(sharedResourceId);
|
|
946
|
+
if (project) {
|
|
947
|
+
require('../workspace/intent').markPublicationNumbered(
|
|
948
|
+
project,
|
|
949
|
+
mutationId,
|
|
950
|
+
version,
|
|
951
|
+
committedAt,
|
|
952
|
+
);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
749
955
|
return { ...result, event };
|
|
750
956
|
}
|
|
751
957
|
|
|
@@ -754,6 +960,10 @@ function failCloudMutation(sharedResourceIdInput, mutationIdInput, failure, opti
|
|
|
754
960
|
const mutationId = cleanText(mutationIdInput, 'mutationId', 160);
|
|
755
961
|
const message = cleanText(failure?.message || failure || 'cloud mutation failed', 'message', 4000);
|
|
756
962
|
const retryable = failure?.retryable !== false;
|
|
963
|
+
const failureCode = typeof failure?.code === 'string'
|
|
964
|
+
&& /^[a-z][a-z0-9_:-]{0,127}$/.test(failure.code)
|
|
965
|
+
? failure.code
|
|
966
|
+
: null;
|
|
757
967
|
const retryAt = retryable
|
|
758
968
|
? (failure?.retryAt || new Date(Date.now() + 1000).toISOString())
|
|
759
969
|
: null;
|
|
@@ -768,9 +978,17 @@ function failCloudMutation(sharedResourceIdInput, mutationIdInput, failure, opti
|
|
|
768
978
|
SET mutation_state = ?,
|
|
769
979
|
next_attempt_at = ?,
|
|
770
980
|
last_error = ?,
|
|
981
|
+
failure_code = ?,
|
|
771
982
|
updated_at = ?
|
|
772
983
|
WHERE journal_id = ?
|
|
773
|
-
`).run(
|
|
984
|
+
`).run(
|
|
985
|
+
retryable ? 'syncing' : 'rejected',
|
|
986
|
+
retryAt,
|
|
987
|
+
message,
|
|
988
|
+
failureCode,
|
|
989
|
+
now,
|
|
990
|
+
row.journal_id,
|
|
991
|
+
);
|
|
774
992
|
trace('cloud-fail', { mutationId, shared: sharedResourceId, retryable, message });
|
|
775
993
|
return normalizeJournalRow(
|
|
776
994
|
cachedStatement(database, 'SELECT * FROM mutation_journal WHERE journal_id = ?')
|
|
@@ -799,18 +1017,38 @@ function markMutationConflicted(sharedResourceIdInput, mutationIdInput, messageI
|
|
|
799
1017
|
function listMutations(options = {}) {
|
|
800
1018
|
const database = options.database || openLocalDb();
|
|
801
1019
|
const limit = Math.max(1, Math.min(Number(options.limit) || 100, 1000));
|
|
802
|
-
const
|
|
803
|
-
?
|
|
1020
|
+
const localResource = options.localResource == null
|
|
1021
|
+
? null
|
|
1022
|
+
: cleanText(options.localResource, 'localResource', 8192);
|
|
1023
|
+
let rows;
|
|
1024
|
+
if (options.state && localResource) {
|
|
1025
|
+
rows = cachedStatement(database, `
|
|
1026
|
+
SELECT * FROM mutation_journal
|
|
1027
|
+
WHERE mutation_state = ? AND local_resource = ?
|
|
1028
|
+
ORDER BY journal_id ASC
|
|
1029
|
+
LIMIT ?
|
|
1030
|
+
`).all(options.state, localResource, limit);
|
|
1031
|
+
} else if (options.state) {
|
|
1032
|
+
rows = cachedStatement(database, `
|
|
804
1033
|
SELECT * FROM mutation_journal
|
|
805
1034
|
WHERE mutation_state = ?
|
|
806
1035
|
ORDER BY journal_id ASC
|
|
807
1036
|
LIMIT ?
|
|
808
|
-
`).all(options.state, limit)
|
|
809
|
-
|
|
1037
|
+
`).all(options.state, limit);
|
|
1038
|
+
} else if (localResource) {
|
|
1039
|
+
rows = cachedStatement(database, `
|
|
1040
|
+
SELECT * FROM mutation_journal
|
|
1041
|
+
WHERE local_resource = ?
|
|
1042
|
+
ORDER BY journal_id ASC
|
|
1043
|
+
LIMIT ?
|
|
1044
|
+
`).all(localResource, limit);
|
|
1045
|
+
} else {
|
|
1046
|
+
rows = cachedStatement(database, `
|
|
810
1047
|
SELECT * FROM mutation_journal
|
|
811
1048
|
ORDER BY journal_id ASC
|
|
812
1049
|
LIMIT ?
|
|
813
1050
|
`).all(limit);
|
|
1051
|
+
}
|
|
814
1052
|
return rows.map((row) => normalizeJournalRow(row, { diagnostic: true }));
|
|
815
1053
|
}
|
|
816
1054
|
|
|
@@ -834,6 +1072,120 @@ function listPendingCloudMutations(resourceIdInput, options = {}) {
|
|
|
834
1072
|
`).all(resourceId).map((row) => normalizeJournalRow(row));
|
|
835
1073
|
}
|
|
836
1074
|
|
|
1075
|
+
/**
|
|
1076
|
+
* Local overlay rows are every unnumbered mutation plus numbered local work
|
|
1077
|
+
* whose official event has not yet folded into the materialized resource.
|
|
1078
|
+
* Snapshot projection must retain the complete set, so this view is
|
|
1079
|
+
* deliberately unpaged.
|
|
1080
|
+
*/
|
|
1081
|
+
function listLocalOverlayMutations(resourceIdInput, options = {}) {
|
|
1082
|
+
const resourceId = cleanText(resourceIdInput, 'resourceId', 132);
|
|
1083
|
+
if (!isCloudResourceId(resourceId)) {
|
|
1084
|
+
throw invalid('resourceId must be an opaque cloud resource id');
|
|
1085
|
+
}
|
|
1086
|
+
const database = options.database || openLocalDb();
|
|
1087
|
+
const throughVersion = options.throughVersion == null
|
|
1088
|
+
? null
|
|
1089
|
+
: nonNegativeInteger(options.throughVersion, 'throughVersion');
|
|
1090
|
+
if (throughVersion !== null) {
|
|
1091
|
+
return cachedStatement(database, `
|
|
1092
|
+
SELECT * FROM mutation_journal
|
|
1093
|
+
WHERE shared_resource_id = ?
|
|
1094
|
+
AND mutation_origin = 'local'
|
|
1095
|
+
AND (
|
|
1096
|
+
mutation_state != 'synced'
|
|
1097
|
+
OR official_cloud_version IS NULL
|
|
1098
|
+
OR official_cloud_version > ?
|
|
1099
|
+
)
|
|
1100
|
+
ORDER BY journal_id ASC
|
|
1101
|
+
`).all(resourceId, throughVersion).map((row) => normalizeJournalRow(row));
|
|
1102
|
+
}
|
|
1103
|
+
return cachedStatement(database, `
|
|
1104
|
+
SELECT * FROM mutation_journal
|
|
1105
|
+
WHERE shared_resource_id = ?
|
|
1106
|
+
AND mutation_origin = 'local'
|
|
1107
|
+
AND (mutation_state != 'synced' OR official_projected_at IS NULL)
|
|
1108
|
+
ORDER BY journal_id ASC
|
|
1109
|
+
`).all(resourceId).map((row) => normalizeJournalRow(row));
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
function listUnappliedOfficialMutations(resourceIdInput, options = {}) {
|
|
1113
|
+
const resourceId = cleanText(resourceIdInput, 'resourceId', 132);
|
|
1114
|
+
if (!isCloudResourceId(resourceId)) {
|
|
1115
|
+
throw invalid('resourceId must be an opaque cloud resource id');
|
|
1116
|
+
}
|
|
1117
|
+
const database = options.database || openLocalDb();
|
|
1118
|
+
const replica = cachedStatement(
|
|
1119
|
+
database,
|
|
1120
|
+
'SELECT applied_version FROM shared_resource_replicas WHERE resource_id = ?',
|
|
1121
|
+
).get(resourceId);
|
|
1122
|
+
const appliedVersion = Number(replica?.applied_version ?? -1);
|
|
1123
|
+
return cachedStatement(database, `
|
|
1124
|
+
SELECT *
|
|
1125
|
+
FROM mutation_journal
|
|
1126
|
+
WHERE shared_resource_id = ?
|
|
1127
|
+
AND mutation_origin = 'local'
|
|
1128
|
+
AND mutation_state = 'synced'
|
|
1129
|
+
AND official_cloud_version > ?
|
|
1130
|
+
ORDER BY official_cloud_version ASC
|
|
1131
|
+
`).all(resourceId, appliedVersion)
|
|
1132
|
+
.filter((row) => mutationNeedsOfficialMaterialization(row.contract))
|
|
1133
|
+
.map((row) => normalizeJournalRow(row));
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
/**
|
|
1137
|
+
* A verified code snapshot through N is the ordinary ordered fold for every
|
|
1138
|
+
* already-numbered local publication at or before N. This repairs the same
|
|
1139
|
+
* durable fact on an equal snapshot retry after a crash.
|
|
1140
|
+
*/
|
|
1141
|
+
function completeOfficialSnapshotFold(resourceIdInput, throughInput, options = {}) {
|
|
1142
|
+
const resourceId = cleanText(resourceIdInput, 'resourceId', 132);
|
|
1143
|
+
const through = nonNegativeInteger(throughInput, 'through');
|
|
1144
|
+
const database = options.database || openLocalDb();
|
|
1145
|
+
const contract = cleanText(
|
|
1146
|
+
options.contract || require('./mutation-contracts').CODE_PROJECT_CONTRACT,
|
|
1147
|
+
'contract',
|
|
1148
|
+
128,
|
|
1149
|
+
);
|
|
1150
|
+
const now = new Date().toISOString();
|
|
1151
|
+
return cachedStatement(database, `
|
|
1152
|
+
UPDATE mutation_journal
|
|
1153
|
+
SET official_projected_at = COALESCE(official_projected_at, ?),
|
|
1154
|
+
updated_at = ?
|
|
1155
|
+
WHERE shared_resource_id = ?
|
|
1156
|
+
AND contract = ?
|
|
1157
|
+
AND mutation_origin = 'local'
|
|
1158
|
+
AND mutation_state = 'synced'
|
|
1159
|
+
AND official_cloud_version <= ?
|
|
1160
|
+
`).run(
|
|
1161
|
+
now,
|
|
1162
|
+
now,
|
|
1163
|
+
resourceId,
|
|
1164
|
+
contract,
|
|
1165
|
+
through,
|
|
1166
|
+
).changes;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
/**
|
|
1170
|
+
* A stale proposal was never an official event. Keep it as a publication
|
|
1171
|
+
* barrier until this replica has actually advanced past the base it used,
|
|
1172
|
+
* then retire it so the unchanged local intent can be proposed anew.
|
|
1173
|
+
*/
|
|
1174
|
+
function retireStaleCodeMutations(resourceIdInput, appliedVersionInput, options = {}) {
|
|
1175
|
+
const resourceId = cleanText(resourceIdInput, 'resourceId', 132);
|
|
1176
|
+
const appliedVersion = nonNegativeInteger(appliedVersionInput, 'appliedVersion');
|
|
1177
|
+
const database = options.database || openLocalDb();
|
|
1178
|
+
return cachedStatement(database, `
|
|
1179
|
+
DELETE FROM mutation_journal
|
|
1180
|
+
WHERE shared_resource_id = ?
|
|
1181
|
+
AND mutation_origin = 'local'
|
|
1182
|
+
AND contract = ?
|
|
1183
|
+
AND mutation_state = 'rejected'
|
|
1184
|
+
AND failure_code = 'code_project_stale_base'
|
|
1185
|
+
AND base_cloud_version < ?
|
|
1186
|
+
`).run(resourceId, require('./mutation-contracts').CODE_PROJECT_CONTRACT, appliedVersion).changes;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
837
1189
|
function pruneMutationJournal(options = {}) {
|
|
838
1190
|
const database = options.database || openLocalDb();
|
|
839
1191
|
const hours = Number(options.retentionHours ?? process.env.AMALGM_EVENT_LOG_RETENTION_HOURS ?? 0.25);
|
|
@@ -843,23 +1195,44 @@ function pruneMutationJournal(options = {}) {
|
|
|
843
1195
|
const cutoffIso = options.cutoffIso
|
|
844
1196
|
|| new Date(Date.now() - hours * 60 * 60 * 1000).toISOString();
|
|
845
1197
|
const batchSize = Math.max(1, Math.floor(Number(options.batchSize) || 1000));
|
|
846
|
-
const
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
AND
|
|
854
|
-
|
|
855
|
-
|
|
1198
|
+
const readPage = cachedStatement(database, `
|
|
1199
|
+
SELECT *
|
|
1200
|
+
FROM mutation_journal
|
|
1201
|
+
WHERE journal_id > ?
|
|
1202
|
+
AND updated_at < ?
|
|
1203
|
+
AND event_seq IS NOT NULL
|
|
1204
|
+
AND (
|
|
1205
|
+
(shared_resource_id IS NULL AND mutation_state = 'saved-local')
|
|
1206
|
+
OR (
|
|
1207
|
+
shared_resource_id IS NOT NULL
|
|
1208
|
+
AND mutation_state = 'synced'
|
|
1209
|
+
AND official_projected_at IS NOT NULL
|
|
856
1210
|
)
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
`)
|
|
1211
|
+
)
|
|
1212
|
+
ORDER BY journal_id ASC
|
|
1213
|
+
LIMIT ?
|
|
1214
|
+
`);
|
|
1215
|
+
const { mutationMayPrune } = require('./mutation-contracts');
|
|
1216
|
+
const snapshotCache = new Map();
|
|
1217
|
+
const deletable = [];
|
|
1218
|
+
let afterJournalId = 0;
|
|
1219
|
+
while (deletable.length < batchSize) {
|
|
1220
|
+
const page = readPage.all(afterJournalId, cutoffIso, batchSize);
|
|
1221
|
+
if (page.length === 0) break;
|
|
1222
|
+
for (const row of page) {
|
|
1223
|
+
afterJournalId = Number(row.journal_id);
|
|
1224
|
+
if (mutationMayPrune(row, { database, snapshotCache })) {
|
|
1225
|
+
deletable.push(row.journal_id);
|
|
1226
|
+
if (deletable.length === batchSize) break;
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
const remove = cachedStatement(database, 'DELETE FROM mutation_journal WHERE journal_id = ?');
|
|
1231
|
+
const deleted = database.transaction((journalIds) => (
|
|
1232
|
+
journalIds.reduce((count, journalId) => count + remove.run(journalId).changes, 0)
|
|
1233
|
+
))(deletable);
|
|
861
1234
|
return {
|
|
862
|
-
deleted
|
|
1235
|
+
deleted,
|
|
863
1236
|
cutoffIso,
|
|
864
1237
|
retentionHours: hours,
|
|
865
1238
|
batchSize,
|
|
@@ -872,13 +1245,17 @@ module.exports = {
|
|
|
872
1245
|
claimCloudMutations,
|
|
873
1246
|
commitLocalMutation,
|
|
874
1247
|
completeMaterializedMutation,
|
|
1248
|
+
completeOfficialSnapshotFold,
|
|
875
1249
|
failCloudMutation,
|
|
876
1250
|
getMutation,
|
|
877
1251
|
ingestCloudMutation,
|
|
878
1252
|
journalCloudMutation,
|
|
879
1253
|
journalMutation,
|
|
1254
|
+
listLocalOverlayMutations,
|
|
1255
|
+
listUnappliedOfficialMutations,
|
|
880
1256
|
listPendingCloudMutations,
|
|
881
1257
|
listMutations,
|
|
882
1258
|
markMutationConflicted,
|
|
883
1259
|
pruneMutationJournal,
|
|
1260
|
+
retireStaleCodeMutations,
|
|
884
1261
|
};
|