mikser-io 9.39.2 → 9.40.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mikser-io",
3
- "version": "9.39.2",
3
+ "version": "9.40.0",
4
4
  "description": "<p align=\"center\"> <img src=\"mikser-lockup-stacked.svg\" alt=\"mikser\" width=\"198\" /> </p>",
5
5
  "main": "index.js",
6
6
  "exports": {
package/src/engine.js CHANGED
@@ -541,7 +541,8 @@ export async function setup(options) {
541
541
  // meta. Opt-out rather than opt-in: a contract that is only
542
542
  // correct when someone remembered to enable it is a contract
543
543
  // nobody can rely on. Set `metaReads: false` to switch it off.
544
- const track = createTrack({ meta: runtime.options.metaReads !== false })
544
+ const track = createTrack({ meta: runtime.options.metaReads !== false,
545
+ consumed: runtime.options.metaReads !== false })
545
546
  const renderOptions = {
546
547
  entity: renderEntity,
547
548
  options: {
@@ -639,10 +640,28 @@ export async function setup(options) {
639
640
  ...(track?.metaReads ?? []),
640
641
  ...(context?.sidecarMetaReads ?? []),
641
642
  ]
643
+ // Which keys of OTHER entities this render read, keyed
644
+ // by the entity they belong to. Merged from the same two
645
+ // places: the render's own track, and the sidecar's,
646
+ // which runs earlier on the main thread.
647
+ const consumedReads = new Map()
648
+ for (const [cid, paths] of [
649
+ ...(track?.consumedReads ?? []),
650
+ ...(context?.sidecarConsumedReads ?? []),
651
+ ]) {
652
+ const into = consumedReads.get(cid) ?? new Set()
653
+ for (const path of paths) into.add(path)
654
+ consumedReads.set(cid, into)
655
+ }
642
656
  entry.output = {
643
657
  success: true,
644
658
  result,
645
659
  ...(metaReads.length ? { metaReads: [...new Set(metaReads)].sort() } : {}),
660
+ ...(consumedReads.size ? {
661
+ consumedReads: [...consumedReads]
662
+ .map(([cid, paths]) => [cid, [...paths].sort()])
663
+ .sort(([a], [b]) => a.localeCompare(b)),
664
+ } : {}),
646
665
  }
647
666
  // A render that produced nothing at all. Distinct from
648
667
  // a render that THREW, which lands in `errors` and
package/src/manifest.js CHANGED
@@ -86,6 +86,11 @@ export const SNAPSHOTS_SCHEMA = `
86
86
  -- JavaScript, and no parser for any template engine will ever find
87
87
  -- an optional chain like row.meta?.hero?.tags.
88
88
  metaReads TEXT,
89
+ -- Keys of OTHER entities this render read, keyed by the entity they
90
+ -- belong to. The counterpart to metaReads, and the only record of what
91
+ -- a document that never renders is actually needed FOR: the engine knew
92
+ -- that a page queried it, never which of its fields mattered.
93
+ consumedReads TEXT,
89
94
  renderedAt INTEGER,
90
95
  parent TEXT,
91
96
  PRIMARY KEY (id, destination)
@@ -206,7 +211,7 @@ function buildRefClosure(entity, deps) {
206
211
  return closure
207
212
  }
208
213
 
209
- function buildSnapshot(entity, deps, outputHash, metaReads) {
214
+ function buildSnapshot(entity, deps, outputHash, metaReads, consumedReads) {
210
215
  const snapshot = {
211
216
  id: entity.id,
212
217
  destination: entity.destination,
@@ -222,6 +227,7 @@ function buildSnapshot(entity, deps, outputHash, metaReads) {
222
227
  // Sorted and deduped: this is a SET of paths, and a stable order keeps a
223
228
  // snapshot comparable between runs.
224
229
  if (metaReads?.length) snapshot.metaReads = [...new Set(metaReads)].sort()
230
+ if (consumedReads?.length) snapshot.consumedReads = consumedReads
225
231
  if (entity.parent) snapshot.parent = entity.parent
226
232
  if (outputHash) snapshot.outputHash = outputHash
227
233
  return snapshot
@@ -239,6 +245,7 @@ function rowToSnap(row) {
239
245
  outputHash: row.outputHash ?? undefined,
240
246
  refClosure: row.refClosure ? JSON.parse(row.refClosure) : undefined,
241
247
  metaReads: row.metaReads ? JSON.parse(row.metaReads) : undefined,
248
+ consumedReads: row.consumedReads ? JSON.parse(row.consumedReads) : undefined,
242
249
  renderedAt: row.renderedAt ?? undefined,
243
250
  parent: row.parent ?? undefined,
244
251
  }
@@ -253,6 +260,7 @@ function snapToRow(snap) {
253
260
  outputHash: snap.outputHash ?? null,
254
261
  refClosure: snap.refClosure ? JSON.stringify(snap.refClosure) : null,
255
262
  metaReads: snap.metaReads ? JSON.stringify(snap.metaReads) : null,
263
+ consumedReads: snap.consumedReads ? JSON.stringify(snap.consumedReads) : null,
256
264
  renderedAt: snap.renderedAt ?? null,
257
265
  parent: snap.parent ?? null,
258
266
  }
@@ -376,7 +384,7 @@ export function createManifest(db) {
376
384
  SELECT id FROM mikser_snapshots WHERE destination = ?
377
385
  `)
378
386
  const stmtLookupByDestination = db.prepare(`
379
- SELECT id, destination, inputHash, inputParts, outputHash, refClosure, metaReads, renderedAt, parent
387
+ SELECT id, destination, inputHash, inputParts, outputHash, refClosure, metaReads, consumedReads, renderedAt, parent
380
388
  FROM mikser_snapshots WHERE destination = ?
381
389
  `)
382
390
  const stmtDeleteByDestination = db.prepare(`
@@ -413,18 +421,18 @@ export function createManifest(db) {
413
421
  `)
414
422
 
415
423
  const stmtLookupById = db.prepare(`
416
- SELECT id, destination, inputHash, inputParts, outputHash, refClosure, metaReads, renderedAt, parent
424
+ SELECT id, destination, inputHash, inputParts, outputHash, refClosure, metaReads, consumedReads, renderedAt, parent
417
425
  FROM mikser_snapshots WHERE id = ? ORDER BY destination
418
426
  `)
419
427
  const stmtLookup = db.prepare(`
420
- SELECT id, destination, inputHash, inputParts, outputHash, refClosure, metaReads, renderedAt, parent
428
+ SELECT id, destination, inputHash, inputParts, outputHash, refClosure, metaReads, consumedReads, renderedAt, parent
421
429
  FROM mikser_snapshots WHERE id = ? AND destination = ?
422
430
  `)
423
431
  const stmtUpsert = db.prepare(`
424
432
  INSERT OR REPLACE INTO mikser_snapshots
425
- (id, destination, inputHash, inputParts, outputHash, refClosure, metaReads, renderedAt, parent)
433
+ (id, destination, inputHash, inputParts, outputHash, refClosure, metaReads, consumedReads, renderedAt, parent)
426
434
  VALUES
427
- (@id, @destination, @inputHash, @inputParts, @outputHash, @refClosure, @metaReads, @renderedAt, @parent)
435
+ (@id, @destination, @inputHash, @inputParts, @outputHash, @refClosure, @metaReads, @consumedReads, @renderedAt, @parent)
428
436
  `)
429
437
  const stmtDeleteByPK = db.prepare(`
430
438
  DELETE FROM mikser_snapshots WHERE id = ? AND destination = ?
@@ -440,7 +448,7 @@ export function createManifest(db) {
440
448
  SELECT id, destination FROM mikser_snapshots WHERE parent = ?
441
449
  `)
442
450
  const stmtSelectAll = db.prepare(`
443
- SELECT id, destination, inputHash, inputParts, outputHash, refClosure, metaReads, renderedAt, parent
451
+ SELECT id, destination, inputHash, inputParts, outputHash, refClosure, metaReads, consumedReads, renderedAt, parent
444
452
  FROM mikser_snapshots
445
453
  `)
446
454
  const stmtCount = db.prepare(`SELECT COUNT(*) AS c FROM mikser_snapshots`)
@@ -859,8 +867,8 @@ export function createManifest(db) {
859
867
  },
860
868
 
861
869
  // Record a successful render. Single INSERT OR REPLACE.
862
- record(entity, deps, metaReads) {
863
- stmtUpsert.run(snapToRow(buildSnapshot(entity, deps, undefined, metaReads)))
870
+ record(entity, deps, metaReads, consumedReads) {
871
+ stmtUpsert.run(snapToRow(buildSnapshot(entity, deps, undefined, metaReads, consumedReads)))
864
872
  },
865
873
 
866
874
  // Return the set of entity ids whose recorded snapshots have a
@@ -1154,7 +1162,8 @@ onFinalize(async () => {
1154
1162
 
1155
1163
  for await (const { output, entity, deps } of useJournal('Output', [OPERATION.RENDER])) {
1156
1164
  if (output?.success && !output.skipped) {
1157
- renderedEntries.push({ entity, deps, metaReads: output.metaReads })
1165
+ renderedEntries.push({ entity, deps, metaReads: output.metaReads,
1166
+ consumedReads: output.consumedReads })
1158
1167
  if (entity.pages > 1) {
1159
1168
  const parentId = entity.parent ?? entity.id
1160
1169
  if (!newDestinationsByParent.has(parentId)) {
@@ -1287,10 +1296,10 @@ onFinalize(async () => {
1287
1296
  // `parent` is checked too, mirroring _stmtDeleteByIdOrParent — a deleted
1288
1297
  // parent takes its paginated children's snapshots with it.
1289
1298
  const recordedSnapshots = []
1290
- for (const { entity, deps, metaReads } of renderedEntries) {
1299
+ for (const { entity, deps, metaReads, consumedReads } of renderedEntries) {
1291
1300
  if (deleted.has(entity.id) || (entity.parent && deleted.has(entity.parent))) continue
1292
1301
  const outputHash = await hashOutputFile(entity.destination)
1293
- recordedSnapshots.push(buildSnapshot(entity, deps, outputHash, metaReads))
1302
+ recordedSnapshots.push(buildSnapshot(entity, deps, outputHash, metaReads, consumedReads))
1294
1303
  }
1295
1304
 
1296
1305
  // Apply all DB mutations atomically.
package/src/track.js CHANGED
@@ -40,7 +40,7 @@ export function filterKey(filter) {
40
40
  // The returned object exposes `partials: Set<string>` and
41
41
  // `queries: Array<filter | null>` directly. Consumers iterate either
42
42
  // shape; both are owned by the track for the lifetime of the run.
43
- export function createTrack({ partial = true, query = true, lookup = true, meta = false } = {}) {
43
+ export function createTrack({ partial = true, query = true, lookup = true, meta = false, consumed = false } = {}) {
44
44
  const track = {}
45
45
  if (lookup) {
46
46
  // Lookups a TEMPLATE made by name: runtime.href('/contacts'),
@@ -100,6 +100,28 @@ export function createTrack({ partial = true, query = true, lookup = true, meta
100
100
  track.metaReads = metaReads
101
101
  track.metaRead = (path) => { if (path) metaReads.add(path) }
102
102
  }
103
+ if (consumed) {
104
+ // Which fields of OTHER entities this render read.
105
+ //
106
+ // `metaReads` covers the entity being rendered. This covers the ones it
107
+ // pulled in — a page reads a navigation document through a query and
108
+ // then takes `items[].label` off it, and until now nothing recorded
109
+ // that. The consequence is that a document which never renders had no
110
+ // derivable contract at all: the engine knew THAT a page queried it,
111
+ // never which of its keys mattered.
112
+ //
113
+ // Keyed by the consumed entity's id, because the answer is per-entity:
114
+ // "what does anything read from navigation.yml" is the question, and it
115
+ // is asked from the other side.
116
+ const consumedReads = new Map()
117
+ track.consumedReads = consumedReads
118
+ track.consumedRead = (id, path) => {
119
+ if (!id || !path) return
120
+ let paths = consumedReads.get(id)
121
+ if (!paths) consumedReads.set(id, paths = new Set())
122
+ paths.add(path)
123
+ }
124
+ }
103
125
  if (query) {
104
126
  const queries = []
105
127
  const queryKeys = new Set()
@@ -261,6 +283,8 @@ export function serializeTrack(track) {
261
283
  queries: track.queries ? [...track.queries] : undefined,
262
284
  metaReads: track.metaReads ? [...track.metaReads] : undefined,
263
285
  lookups: track.lookups ? [...track.lookups].map(([k, v]) => [k, [...v]]) : undefined,
286
+ consumedReads: track.consumedReads
287
+ ? [...track.consumedReads].map(([k, v]) => [k, [...v]]) : undefined,
264
288
  }
265
289
  }
266
290
 
@@ -273,5 +297,20 @@ export function mergeTrack(target, data) {
273
297
  for (const q of data.queries ?? []) target.query?.(q)
274
298
  for (const m of data.metaReads ?? []) target.metaRead?.(m)
275
299
  for (const [name, ids] of data.lookups ?? []) target.lookup?.(name, ids)
300
+ for (const [id, paths] of data.consumedReads ?? []) {
301
+ for (const path of paths) target.consumedRead?.(id, path)
302
+ }
276
303
  return target
277
304
  }
305
+
306
+ // A queried entity, wrapped so what a render reads off it is recorded against
307
+ // THAT entity rather than the one being rendered.
308
+ //
309
+ // Only `meta` is wrapped: id, name and uri are how the caller identifies the
310
+ // row, not content, and recording them would bury the keys that matter. Returns
311
+ // the entity untouched when there is nothing to record into, so a call site can
312
+ // wrap unconditionally.
313
+ export function observeConsumed(entity, track) {
314
+ if (!track?.consumedRead || !entity?.id || !entity.meta || typeof entity.meta !== 'object') return entity
315
+ return { ...entity, meta: recordReads(entity.meta, '', (path) => track.consumedRead(entity.id, path)) }
316
+ }