mikser-io 9.3.0 → 9.3.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/catalog.js +22 -0
- package/src/manifest.js +31 -5
- package/src/subscriptions.js +25 -0
package/package.json
CHANGED
package/src/catalog.js
CHANGED
|
@@ -319,6 +319,28 @@ onPersist(async () => {
|
|
|
319
319
|
})
|
|
320
320
|
|
|
321
321
|
onFinalize(async () => {
|
|
322
|
+
// Second drain, and the one that closes the cycle.
|
|
323
|
+
//
|
|
324
|
+
// onPersist runs BEFORE render, so the drain above only ever sees what
|
|
325
|
+
// the load and process phases journalled. Everything a render or a
|
|
326
|
+
// postprocess journals lands after it — and onFinalized's clearJournal()
|
|
327
|
+
// then throws those entries away unread. The manifest never had this
|
|
328
|
+
// problem because it drains at onFinalize; the catalog simply had a
|
|
329
|
+
// shorter view of the same journal.
|
|
330
|
+
//
|
|
331
|
+
// The visible symptom was pruning that silently did nothing: a render
|
|
332
|
+
// asking for `catalog: false` journals its DELETE once the render
|
|
333
|
+
// resolves, which is past persist, so the row stayed. gpoint's cms
|
|
334
|
+
// accumulated 1,134 scratch entities carrying 86 MB of render payload,
|
|
335
|
+
// took its public endpoint from 70ms to 8-15s, and blanked the site.
|
|
336
|
+
//
|
|
337
|
+
// Draining again here rather than moving the persist drain: the phases
|
|
338
|
+
// before render still need their mutations committed at persist (the
|
|
339
|
+
// render reads the catalog), and journal consumers are named and
|
|
340
|
+
// independent, so a second pass only ever picks up what the first could
|
|
341
|
+
// not have seen.
|
|
342
|
+
await applyJournalMutations()
|
|
343
|
+
|
|
322
344
|
// Checkpoint the WAL so the main file size stays representative
|
|
323
345
|
// and external tools (mikser --verify on a separate run, debug
|
|
324
346
|
// scripts) see committed state. PASSIVE never blocks readers or
|
package/src/manifest.js
CHANGED
|
@@ -61,8 +61,6 @@ import { filterKey } from './track.js'
|
|
|
61
61
|
import { findById } from './catalog.js'
|
|
62
62
|
import { useDatabase, registerSchema } from './database/index.js'
|
|
63
63
|
|
|
64
|
-
export { inputHashOf } from './utils.js'
|
|
65
|
-
|
|
66
64
|
// Schema registration. Applied at db.open(). PRIMARY KEY (id,
|
|
67
65
|
// destination) — leading id column means `WHERE id = ?` queries use the
|
|
68
66
|
// PK index, no separate id index needed. Parent index is for pagination
|
|
@@ -542,11 +540,27 @@ onFinalize(async () => {
|
|
|
542
540
|
const m = sharedManifest
|
|
543
541
|
|
|
544
542
|
// 2a. Stage file unlinks for deleted entities + their children.
|
|
543
|
+
const deleted = new Set(deletedIds)
|
|
545
544
|
const filesToUnlink = []
|
|
545
|
+
const staged = new Set()
|
|
546
|
+
const stageUnlink = (destination) => {
|
|
547
|
+
if (!destination || staged.has(destination)) return
|
|
548
|
+
staged.add(destination)
|
|
549
|
+
filesToUnlink.push({ destination, reason: 'Entity deleted' })
|
|
550
|
+
}
|
|
546
551
|
for (const id of deletedIds) {
|
|
547
|
-
const
|
|
548
|
-
|
|
549
|
-
|
|
552
|
+
for (const row of m._stmtSelectByIdOrParent.all(id, id)) {
|
|
553
|
+
stageUnlink(row.destination)
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
// Recorded snapshots only describe PRIOR cycles. An entity rendered and
|
|
557
|
+
// deleted within the same cycle has no row to look its destination up in —
|
|
558
|
+
// on a first build there is no row at all — so the file it just wrote would
|
|
559
|
+
// survive both the entity and its snapshot. Take the destination from this
|
|
560
|
+
// cycle's render entries instead.
|
|
561
|
+
for (const { entity } of renderedEntries) {
|
|
562
|
+
if (deleted.has(entity.id) || (entity.parent && deleted.has(entity.parent))) {
|
|
563
|
+
stageUnlink(entity.destination)
|
|
550
564
|
}
|
|
551
565
|
}
|
|
552
566
|
|
|
@@ -581,8 +595,20 @@ onFinalize(async () => {
|
|
|
581
595
|
}
|
|
582
596
|
|
|
583
597
|
// 2e. Hash the rendered output files (async).
|
|
598
|
+
//
|
|
599
|
+
// An id can appear in BOTH drains within one cycle — rendered, then
|
|
600
|
+
// deleted. A render that opts out of the catalog journals its DELETE the
|
|
601
|
+
// moment it resolves, so the RENDER and the DELETE sit in the same
|
|
602
|
+
// journal. 3a below drops the snapshot and 3c would insert it straight
|
|
603
|
+
// back, undoing the delete inside its own transaction. The DELETE wins:
|
|
604
|
+
// the entity is gone, so nothing may go on describing it. Skipping here
|
|
605
|
+
// rather than in 3c also saves hashing an output that was just unlinked.
|
|
606
|
+
//
|
|
607
|
+
// `parent` is checked too, mirroring _stmtDeleteByIdOrParent — a deleted
|
|
608
|
+
// parent takes its paginated children's snapshots with it.
|
|
584
609
|
const recordedSnapshots = []
|
|
585
610
|
for (const { entity, deps } of renderedEntries) {
|
|
611
|
+
if (deleted.has(entity.id) || (entity.parent && deleted.has(entity.parent))) continue
|
|
586
612
|
const outputHash = await hashOutputFile(entity.destination)
|
|
587
613
|
recordedSnapshots.push(buildSnapshot(entity, deps, outputHash))
|
|
588
614
|
}
|
package/src/subscriptions.js
CHANGED
|
@@ -35,6 +35,7 @@ import { onFinalize } from './lifecycle.js'
|
|
|
35
35
|
import { useJournal } from './journal.js'
|
|
36
36
|
import { OPERATION } from './constants.js'
|
|
37
37
|
import { assertExpand, queryEntities } from './catalog.js'
|
|
38
|
+
import sift from 'sift'
|
|
38
39
|
|
|
39
40
|
// Per-module subscription registry. The dispatcher walks this Set
|
|
40
41
|
// every onFinalize. A Set (not a Map) because subscriptions are keyed
|
|
@@ -50,10 +51,34 @@ const opNames = {
|
|
|
50
51
|
[OPERATION.DELETE]: 'delete',
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
// A scope may be a predicate OR a sift object, and this is the ONE place that
|
|
55
|
+
// difference is resolved.
|
|
56
|
+
//
|
|
57
|
+
// An endpoint declares its scope once and it then reaches several consumers:
|
|
58
|
+
// queryEntities merges a sift object into the WHERE clause, while a dispatch
|
|
59
|
+
// holding a single entity can only test it. Every consumer that reached for
|
|
60
|
+
// `scope(entity)` therefore worked with the function form and threw
|
|
61
|
+
// `TypeError: scope is not a function` with the object one — which took a
|
|
62
|
+
// production render endpoint down and, because it needed a live subscription to
|
|
63
|
+
// fire at all, reproduced in nothing smaller than production.
|
|
64
|
+
//
|
|
65
|
+
// Compiling here rather than at each call site is the point: a call site that
|
|
66
|
+
// has to remember is a call site that will forget, and the next consumer added
|
|
67
|
+
// inherits the fix instead of the bug.
|
|
68
|
+
function toPredicate(scope) {
|
|
69
|
+
if (scope == null) return null
|
|
70
|
+
if (typeof scope === 'function') return scope
|
|
71
|
+
if (typeof scope === 'object') return sift(scope)
|
|
72
|
+
throw new Error('subscribe: scope must be a function or a sift filter object')
|
|
73
|
+
}
|
|
74
|
+
|
|
53
75
|
export function subscribe({ filter, scope, expand, onChange, signal } = {}) {
|
|
54
76
|
if (typeof onChange !== 'function') {
|
|
55
77
|
throw new Error('subscribe: onChange must be a function')
|
|
56
78
|
}
|
|
79
|
+
// Normalised before it is stored, so both the journal-walk dispatch and the
|
|
80
|
+
// graph dispatch below see a predicate and neither has to care.
|
|
81
|
+
scope = toPredicate(scope)
|
|
57
82
|
|
|
58
83
|
// Reject bad expand at registration. A misconfigured subscriber
|
|
59
84
|
// can otherwise open a session that's expensive to re-dispatch on
|