amalgm 0.1.245 → 0.1.246
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -0
- package/lib/cli.js +146 -8
- package/lib/layout.js +89 -4
- package/lib/shared-realtime-tunnel.js +384 -43
- package/lib/supervisor.js +0 -3
- package/package.json +2 -2
- package/runtime/scripts/amalgm-mcp/adapters/contract.js +197 -0
- package/runtime/scripts/amalgm-mcp/adapters/filesystem.js +135 -0
- package/runtime/scripts/amalgm-mcp/adapters/git.js +84 -0
- package/runtime/scripts/amalgm-mcp/adapters/index.js +43 -0
- package/runtime/scripts/amalgm-mcp/adapters/reference.js +31 -0
- package/runtime/scripts/amalgm-mcp/adapters/truth.js +19 -0
- package/runtime/scripts/amalgm-mcp/browser/cookie-jar.js +20 -68
- package/runtime/scripts/amalgm-mcp/index.js +4 -0
- package/runtime/scripts/amalgm-mcp/lib/layout.js +89 -4
- package/runtime/scripts/amalgm-mcp/observer/README.md +259 -0
- package/runtime/scripts/amalgm-mcp/observer/apply.js +150 -0
- package/runtime/scripts/amalgm-mcp/observer/continuity.js +111 -0
- package/runtime/scripts/amalgm-mcp/observer/edges.js +111 -0
- package/runtime/scripts/amalgm-mcp/observer/index.js +1146 -0
- package/runtime/scripts/amalgm-mcp/observer/scan.js +387 -0
- package/runtime/scripts/amalgm-mcp/observer/store.js +262 -0
- package/runtime/scripts/amalgm-mcp/observer/verify.js +200 -0
- package/runtime/scripts/amalgm-mcp/observer/watch.js +62 -0
- package/runtime/scripts/amalgm-mcp/project-context/store.js +19 -87
- package/runtime/scripts/amalgm-mcp/registration/classify.js +33 -0
- package/runtime/scripts/amalgm-mcp/registration/entity-cloud.js +456 -0
- package/runtime/scripts/amalgm-mcp/registration/entity-content.js +345 -0
- package/runtime/scripts/amalgm-mcp/registration/index.js +752 -0
- package/runtime/scripts/amalgm-mcp/registration/refusal.js +42 -0
- package/runtime/scripts/amalgm-mcp/registration/repo-followers.js +127 -0
- package/runtime/scripts/amalgm-mcp/registration/repo-states.js +109 -0
- package/runtime/scripts/amalgm-mcp/registration/service.js +310 -0
- package/runtime/scripts/amalgm-mcp/registration/tree.js +178 -0
- package/runtime/scripts/amalgm-mcp/registry/evidence.js +826 -0
- package/runtime/scripts/amalgm-mcp/registry/index.js +666 -0
- package/runtime/scripts/amalgm-mcp/registry/store.js +290 -0
- package/runtime/scripts/amalgm-mcp/repocard/README.md +103 -0
- package/runtime/scripts/amalgm-mcp/repocard/apply.js +111 -0
- package/runtime/scripts/amalgm-mcp/repocard/capture-worker.js +94 -0
- package/runtime/scripts/amalgm-mcp/repocard/capture.js +149 -0
- package/runtime/scripts/amalgm-mcp/repocard/follow.js +310 -0
- package/runtime/scripts/amalgm-mcp/repocard/git.js +48 -0
- package/runtime/scripts/amalgm-mcp/repocard/index.js +48 -0
- package/runtime/scripts/amalgm-mcp/server/local-service-router.js +2 -0
- package/runtime/scripts/amalgm-mcp/server/routes/entities.js +287 -0
- package/runtime/scripts/amalgm-mcp/server/routes/state.js +0 -13
- package/runtime/scripts/amalgm-mcp/state/attachments.js +11 -45
- package/runtime/scripts/amalgm-mcp/state/db.js +46 -215
- package/runtime/scripts/amalgm-mcp/state/docs.js +301 -1101
- package/runtime/scripts/amalgm-mcp/state/mutation-contracts.js +0 -99
- package/runtime/scripts/amalgm-mcp/state/mutations.js +130 -507
- package/runtime/scripts/amalgm-mcp/state/promotions.js +1 -21
- package/runtime/scripts/amalgm-mcp/state/replicas.js +13 -134
- package/runtime/scripts/amalgm-mcp/state/shared-rest.js +3 -46
- package/runtime/scripts/amalgm-mcp/tests/adapters.test.js +379 -0
- package/runtime/scripts/amalgm-mcp/tests/browser-cookie-cloud.test.js +0 -193
- package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.life.test.js +449 -0
- package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.rig.js +107 -0
- package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.watch.test.js +213 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-oracle/adapter.js +72 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-oracle/assert.js +497 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-oracle/bind.js +91 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-oracle/graph.js +1031 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-oracle/ledger.js +234 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-oracle/referee.js +118 -0
- package/runtime/scripts/amalgm-mcp/tests/entity.oracle.test.js +1365 -0
- package/runtime/scripts/amalgm-mcp/tests/entity.registry.test.js +967 -0
- package/runtime/scripts/amalgm-mcp/tests/entity.rig.js +239 -0
- package/runtime/scripts/amalgm-mcp/tests/entity.storm.test.js +486 -0
- package/runtime/scripts/amalgm-mcp/tests/fake-watch.js +35 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.links.test.js +289 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.rig.js +103 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.roots.test.js +505 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.storm.test.js +389 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.territory.test.js +845 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.test.js +657 -0
- package/runtime/scripts/amalgm-mcp/tests/project-context.test.js +17 -32
- package/runtime/scripts/amalgm-mcp/tests/registration.service.test.js +256 -0
- package/runtime/scripts/amalgm-mcp/tests/registration.test.js +931 -0
- package/runtime/scripts/amalgm-mcp/tests/repocard.storm.test.js +284 -0
- package/runtime/scripts/amalgm-mcp/tests/repocard.test.js +371 -0
- package/runtime/scripts/amalgm-mcp/tests/repofollow.storm.test.js +435 -0
- package/runtime/scripts/amalgm-mcp/tests/repofollow.test.js +675 -0
- package/runtime/scripts/amalgm-mcp/tests/shared-tunnel-rehydration.test.js +0 -84
- package/runtime/scripts/amalgm-mcp/tests/state-docs.test.js +13 -376
- package/runtime/scripts/amalgm-mcp/tests/state-mutations.test.js +0 -309
- package/runtime/scripts/amalgm-mcp/tests/state-shared-replica.test.js +30 -758
- package/runtime/scripts/amalgm-mcp/tests/workspace-cards-store.test.js +3 -72
- package/runtime/scripts/amalgm-mcp/tests/workspace-cards.test.js +1 -89
- package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint-reducer.test.js +24 -55
- package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint.test.js +141 -424
- package/runtime/scripts/amalgm-mcp/tests/workspace-object-tunnel.test.js +3 -171
- package/runtime/scripts/amalgm-mcp/tests/workspace-objects.test.js +7 -220
- package/runtime/scripts/amalgm-mcp/tests/workspace-publication-invariants.test.js +236 -179
- package/runtime/scripts/amalgm-mcp/tests/workspace-reducer.test.js +326 -2228
- package/runtime/scripts/amalgm-mcp/tests/workspace-tree-cloud.test.js +3 -5319
- package/runtime/scripts/amalgm-mcp/tests/workspace-tree-core.test.js +1 -191
- package/runtime/scripts/amalgm-mcp/tests/workspace-tree-store.test.js +17 -3140
- package/runtime/scripts/amalgm-mcp/tests/workspace-wall.test.js +740 -137
- package/runtime/scripts/amalgm-mcp/workspace/access-store.js +1 -5
- package/runtime/scripts/amalgm-mcp/workspace/card.js +11 -35
- package/runtime/scripts/amalgm-mcp/workspace/cards.js +14 -39
- package/runtime/scripts/amalgm-mcp/workspace/checkpoint.js +147 -587
- package/runtime/scripts/amalgm-mcp/workspace/git.js +34 -406
- package/runtime/scripts/amalgm-mcp/workspace/merge.js +181 -102
- package/runtime/scripts/amalgm-mcp/workspace/objects.js +56 -221
- package/runtime/scripts/amalgm-mcp/workspace/reducer.js +538 -1964
- package/runtime/scripts/amalgm-mcp/workspace/rest.js +28 -49
- package/runtime/scripts/amalgm-mcp/workspace/store.js +4 -77
- package/runtime/scripts/amalgm-mcp/workspace/transition.js +192 -1734
- package/runtime/scripts/amalgm-mcp/workspace/tree/inclusion.js +1 -7
- package/runtime/scripts/amalgm-mcp/workspace/tree/local-state.js +61 -73
- package/runtime/scripts/amalgm-mcp/workspace/tree/reducer.js +6 -51
- package/runtime/scripts/amalgm-mcp/workspace/tree/snapshot.js +8 -79
- package/runtime/scripts/amalgm-mcp/workspace/tree-cloud.js +146 -3461
- package/runtime/scripts/amalgm-mcp/workspace/tree-store.js +71 -899
- package/runtime/scripts/amalgm-mcp/workspace/wall.js +538 -370
- package/runtime/scripts/local-gateway.js +1 -0
- package/runtime/scripts/amalgm-mcp/state/doc-disk.js +0 -550
- package/runtime/scripts/amalgm-mcp/tests/code-project-stream.test.js +0 -399
- package/runtime/scripts/amalgm-mcp/tests/fixtures/code-project-stream-v2.json +0 -1
- package/runtime/scripts/amalgm-mcp/tests/workspace-durable.test.js +0 -24
- package/runtime/scripts/amalgm-mcp/tests/workspace-local-intent.test.js +0 -1999
- package/runtime/scripts/amalgm-mcp/tests/workspace-native-file-transaction.test.js +0 -364
- package/runtime/scripts/amalgm-mcp/tests/workspace-provenance.test.js +0 -185
- package/runtime/scripts/amalgm-mcp/tests/workspace-reducer-liveness.test.js +0 -26
- package/runtime/scripts/amalgm-mcp/tests/workspace-sync-stats.test.js +0 -119
- package/runtime/scripts/amalgm-mcp/tests/workspace-transition.test.js +0 -1446
- package/runtime/scripts/amalgm-mcp/workspace/content.js +0 -32
- package/runtime/scripts/amalgm-mcp/workspace/durable.js +0 -45
- package/runtime/scripts/amalgm-mcp/workspace/intent.js +0 -535
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/BUILD.md +0 -31
- 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 +0 -72
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/client.js +0 -165
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.mod +0 -5
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.sum +0 -2
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/main.go +0 -2201
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/manifest.json +0 -19
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_darwin.go +0 -18
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_linux.go +0 -18
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_darwin.go +0 -33
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_linux.go +0 -47
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/verify.mjs +0 -43
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_darwin.go +0 -114
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_linux.go +0 -84
- package/runtime/scripts/amalgm-mcp/workspace/project-stream.js +0 -771
- package/runtime/scripts/amalgm-mcp/workspace/provenance.js +0 -240
- package/runtime/scripts/amalgm-mcp/workspace/sync-stats.js +0 -240
- package/runtime/scripts/amalgm-mcp/workspace/tree/floor.js +0 -100
- package/runtime/scripts/amalgm-mcp/workspace/tree/observation-holds.js +0 -127
- package/runtime/scripts/amalgm-mcp/workspace/tree/path-state.js +0 -12
- package/runtime/scripts/amalgm-mcp/workspace/tree/root-binding.js +0 -125
- package/runtime/scripts/amalgm-mcp/workspace/tree/symlinks.js +0 -76
- package/runtime/scripts/amalgm-mcp/workspace/tree-materialization.js +0 -3766
|
@@ -16,8 +16,6 @@ const { cachedStatement, openLocalDb } = require('./db');
|
|
|
16
16
|
const { insertStateEvent, publishStateEvent } = require('./events');
|
|
17
17
|
const {
|
|
18
18
|
diagnosticOperation,
|
|
19
|
-
mutationNeedsOfficialMaterialization,
|
|
20
|
-
mutationNeedsOrderedEcho,
|
|
21
19
|
protectOperation,
|
|
22
20
|
revealOperation,
|
|
23
21
|
} = require('./mutation-contracts');
|
|
@@ -176,12 +174,10 @@ function normalizeJournalRow(row, options = {}) {
|
|
|
176
174
|
state: row.mutation_state,
|
|
177
175
|
localRevision: row.local_revision == null ? null : Number(row.local_revision),
|
|
178
176
|
materializedAt: row.materialized_at || null,
|
|
179
|
-
officialProjectedAt: row.official_projected_at || null,
|
|
180
177
|
eventSeq: row.event_seq == null ? null : Number(row.event_seq),
|
|
181
178
|
attemptCount: Number(row.attempt_count || 0),
|
|
182
179
|
nextAttemptAt: row.next_attempt_at || null,
|
|
183
180
|
lastError: row.last_error || null,
|
|
184
|
-
failureCode: row.failure_code || null,
|
|
185
181
|
cloudCommittedAt: row.cloud_committed_at || null,
|
|
186
182
|
createdAt: row.created_at,
|
|
187
183
|
updatedAt: row.updated_at,
|
|
@@ -228,67 +224,66 @@ function sameCloudEnvelope(existing, mutation) {
|
|
|
228
224
|
&& JSON.stringify(existingOperation) === mutation.operationJson;
|
|
229
225
|
}
|
|
230
226
|
|
|
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
|
-
);
|
|
239
|
-
}
|
|
240
|
-
return { mutation: normalizeJournalRow(existing), duplicate: true };
|
|
241
|
-
}
|
|
242
|
-
|
|
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
|
-
);
|
|
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
|
-
}
|
|
286
|
-
|
|
287
227
|
function journalMutation(input, options = {}) {
|
|
288
228
|
const mutation = normalizeMutationInput(input);
|
|
289
229
|
const database = options.database || openLocalDb();
|
|
290
230
|
const persist = typeof options.persist === 'function' ? options.persist : null;
|
|
291
|
-
|
|
231
|
+
|
|
232
|
+
return database.transaction(() => {
|
|
233
|
+
const existing = getMutationRow(database, mutation.channelId, mutation.mutationId);
|
|
234
|
+
if (existing) {
|
|
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 };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (persist) persist(database, mutation);
|
|
245
|
+
|
|
246
|
+
const info = cachedStatement(database, `
|
|
247
|
+
INSERT INTO mutation_journal (
|
|
248
|
+
channel_id,
|
|
249
|
+
local_resource,
|
|
250
|
+
shared_resource_id,
|
|
251
|
+
mutation_id,
|
|
252
|
+
device_id,
|
|
253
|
+
authority_epoch,
|
|
254
|
+
base_cloud_version,
|
|
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
|
+
|
|
278
|
+
const row = cachedStatement(database, 'SELECT * FROM mutation_journal WHERE journal_id = ?')
|
|
279
|
+
.get(Number(info.lastInsertRowid));
|
|
280
|
+
trace('journal', {
|
|
281
|
+
mutationId: mutation.mutationId,
|
|
282
|
+
channel: mutation.channelId,
|
|
283
|
+
shared: mutation.sharedResourceId,
|
|
284
|
+
});
|
|
285
|
+
return { mutation: normalizeJournalRow(row), duplicate: false };
|
|
286
|
+
})();
|
|
292
287
|
}
|
|
293
288
|
|
|
294
289
|
function recordMaterializationError(database, channelId, mutationId, error) {
|
|
@@ -312,7 +307,7 @@ function advanceReplicaCursor(database, sharedResourceId) {
|
|
|
312
307
|
WHERE shared_resource_id = ?
|
|
313
308
|
AND official_cloud_version = ?
|
|
314
309
|
AND mutation_state = 'synced'
|
|
315
|
-
AND
|
|
310
|
+
AND materialized_at IS NOT NULL
|
|
316
311
|
LIMIT 1
|
|
317
312
|
`);
|
|
318
313
|
const update = cachedStatement(database, `
|
|
@@ -337,168 +332,61 @@ function advanceReplicaCursor(database, sharedResourceId) {
|
|
|
337
332
|
return replica ? Number(replica.applied_version) : null;
|
|
338
333
|
}
|
|
339
334
|
|
|
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
|
-
|
|
393
335
|
function completeMaterializedMutation(channelIdInput, mutationIdInput, eventInput, options = {}) {
|
|
394
|
-
const channelId = cleanText(channelIdInput, 'channelId');
|
|
395
|
-
const mutationId = cleanText(mutationIdInput, 'mutationId', 160);
|
|
396
|
-
const database = options.database || openLocalDb();
|
|
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
|
-
}
|
|
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
336
|
const channelId = cleanText(channelIdInput, 'channelId');
|
|
426
337
|
const mutationId = cleanText(mutationIdInput, 'mutationId', 160);
|
|
427
338
|
const database = options.database || openLocalDb();
|
|
428
339
|
let event = null;
|
|
340
|
+
|
|
429
341
|
const result = database.transaction(() => {
|
|
430
|
-
const finalize = (completed) => {
|
|
431
|
-
if (typeof options.finalize === 'function') {
|
|
432
|
-
options.finalize(database, completed);
|
|
433
|
-
}
|
|
434
|
-
return completed;
|
|
435
|
-
};
|
|
436
342
|
const row = getMutationRow(database, channelId, mutationId);
|
|
437
343
|
if (!row) throw invalid(`unknown mutation ${mutationId}`, 404);
|
|
438
|
-
if (
|
|
439
|
-
row
|
|
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
|
-
});
|
|
344
|
+
if (row.event_seq != null) {
|
|
345
|
+
return { mutation: normalizeJournalRow(row), duplicate: true };
|
|
467
346
|
}
|
|
468
|
-
if (
|
|
469
|
-
|
|
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;
|
|
347
|
+
if (row.mutation_state !== 'saving-local') {
|
|
348
|
+
throw invalid(`mutation ${mutationId} cannot materialize from ${row.mutation_state}`, 409);
|
|
477
349
|
}
|
|
478
350
|
if (!eventInput || eventInput.resource !== row.local_resource) {
|
|
479
351
|
throw invalid('materialized event resource must match the journal localResource');
|
|
480
352
|
}
|
|
353
|
+
|
|
481
354
|
event = insertStateEvent(database, {
|
|
482
355
|
...eventInput,
|
|
483
356
|
mutationId: row.mutation_id,
|
|
484
357
|
sharedResourceId: row.shared_resource_id,
|
|
485
358
|
});
|
|
486
359
|
const now = new Date().toISOString();
|
|
360
|
+
const nextState = row.mutation_origin === 'cloud' ? 'synced' : 'saved-local';
|
|
487
361
|
cachedStatement(database, `
|
|
488
362
|
UPDATE mutation_journal
|
|
489
|
-
SET
|
|
363
|
+
SET mutation_state = ?,
|
|
364
|
+
materialized_at = ?,
|
|
365
|
+
event_seq = ?,
|
|
366
|
+
last_error = NULL,
|
|
490
367
|
updated_at = ?
|
|
491
368
|
WHERE journal_id = ?
|
|
492
|
-
`).run(now, now, row.journal_id);
|
|
493
|
-
|
|
494
|
-
|
|
369
|
+
`).run(nextState, now, event.seq, now, row.journal_id);
|
|
370
|
+
|
|
371
|
+
if (row.mutation_origin === 'cloud') {
|
|
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 {
|
|
495
382
|
mutation: normalizeJournalRow(
|
|
496
383
|
cachedStatement(database, 'SELECT * FROM mutation_journal WHERE journal_id = ?')
|
|
497
384
|
.get(row.journal_id),
|
|
498
385
|
),
|
|
499
386
|
duplicate: false,
|
|
500
|
-
}
|
|
387
|
+
};
|
|
501
388
|
})();
|
|
389
|
+
|
|
502
390
|
if (event) publishStateEvent(event);
|
|
503
391
|
return { ...result, event };
|
|
504
392
|
}
|
|
@@ -523,7 +411,6 @@ function journalCloudMutation(input, options = {}) {
|
|
|
523
411
|
const version = positiveInteger(input?.version, 'version');
|
|
524
412
|
const committedAt = cleanText(input?.committedAt, 'committedAt', 64);
|
|
525
413
|
const database = options.database || openLocalDb();
|
|
526
|
-
const persist = typeof options.persist === 'function' ? options.persist : null;
|
|
527
414
|
|
|
528
415
|
return database.transaction(() => {
|
|
529
416
|
const replica = cachedStatement(
|
|
@@ -553,60 +440,22 @@ function journalCloudMutation(input, options = {}) {
|
|
|
553
440
|
throw invalid(`mutationId ${mutation.mutationId} was reused with different content`, 409);
|
|
554
441
|
}
|
|
555
442
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
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
|
-
}
|
|
443
|
+
// The cloud tail is also the lost-ack recovery path for this runtime's
|
|
444
|
+
// own optimistic mutation. Reconcile the official version onto the
|
|
445
|
+
// existing row; never create or materialize a second local edit.
|
|
446
|
+
if (existing.mutation_origin === 'local' && existing.official_cloud_version == null) {
|
|
447
|
+
const now = new Date().toISOString();
|
|
448
|
+
cachedStatement(database, `
|
|
449
|
+
UPDATE mutation_journal
|
|
450
|
+
SET mutation_state = 'synced',
|
|
451
|
+
official_cloud_version = ?,
|
|
452
|
+
cloud_committed_at = ?,
|
|
453
|
+
next_attempt_at = NULL,
|
|
454
|
+
last_error = NULL,
|
|
455
|
+
updated_at = ?
|
|
456
|
+
WHERE journal_id = ?
|
|
457
|
+
`).run(version, committedAt, now, existing.journal_id);
|
|
458
|
+
advanceReplicaCursor(database, sharedResourceId);
|
|
610
459
|
const reconciled = cachedStatement(
|
|
611
460
|
database,
|
|
612
461
|
'SELECT * FROM mutation_journal WHERE journal_id = ?',
|
|
@@ -614,15 +463,14 @@ function journalCloudMutation(input, options = {}) {
|
|
|
614
463
|
return {
|
|
615
464
|
mutation: normalizeJournalRow(reconciled),
|
|
616
465
|
duplicate: true,
|
|
617
|
-
alreadyApplied:
|
|
618
|
-
officialMaterialization: appliedVersion < version,
|
|
466
|
+
alreadyApplied: reconciled.event_seq != null,
|
|
619
467
|
};
|
|
620
468
|
}
|
|
621
469
|
|
|
622
470
|
return {
|
|
623
471
|
mutation: normalizeJournalRow(existing),
|
|
624
472
|
duplicate: true,
|
|
625
|
-
alreadyApplied: existing.
|
|
473
|
+
alreadyApplied: existing.event_seq != null,
|
|
626
474
|
};
|
|
627
475
|
}
|
|
628
476
|
|
|
@@ -642,7 +490,6 @@ function journalCloudMutation(input, options = {}) {
|
|
|
642
490
|
}
|
|
643
491
|
|
|
644
492
|
const now = new Date().toISOString();
|
|
645
|
-
if (persist) persist(database, mutation);
|
|
646
493
|
const info = cachedStatement(database, `
|
|
647
494
|
INSERT INTO mutation_journal (
|
|
648
495
|
channel_id,
|
|
@@ -694,10 +541,7 @@ function journalCloudMutation(input, options = {}) {
|
|
|
694
541
|
|
|
695
542
|
function ingestCloudMutation(input, options = {}) {
|
|
696
543
|
const database = options.database || openLocalDb();
|
|
697
|
-
const started = journalCloudMutation(input, {
|
|
698
|
-
database,
|
|
699
|
-
persist: options.persist,
|
|
700
|
-
});
|
|
544
|
+
const started = journalCloudMutation(input, { database });
|
|
701
545
|
if (started.alreadyApplied) return { ...started, event: null };
|
|
702
546
|
|
|
703
547
|
try {
|
|
@@ -714,56 +558,17 @@ function ingestCloudMutation(input, options = {}) {
|
|
|
714
558
|
const eventInput = typeof options.event === 'function'
|
|
715
559
|
? options.event(started.mutation)
|
|
716
560
|
: options.event;
|
|
717
|
-
const completed =
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
)
|
|
724
|
-
: completeMaterializedMutation(
|
|
725
|
-
started.mutation.channelId,
|
|
726
|
-
started.mutation.mutationId,
|
|
727
|
-
eventInput,
|
|
728
|
-
{ database, finalize: options.finalize },
|
|
729
|
-
);
|
|
561
|
+
const completed = completeMaterializedMutation(
|
|
562
|
+
started.mutation.channelId,
|
|
563
|
+
started.mutation.mutationId,
|
|
564
|
+
eventInput,
|
|
565
|
+
{ database },
|
|
566
|
+
);
|
|
730
567
|
return { ...completed, duplicate: started.duplicate || completed.duplicate };
|
|
731
568
|
}
|
|
732
569
|
|
|
733
570
|
function commitLocalMutation(input, options = {}) {
|
|
734
571
|
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
|
-
|
|
767
572
|
const started = journalMutation(input, { database, persist: options.persist });
|
|
768
573
|
if (started.duplicate && started.mutation.state !== 'saving-local') {
|
|
769
574
|
return { ...started, event: null };
|
|
@@ -790,7 +595,7 @@ function commitLocalMutation(input, options = {}) {
|
|
|
790
595
|
started.mutation.channelId,
|
|
791
596
|
started.mutation.mutationId,
|
|
792
597
|
eventInput,
|
|
793
|
-
{ database
|
|
598
|
+
{ database },
|
|
794
599
|
);
|
|
795
600
|
return {
|
|
796
601
|
...completed,
|
|
@@ -918,10 +723,10 @@ function acknowledgeCloudMutation(sharedResourceIdInput, mutationIdInput, acknow
|
|
|
918
723
|
cloud_committed_at = ?,
|
|
919
724
|
next_attempt_at = NULL,
|
|
920
725
|
last_error = NULL,
|
|
921
|
-
failure_code = NULL,
|
|
922
726
|
updated_at = ?
|
|
923
727
|
WHERE journal_id = ?
|
|
924
728
|
`).run(version, committedAt, now, row.journal_id);
|
|
729
|
+
advanceReplicaCursor(database, sharedResourceId);
|
|
925
730
|
event = insertStateEvent(database, {
|
|
926
731
|
resource: 'shared-mutations',
|
|
927
732
|
op: 'ack',
|
|
@@ -941,17 +746,6 @@ function acknowledgeCloudMutation(sharedResourceIdInput, mutationIdInput, acknow
|
|
|
941
746
|
};
|
|
942
747
|
})();
|
|
943
748
|
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
|
-
}
|
|
955
749
|
return { ...result, event };
|
|
956
750
|
}
|
|
957
751
|
|
|
@@ -960,10 +754,6 @@ function failCloudMutation(sharedResourceIdInput, mutationIdInput, failure, opti
|
|
|
960
754
|
const mutationId = cleanText(mutationIdInput, 'mutationId', 160);
|
|
961
755
|
const message = cleanText(failure?.message || failure || 'cloud mutation failed', 'message', 4000);
|
|
962
756
|
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;
|
|
967
757
|
const retryAt = retryable
|
|
968
758
|
? (failure?.retryAt || new Date(Date.now() + 1000).toISOString())
|
|
969
759
|
: null;
|
|
@@ -978,17 +768,9 @@ function failCloudMutation(sharedResourceIdInput, mutationIdInput, failure, opti
|
|
|
978
768
|
SET mutation_state = ?,
|
|
979
769
|
next_attempt_at = ?,
|
|
980
770
|
last_error = ?,
|
|
981
|
-
failure_code = ?,
|
|
982
771
|
updated_at = ?
|
|
983
772
|
WHERE journal_id = ?
|
|
984
|
-
`).run(
|
|
985
|
-
retryable ? 'syncing' : 'rejected',
|
|
986
|
-
retryAt,
|
|
987
|
-
message,
|
|
988
|
-
failureCode,
|
|
989
|
-
now,
|
|
990
|
-
row.journal_id,
|
|
991
|
-
);
|
|
773
|
+
`).run(retryable ? 'syncing' : 'rejected', retryAt, message, now, row.journal_id);
|
|
992
774
|
trace('cloud-fail', { mutationId, shared: sharedResourceId, retryable, message });
|
|
993
775
|
return normalizeJournalRow(
|
|
994
776
|
cachedStatement(database, 'SELECT * FROM mutation_journal WHERE journal_id = ?')
|
|
@@ -1017,38 +799,18 @@ function markMutationConflicted(sharedResourceIdInput, mutationIdInput, messageI
|
|
|
1017
799
|
function listMutations(options = {}) {
|
|
1018
800
|
const database = options.database || openLocalDb();
|
|
1019
801
|
const limit = Math.max(1, Math.min(Number(options.limit) || 100, 1000));
|
|
1020
|
-
const
|
|
1021
|
-
?
|
|
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, `
|
|
802
|
+
const rows = options.state
|
|
803
|
+
? cachedStatement(database, `
|
|
1033
804
|
SELECT * FROM mutation_journal
|
|
1034
805
|
WHERE mutation_state = ?
|
|
1035
806
|
ORDER BY journal_id ASC
|
|
1036
807
|
LIMIT ?
|
|
1037
|
-
`).all(options.state, limit)
|
|
1038
|
-
|
|
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, `
|
|
808
|
+
`).all(options.state, limit)
|
|
809
|
+
: cachedStatement(database, `
|
|
1047
810
|
SELECT * FROM mutation_journal
|
|
1048
811
|
ORDER BY journal_id ASC
|
|
1049
812
|
LIMIT ?
|
|
1050
813
|
`).all(limit);
|
|
1051
|
-
}
|
|
1052
814
|
return rows.map((row) => normalizeJournalRow(row, { diagnostic: true }));
|
|
1053
815
|
}
|
|
1054
816
|
|
|
@@ -1072,120 +834,6 @@ function listPendingCloudMutations(resourceIdInput, options = {}) {
|
|
|
1072
834
|
`).all(resourceId).map((row) => normalizeJournalRow(row));
|
|
1073
835
|
}
|
|
1074
836
|
|
|
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
|
-
|
|
1189
837
|
function pruneMutationJournal(options = {}) {
|
|
1190
838
|
const database = options.database || openLocalDb();
|
|
1191
839
|
const hours = Number(options.retentionHours ?? process.env.AMALGM_EVENT_LOG_RETENTION_HOURS ?? 0.25);
|
|
@@ -1195,44 +843,23 @@ function pruneMutationJournal(options = {}) {
|
|
|
1195
843
|
const cutoffIso = options.cutoffIso
|
|
1196
844
|
|| new Date(Date.now() - hours * 60 * 60 * 1000).toISOString();
|
|
1197
845
|
const batchSize = Math.max(1, Math.floor(Number(options.batchSize) || 1000));
|
|
1198
|
-
const
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
AND mutation_state = 'synced'
|
|
1209
|
-
AND official_projected_at IS NOT NULL
|
|
846
|
+
const result = cachedStatement(database, `
|
|
847
|
+
DELETE FROM mutation_journal
|
|
848
|
+
WHERE journal_id IN (
|
|
849
|
+
SELECT journal_id
|
|
850
|
+
FROM mutation_journal
|
|
851
|
+
WHERE updated_at < ?
|
|
852
|
+
AND event_seq IS NOT NULL
|
|
853
|
+
AND (
|
|
854
|
+
(shared_resource_id IS NULL AND mutation_state = 'saved-local')
|
|
855
|
+
OR mutation_state = 'synced'
|
|
1210
856
|
)
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
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);
|
|
857
|
+
ORDER BY journal_id ASC
|
|
858
|
+
LIMIT ?
|
|
859
|
+
)
|
|
860
|
+
`).run(cutoffIso, batchSize);
|
|
1234
861
|
return {
|
|
1235
|
-
deleted,
|
|
862
|
+
deleted: result.changes || 0,
|
|
1236
863
|
cutoffIso,
|
|
1237
864
|
retentionHours: hours,
|
|
1238
865
|
batchSize,
|
|
@@ -1245,17 +872,13 @@ module.exports = {
|
|
|
1245
872
|
claimCloudMutations,
|
|
1246
873
|
commitLocalMutation,
|
|
1247
874
|
completeMaterializedMutation,
|
|
1248
|
-
completeOfficialSnapshotFold,
|
|
1249
875
|
failCloudMutation,
|
|
1250
876
|
getMutation,
|
|
1251
877
|
ingestCloudMutation,
|
|
1252
878
|
journalCloudMutation,
|
|
1253
879
|
journalMutation,
|
|
1254
|
-
listLocalOverlayMutations,
|
|
1255
|
-
listUnappliedOfficialMutations,
|
|
1256
880
|
listPendingCloudMutations,
|
|
1257
881
|
listMutations,
|
|
1258
882
|
markMutationConflicted,
|
|
1259
883
|
pruneMutationJournal,
|
|
1260
|
-
retireStaleCodeMutations,
|
|
1261
884
|
};
|