mikser-io 11.1.0 → 11.2.1
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/render.js +27 -1
- 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/render.js
CHANGED
|
@@ -544,6 +544,25 @@ export function useRenderer(runtime, { defaultTimeout = 30_000 } = {}) {
|
|
|
544
544
|
* @returns {Promise<{output, entity}>}
|
|
545
545
|
*/
|
|
546
546
|
async function render(entity, { timeout = defaultTimeout, catalog = true, save = true } = {}) {
|
|
547
|
+
// Was this row already in the catalog BEFORE the render? `catalog:
|
|
548
|
+
// false` means "do not leave a row behind", and that is only the
|
|
549
|
+
// render's to decide for a row the render created. Asked here, before
|
|
550
|
+
// the render puts one there.
|
|
551
|
+
//
|
|
552
|
+
// Without it, `catalog: false` deleted whatever it was handed: a
|
|
553
|
+
// preview of an EXISTING entity pruned the real row, so the entity
|
|
554
|
+
// vanished from the site while its file sat on disk — no change set,
|
|
555
|
+
// no cycle, and the removal logged only at debug. It cost a day to
|
|
556
|
+
// find, in a form that rendered once and then answered "Entity not
|
|
557
|
+
// found".
|
|
558
|
+
// Imported HERE, not at the top: catalog.js reaches back into this
|
|
559
|
+
// module, and a static import makes that cycle load-bearing at
|
|
560
|
+
// module-evaluation time — it surfaced as "Cannot access 'schemas'
|
|
561
|
+
// before initialization" in three unrelated test files.
|
|
562
|
+
const preexisting = catalog === false && entity?.id
|
|
563
|
+
? Boolean(await (await import('./catalog.js')).findById(entity.id))
|
|
564
|
+
: false
|
|
565
|
+
|
|
547
566
|
const result = await new Promise((resolve, reject) => {
|
|
548
567
|
const correlationId = randomUUID()
|
|
549
568
|
// Engine-set fields live under entity.options. The caller's
|
|
@@ -585,7 +604,14 @@ export function useRenderer(runtime, { defaultTimeout = 30_000 } = {}) {
|
|
|
585
604
|
// and wrong for one that did. `catalog: false, save: true`
|
|
586
605
|
// therefore keeps its row, and says so rather than dropping
|
|
587
606
|
// the output on the floor.
|
|
588
|
-
if (
|
|
607
|
+
if (preexisting) {
|
|
608
|
+
// Someone else's row. It was here before this render and is
|
|
609
|
+
// not this render's to remove.
|
|
610
|
+
useLogger()?.debug(
|
|
611
|
+
'render: catalog:false ignored for %s — the entity was already in the catalog',
|
|
612
|
+
result.entity.id,
|
|
613
|
+
)
|
|
614
|
+
} else if (save === false) {
|
|
589
615
|
await runtime.delete(result.entity)
|
|
590
616
|
} else {
|
|
591
617
|
useLogger()?.warn(
|
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,
|