mikser-io 11.1.0 → 11.2.0
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/package.json +1 -1
- package/src/changeset.js +47 -5
- package/src/utils/entity.js +6 -1
- package/src/write.js +5 -2
package/package.json
CHANGED
package/src/changeset.js
CHANGED
|
@@ -84,6 +84,27 @@ registerMigrations('change-sets', [
|
|
|
84
84
|
})
|
|
85
85
|
},
|
|
86
86
|
},
|
|
87
|
+
{
|
|
88
|
+
// What the write actually put there.
|
|
89
|
+
//
|
|
90
|
+
// Without it, a set that produced no commit is ambiguous in a way that
|
|
91
|
+
// matters: the file on disk being identical to the last commit means
|
|
92
|
+
// EITHER the write matched what was already committed, or something
|
|
93
|
+
// replaced it before the commit pass ran. The first is a no-op; the
|
|
94
|
+
// second is lost work. From disk alone they are the same picture, and
|
|
95
|
+
// a real one was misread as the harmless case on a live site.
|
|
96
|
+
//
|
|
97
|
+
// Recorded at write time, so a consumer can ask afterwards whether
|
|
98
|
+
// what it wrote is still there. Nullable: a writer that does not know
|
|
99
|
+
// its own bytes records nothing, and a consumer must then say it
|
|
100
|
+
// cannot tell rather than assume either way.
|
|
101
|
+
name: '002-write-checksums',
|
|
102
|
+
up: async (knex) => {
|
|
103
|
+
await knex.schema.alterTable('mikser_change_set_paths', (table) => {
|
|
104
|
+
table.string('checksum')
|
|
105
|
+
})
|
|
106
|
+
},
|
|
107
|
+
},
|
|
87
108
|
])
|
|
88
109
|
|
|
89
110
|
// How many sets to keep. An undo log is only useful while the change is
|
|
@@ -197,7 +218,7 @@ async function settled() {
|
|
|
197
218
|
try { await queue } catch { /* reported at the point of failure */ }
|
|
198
219
|
}
|
|
199
220
|
|
|
200
|
-
async function persist(knex, set, rel, operation, entityId) {
|
|
221
|
+
async function persist(knex, set, rel, operation, entityId, checksum) {
|
|
201
222
|
// COALESCE rather than a plain merge: the first write's summary is the
|
|
202
223
|
// request's own description of itself, and a later write in the same set
|
|
203
224
|
// must not overwrite it with its own. `excluded` is the pseudo-table on
|
|
@@ -220,9 +241,17 @@ async function persist(knex, set, rel, operation, entityId) {
|
|
|
220
241
|
})
|
|
221
242
|
|
|
222
243
|
await knex('mikser_change_set_paths')
|
|
223
|
-
.insert({ change_set: set.id, path: rel, operation, entity_id: entityId ?? null
|
|
244
|
+
.insert({ change_set: set.id, path: rel, operation, entity_id: entityId ?? null,
|
|
245
|
+
checksum: checksum ?? null })
|
|
224
246
|
.onConflict(['change_set', 'path'])
|
|
225
|
-
|
|
247
|
+
// The LATEST write to a path is the one that says what should be there
|
|
248
|
+
// now, so its checksum replaces the earlier one — but only when it has
|
|
249
|
+
// one, or a writer that knows its bytes is overwritten by one that
|
|
250
|
+
// does not and the path silently becomes unverifiable.
|
|
251
|
+
.merge({
|
|
252
|
+
operation: knex.raw('excluded.operation'),
|
|
253
|
+
checksum: knex.raw('COALESCE(excluded.checksum, mikser_change_set_paths.checksum)'),
|
|
254
|
+
})
|
|
226
255
|
|
|
227
256
|
await prune(knex)
|
|
228
257
|
}
|
|
@@ -267,7 +296,7 @@ async function rowsToSets(knex, rows) {
|
|
|
267
296
|
const paths = await knex('mikser_change_set_paths')
|
|
268
297
|
.whereIn('change_set', rows.map(row => row.id))
|
|
269
298
|
.orderBy('path')
|
|
270
|
-
.select('change_set', 'path', 'operation')
|
|
299
|
+
.select('change_set', 'path', 'operation', 'checksum')
|
|
271
300
|
|
|
272
301
|
const bySet = new Map()
|
|
273
302
|
for (const row of paths) {
|
|
@@ -292,6 +321,10 @@ async function rowsToSets(knex, rows) {
|
|
|
292
321
|
outcome: row.outcome ?? null,
|
|
293
322
|
paths: mine.map(p => p.path),
|
|
294
323
|
deletions: mine.filter(p => p.operation === 'delete').map(p => p.path),
|
|
324
|
+
// path -> the checksum of what was written there, for the paths
|
|
325
|
+
// whose writer knew. Absent means unverifiable, not unchanged.
|
|
326
|
+
checksums: Object.fromEntries(
|
|
327
|
+
mine.filter(p => p.checksum).map(p => [p.path, p.checksum])),
|
|
295
328
|
}
|
|
296
329
|
})
|
|
297
330
|
}
|
|
@@ -312,6 +345,9 @@ function memorySets(filter = () => true) {
|
|
|
312
345
|
outcome: set.outcome ?? null,
|
|
313
346
|
paths: [...set.paths.keys()],
|
|
314
347
|
deletions: [...set.paths.entries()].filter(([, op]) => op === 'delete').map(([p]) => p),
|
|
348
|
+
// Same shape as the durable path, so a consumer reads one contract
|
|
349
|
+
// whether or not a database is configured.
|
|
350
|
+
checksums: Object.fromEntries(set.checksums ?? []),
|
|
315
351
|
}))
|
|
316
352
|
}
|
|
317
353
|
|
|
@@ -336,6 +372,11 @@ function relativeToWorkingFolder(uri) {
|
|
|
336
372
|
// First one wins: later writes in the same set are the same request.
|
|
337
373
|
export function recordChangeSetWrite({
|
|
338
374
|
changeSet, summary, principal, uri, operation = 'write', undoOf, entityId,
|
|
375
|
+
// The checksum of the bytes just written, when the caller knows them.
|
|
376
|
+
// Optional on purpose: a writer that does not have the content in hand
|
|
377
|
+
// records nothing rather than a wrong value, and a consumer reading the
|
|
378
|
+
// log must treat its absence as "cannot tell".
|
|
379
|
+
checksum,
|
|
339
380
|
} = {}) {
|
|
340
381
|
// An explicit id always wins; otherwise take whatever set is in effect.
|
|
341
382
|
// A write with neither stays unclaimed, which is the correct outcome for
|
|
@@ -373,6 +414,7 @@ export function recordChangeSetWrite({
|
|
|
373
414
|
if (!set.undoOf && undoOf) set.undoOf = undoOf
|
|
374
415
|
set.updatedAt = Date.now()
|
|
375
416
|
set.paths.set(rel, operation)
|
|
417
|
+
if (checksum) (set.checksums ??= new Map()).set(rel, checksum)
|
|
376
418
|
|
|
377
419
|
if (db()) {
|
|
378
420
|
// Snapshotted, because the queued write runs later and the set keeps
|
|
@@ -381,7 +423,7 @@ export function recordChangeSetWrite({
|
|
|
381
423
|
const snapshot = { ...set }
|
|
382
424
|
enqueue(async () => {
|
|
383
425
|
const knex = db()
|
|
384
|
-
if (knex) await persist(knex, snapshot, rel, operation, entityId)
|
|
426
|
+
if (knex) await persist(knex, snapshot, rel, operation, entityId, checksum)
|
|
385
427
|
})
|
|
386
428
|
}
|
|
387
429
|
return set.id
|
package/src/utils/entity.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from 'path'
|
|
|
3
3
|
import runtime from '../runtime.js'
|
|
4
4
|
import yaml from 'yaml'
|
|
5
5
|
|
|
6
|
+
import { checksumOf } from './hash.js'
|
|
6
7
|
import { isRefKey } from './refs.js'
|
|
7
8
|
import { contentType } from 'mime-types'
|
|
8
9
|
import { minimatch } from 'minimatch'
|
|
@@ -449,7 +450,11 @@ export function useCollection(runtime, name) {
|
|
|
449
450
|
// of change sets still produces undoable work — the alternative is
|
|
450
451
|
// every writer remembering, and the one that forgets is the one
|
|
451
452
|
// whose edit cannot be taken back.
|
|
452
|
-
|
|
453
|
+
// The checksum goes with it, from the bytes just written. Same
|
|
454
|
+
// reason the recording is here: a consumer asking later whether
|
|
455
|
+
// its write is still on disk needs to know what it wrote, and the
|
|
456
|
+
// only place that reliably knows is the write itself.
|
|
457
|
+
runtime.recordChangeSetWrite?.({ uri, checksum: checksumOf(content) })
|
|
453
458
|
return uri
|
|
454
459
|
},
|
|
455
460
|
|
package/src/write.js
CHANGED
|
@@ -22,7 +22,7 @@ import { readdir, readFile } from 'node:fs/promises'
|
|
|
22
22
|
|
|
23
23
|
import runtime from './runtime.js'
|
|
24
24
|
import { readEntity, findEntities } from './catalog.js'
|
|
25
|
-
import { useCollection, checksum, readEntityContent, lookupKeys, validateSource } from './utils/index.js'
|
|
25
|
+
import { useCollection, checksum, checksumOf, readEntityContent, lookupKeys, validateSource } from './utils/index.js'
|
|
26
26
|
import { nextCycleId, whenCycleCompletes } from './report.js'
|
|
27
27
|
import { recordChangeSetWrite, currentChangeSet } from './changeset.js'
|
|
28
28
|
|
|
@@ -275,7 +275,10 @@ export async function writeEntitySource({
|
|
|
275
275
|
// AFTER the write, so a set only ever claims paths that actually moved.
|
|
276
276
|
// Claiming on intent would make a failed write undoable, and undoing a
|
|
277
277
|
// write that never happened is a way to delete someone else's file.
|
|
278
|
-
|
|
278
|
+
// The checksum of what was just written, so a consumer can tell later
|
|
279
|
+
// whether what it wrote is still there. See the change-set log's
|
|
280
|
+
// 002-write-checksums migration.
|
|
281
|
+
if (changeSet) recordChangeSetWrite({ changeSet, summary, principal, uri, checksum: checksumOf(content) })
|
|
279
282
|
|
|
280
283
|
const result = {
|
|
281
284
|
ok: true, collection, relativePath,
|