mikser-io 8.3.1 → 8.3.2
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/lifecycle.js +15 -3
- package/src/manifest.js +48 -0
- package/src/plugins/layouts.js +24 -3
- package/src/source.js +6 -1
package/package.json
CHANGED
package/src/lifecycle.js
CHANGED
|
@@ -14,11 +14,23 @@ export async function createEntity(entity) {
|
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
// deleteEntity accepts either a minimal { id, type, collection }
|
|
18
|
+
// payload or a full entity. The full-entity form is preferred when
|
|
19
|
+
// the caller has it on hand (source.sweepDeleted does — it just
|
|
20
|
+
// findById'd the entity to compute the deletion), because the
|
|
21
|
+
// manifest's query-affected dispatch (and any downstream sift-based
|
|
22
|
+
// invalidation) can only match against the entity's actual fields.
|
|
23
|
+
// A minimal {id,type,collection} still works for backwards
|
|
24
|
+
// compatibility but query deps on fields not in that triple
|
|
25
|
+
// (format, meta.*, name, etc.) won't fire.
|
|
26
|
+
export async function deleteEntity(payload) {
|
|
18
27
|
const logger = useLogger()
|
|
19
|
-
const
|
|
28
|
+
const entity = payload?.id
|
|
29
|
+
? payload // accept full entity payload as-is
|
|
30
|
+
: { id: payload?.id, type: payload?.type, collection: payload?.collection }
|
|
31
|
+
const entry = { operation: OPERATION.DELETE, entity }
|
|
20
32
|
if (await runtime.validate(entry)) {
|
|
21
|
-
logger.debug('Delete %s entity: %s %s', collection, type, id)
|
|
33
|
+
logger.debug('Delete %s entity: %s %s', entity.collection, entity.type, entity.id)
|
|
22
34
|
await addEntry(entry)
|
|
23
35
|
}
|
|
24
36
|
}
|
package/src/manifest.js
CHANGED
|
@@ -232,6 +232,15 @@ export function createManifest(db) {
|
|
|
232
232
|
AND json_extract(value, '$.hash') IS NOT NULL
|
|
233
233
|
GROUP BY json_extract(value, '$.target')
|
|
234
234
|
`)
|
|
235
|
+
// Snapshots whose refClosure contains at least one query dep. The
|
|
236
|
+
// LIKE pre-filter is cheap (JSON column scan with no parse) and
|
|
237
|
+
// narrows down to the small set of aggregate-layout renders.
|
|
238
|
+
// queryAffected then JSON.parses just those rows and sift-matches
|
|
239
|
+
// each query filter against each mutated entity.
|
|
240
|
+
const stmtSnapshotsWithQuery = db.prepare(`
|
|
241
|
+
SELECT id, refClosure FROM mikser_snapshots
|
|
242
|
+
WHERE refClosure LIKE '%"kind":"query"%'
|
|
243
|
+
`)
|
|
235
244
|
|
|
236
245
|
const manifest = {
|
|
237
246
|
// Look up a previously-recorded entry by entity (or by an
|
|
@@ -293,6 +302,45 @@ export function createManifest(db) {
|
|
|
293
302
|
stmtUpsert.run(snapToRow(buildSnapshot(entity, deps)))
|
|
294
303
|
},
|
|
295
304
|
|
|
305
|
+
// Return the set of entity ids whose recorded snapshots have a
|
|
306
|
+
// query dep that matches any of the cycle's mutated entities.
|
|
307
|
+
// The static-ref closure walk (refs.inverseClosureOf) only finds
|
|
308
|
+
// entities reachable via $-keyed edges in mikser_refs; aggregate
|
|
309
|
+
// layouts that depend on findEntities(...) results need this
|
|
310
|
+
// second-pass dispatch hint.
|
|
311
|
+
//
|
|
312
|
+
// Cheap when there are no aggregate layouts (LIKE pre-filter
|
|
313
|
+
// returns no rows). Cost scales with (snapshots-with-query) ×
|
|
314
|
+
// (mutated-entities), both typically small.
|
|
315
|
+
queryAffected(mutatedEntities) {
|
|
316
|
+
const affected = new Set()
|
|
317
|
+
if (!mutatedEntities?.size) return affected
|
|
318
|
+
for (const row of stmtSnapshotsWithQuery.iterate()) {
|
|
319
|
+
const refClosure = row.refClosure ? JSON.parse(row.refClosure) : []
|
|
320
|
+
let hit = false
|
|
321
|
+
for (const entry of refClosure) {
|
|
322
|
+
if (entry.kind !== 'query') continue
|
|
323
|
+
if (!entry.filter) {
|
|
324
|
+
// Null filter = unserializable predicate captured
|
|
325
|
+
// at render time. Conservative: invalidate on any
|
|
326
|
+
// mutation.
|
|
327
|
+
hit = true
|
|
328
|
+
break
|
|
329
|
+
}
|
|
330
|
+
const matcher = sift(entry.filter)
|
|
331
|
+
for (const mutated of mutatedEntities.values()) {
|
|
332
|
+
if (matcher(mutated)) {
|
|
333
|
+
hit = true
|
|
334
|
+
break
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (hit) break
|
|
338
|
+
}
|
|
339
|
+
if (hit) affected.add(row.id)
|
|
340
|
+
}
|
|
341
|
+
return affected
|
|
342
|
+
},
|
|
343
|
+
|
|
296
344
|
// Drop all snapshots owned by entity id (direct outputs and any
|
|
297
345
|
// paginated children whose `parent` is set to this id). Returns
|
|
298
346
|
// the destinations that were removed so callers can unlink the
|
package/src/plugins/layouts.js
CHANGED
|
@@ -565,12 +565,24 @@ export default ({
|
|
|
565
565
|
// typical site has 0-10 of these, no full scan.
|
|
566
566
|
const optOutEntities = await findEntities({ 'meta.cache': 0 })
|
|
567
567
|
|
|
568
|
-
|
|
568
|
+
// Query-dep affected snapshots: aggregate layouts that
|
|
569
|
+
// depend on findEntities(...) instead of static $-refs need
|
|
570
|
+
// a second-pass dispatch hint. manifest.queryAffected walks
|
|
571
|
+
// every snapshot whose refClosure contains a `query` entry
|
|
572
|
+
// and sift-matches the recorded filter against the cycle's
|
|
573
|
+
// mutated entities. Bounded by snapshots-with-query (small
|
|
574
|
+
// — index pages, sitemaps, RSS) × seeds.
|
|
575
|
+
const mutatedEntities = new Map(seeds.map(s => [s.id, s]))
|
|
576
|
+
const queryAffected = runtime.manifest?.queryAffected(mutatedEntities) ?? new Set()
|
|
577
|
+
|
|
578
|
+
if (seeds.length === 0 && optOutEntities.length === 0 && queryAffected.size === 0) return
|
|
569
579
|
|
|
570
580
|
const closure = seeds.length ? runtime.refs.inverseClosureOf(seeds) : new Set()
|
|
571
|
-
// Combine closure ids + opt-out ids
|
|
581
|
+
// Combine closure ids + opt-out ids + query-affected ids
|
|
582
|
+
// into one dispatch set.
|
|
572
583
|
const dispatchIds = new Set(closure)
|
|
573
584
|
for (const e of optOutEntities) dispatchIds.add(e.id)
|
|
585
|
+
for (const id of queryAffected) dispatchIds.add(id)
|
|
574
586
|
|
|
575
587
|
// Hydrate each id via findById. LRU cache absorbs
|
|
576
588
|
// duplicates (refs BFS revisits, partial dispatches
|
|
@@ -700,7 +712,16 @@ export default ({
|
|
|
700
712
|
postprocessor: entity.layout.postprocessor,
|
|
701
713
|
tasks: entity.meta?.task || TASKS.INLINE
|
|
702
714
|
},
|
|
703
|
-
|
|
715
|
+
// sidecarQueries threads the sidecar load()'s
|
|
716
|
+
// findEntities calls into manifest.collectEdges
|
|
717
|
+
// as `{kind: 'query', filter}` refClosure entries.
|
|
718
|
+
// Without it, aggregate layouts that don't
|
|
719
|
+
// paginate (sitemap.xml, index pages, RSS feeds)
|
|
720
|
+
// lose query-dep tracking and never invalidate
|
|
721
|
+
// when matching entities are added/modified/
|
|
722
|
+
// deleted. The paginated branch above already
|
|
723
|
+
// does this.
|
|
724
|
+
context: { data, plugins, sidecarQueries: sidecarTrack.queries }
|
|
704
725
|
})
|
|
705
726
|
}
|
|
706
727
|
}
|
package/src/source.js
CHANGED
|
@@ -351,7 +351,12 @@ export function useSource(core, options) {
|
|
|
351
351
|
}, { concurrency: SCAN_CONCURRENCY })
|
|
352
352
|
|
|
353
353
|
scanStats.deleted = await sweepDeleted(collection, scanned, async (e) => {
|
|
354
|
-
|
|
354
|
+
// Pass the full entity (e is hydrated from catalog by
|
|
355
|
+
// sweepDeleted) so the manifest's query-affected dispatch
|
|
356
|
+
// can sift-match aggregate-layout filters against the
|
|
357
|
+
// actual fields (format, meta.layout, etc.). Minimal
|
|
358
|
+
// {id,type,collection} would miss those filters.
|
|
359
|
+
await deleteEntity(e)
|
|
355
360
|
logger.debug('%s removed (file gone): %s', collection, e.name)
|
|
356
361
|
})
|
|
357
362
|
|