mikser-io 8.3.2 → 8.3.6

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": "8.3.2",
3
+ "version": "8.3.6",
4
4
  "description": "<p align=\"center\"> <img src=\"mikser-lockup-stacked.svg\" alt=\"mikser\" width=\"198\" /> </p>",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/src/catalog.js CHANGED
@@ -71,9 +71,38 @@ registerSchema('mikser_entities', `
71
71
  CREATE INDEX IF NOT EXISTS idx_mikser_entities_uri ON mikser_entities(uri);
72
72
  `)
73
73
 
74
+ // Per-process dedupe of the no-filter findEntities/iterateEntities
75
+ // warning. Keyed by the rendering entity id (which is what gets
76
+ // blamed in the recorded refClosure). Once per offending site is
77
+ // enough — the warning is educational, not load-bearing.
78
+ const _warnedNullFilter = new Set()
79
+
74
80
  function recordQuery(filter) {
75
81
  const ctx = queryContext.getStore()
76
82
  if (!ctx?.track) return
83
+ // Null/undefined filter records as `null` in the snapshot's
84
+ // refClosure, which manifest.shouldSkip and manifest.queryAffected
85
+ // treat as "any mutation could have affected this render."
86
+ // Architecturally correct, but it means an aggregate layout whose
87
+ // sidecar calls findEntities() with no args invalidates on every
88
+ // single CREATE/UPDATE/DELETE — including spurious ones (plugins
89
+ // re-emitting unchanged entities, etc.).
90
+ //
91
+ // Warn once per rendering entity so authors can narrow the filter.
92
+ // The fix is almost always to add the collection / type / format
93
+ // dimension the sidecar actually cares about; the JS-side .filter()
94
+ // chain that usually follows findEntities() is the signal that the
95
+ // filter belongs in the SQL.
96
+ if ((filter === undefined || filter === null) && ctx.entityId) {
97
+ if (!_warnedNullFilter.has(ctx.entityId)) {
98
+ _warnedNullFilter.add(ctx.entityId)
99
+ const logger = useLogger()
100
+ logger?.warn(
101
+ 'findEntities()/iterateEntities() called with no filter from %s — recorded query dep invalidates on every mutation. For "all renderable entities" use findEntities({"meta.href": {$exists: true}}). For narrower scopes use any indexed column ({collection, type, format, "meta.layout", "meta.lang"}); pushing the filter into SQL keeps invalidation precise.',
102
+ ctx.entityId,
103
+ )
104
+ }
105
+ }
77
106
  ctx.track.query(normalizeFilter(filter))
78
107
  }
79
108
 
package/src/engine.js CHANGED
@@ -410,7 +410,20 @@ export async function setup(options) {
410
410
  sidecarQueries: context?.sidecarQueries,
411
411
  })
412
412
  entry.deps = edges
413
- runtime.refs?.replaceDynamic(entity.id, edges)
413
+ // Pagination produces synthetic pageEntities
414
+ // (index.2.html, index.3.html, ...) whose ids
415
+ // are NOT in mikser_entities — they exist only
416
+ // at render time. Roll their dynamic refs up to
417
+ // entity.parent (set by layouts.onBeforeRender
418
+ // for pages 2+) so the mikser_refs FK to
419
+ // mikser_entities holds. The parent's own
420
+ // render also writes to the same source_id;
421
+ // INSERT OR IGNORE in stmtInsertEdge handles the
422
+ // dedup across pages. Invalidation re-dispatches
423
+ // the parent and the pagination expansion
424
+ // produces the children from there, so granular
425
+ // per-page refs aren't needed.
426
+ runtime.refs?.replaceDynamic(entity.parent ?? entity.id, edges)
414
427
  await runtime.complete(entry)
415
428
  await updateEntry({ id, output: entry.output, deps: edges })
416
429
  }
@@ -1,6 +1,8 @@
1
1
  import path from 'node:path'
2
2
  import { mkdir, symlink, unlink, lstat, realpath } from 'fs/promises'
3
3
  import { globby } from 'globby'
4
+ import pMap from 'p-map'
5
+ import { checksumsByCollection } from '../catalog.js'
4
6
 
5
7
  export default ({
6
8
  runtime,
@@ -125,25 +127,44 @@ export default ({
125
127
 
126
128
  const paths = await globby('**/*', { cwd: runtime.options.filesFolder })
127
129
  trackProgress('Files import', paths.length)
128
- return Promise.all(paths.map(async relativePath => {
130
+ // Bulk-prefetch the catalog's existing (id checksum) map for
131
+ // this collection once at scan start, so the gate below reads
132
+ // from it per-file instead of doing per-file SQL lookups. Same
133
+ // pattern source.js uses for documents/layouts via useSource.
134
+ // Without this gate the plugin re-emitted createEntity on every
135
+ // cycle for every file regardless of changes, inflating the
136
+ // journal with phantom mutations and triggering downstream
137
+ // re-dispatch of aggregate layouts whose recorded query deps
138
+ // matched the collection.
139
+ const priorChecksums = checksumsByCollection(collection)
140
+ await pMap(paths, async relativePath => {
129
141
  const { uri, source } = await ensureLink(relativePath)
130
142
  let name = relativePath
131
143
  if (runtime.config.files?.outputFolder) {
132
144
  name = path.join(runtime.config.files.outputFolder, relativePath)
133
145
  }
146
+ const id = path.join(`/${collection}`, relativePath)
147
+ const newChecksum = await checksum(source)
148
+ updateProgress()
149
+ // Gate: if the catalog already has this entity with the same
150
+ // checksum, the file hasn't changed since the last cycle.
151
+ // Skip emitting a CREATE — the catalog row stays correct,
152
+ // the journal stays accurate (mutations = actual changes),
153
+ // and downstream aggregate-layout invalidation isn't fired
154
+ // spuriously.
155
+ if (priorChecksums.get(id) === newChecksum) return
134
156
  await createEntity({
135
- id: path.join(`/${collection}`, relativePath),
157
+ id,
136
158
  uri,
137
159
  collection,
138
160
  type,
139
161
  format: path.extname(relativePath).substring(1).toLowerCase(),
140
162
  name,
141
163
  source,
142
- checksum: await checksum(source),
143
- link: await link(source)
164
+ checksum: newChecksum,
165
+ link: await link(source),
144
166
  })
145
- updateProgress()
146
- }))
167
+ }, { concurrency: 16 })
147
168
  })
148
169
 
149
170
  return {