mikser-io 11.14.0 → 11.14.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/instance.js +41 -8
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mikser-io",
3
- "version": "11.14.0",
3
+ "version": "11.14.1",
4
4
  "files": [
5
5
  "app.js",
6
6
  "index.js",
package/src/instance.js CHANGED
@@ -31,7 +31,8 @@ import { createHash } from 'node:crypto'
31
31
  import { tmpdir } from 'node:os'
32
32
  import path from 'node:path'
33
33
  import { existsSync, unlinkSync } from 'node:fs'
34
- import { chmod } from 'node:fs/promises'
34
+ import { chmod, stat } from 'node:fs/promises'
35
+ import { checksum } from './utils/index.js'
35
36
 
36
37
  import runtime from './runtime.js'
37
38
  import { onLoaded } from './lifecycle.js'
@@ -288,23 +289,55 @@ function configMismatch(theirs) {
288
289
  // configCoverage lists every local module the config graph pulled in, so a
289
290
  // stat over that list catches an edit to an imported module — which a
290
291
  // client-side checksum of the entry file would miss entirely.
292
+ // Has a config file CHANGED since this instance started?
293
+ //
294
+ // Content, not mtime. A `git rebase`, `git checkout` or `rsync` rewrites a
295
+ // file with byte-identical content and a new mtime, and the instance then
296
+ // refused every forwarded command — "this instance's config changed on disk
297
+ // since it started" — while `git diff HEAD` reported nothing. Twice in one
298
+ // afternoon on one machine.
299
+ //
300
+ // On production the cost is not a restart. The config checksum is what the
301
+ // cache is stamped with, so a config the engine believes has moved buys a
302
+ // full cold re-derive of every asset, which is how a no-op rebase turns into
303
+ // twenty minutes of ffmpeg.
304
+ //
305
+ // mtime is kept as a PRE-FILTER, not as the answer: it is a stat against a
306
+ // read, and in the ordinary case nothing has moved and nothing is read. Only
307
+ // a file whose mtime shifted gets hashed, and only a file whose CONTENT
308
+ // shifted is reported. A file rewritten identically has its stamp refreshed
309
+ // so the next check is cheap again.
310
+ async function configFingerprint(file) {
311
+ try {
312
+ return { mtime: (await stat(file)).mtimeMs, hash: await checksum(file) }
313
+ } catch {
314
+ // Deleted, or unreadable. A stable sentinel, so a file that is still
315
+ // missing on the next check does not read as having changed again.
316
+ return { mtime: 0, hash: null }
317
+ }
318
+ }
319
+
291
320
  export async function configStale() {
292
321
  const covered = runtime.options.configCoverage?.files ?? []
293
322
  if (!covered.length) return null
294
- const { stat } = await import('node:fs/promises')
295
323
  const stamps = runtime.options.configStamps
296
324
  if (!stamps) {
297
325
  // First call: record, do not judge. Nothing to compare against yet.
298
326
  runtime.options.configStamps = Object.fromEntries(
299
- await Promise.all(covered.map(async (file) => {
300
- try { return [file, (await stat(file)).mtimeMs] } catch { return [file, 0] }
301
- })))
327
+ await Promise.all(covered.map(async (file) => [file, await configFingerprint(file)])))
302
328
  return null
303
329
  }
304
330
  for (const file of covered) {
305
- let now = 0
306
- try { now = (await stat(file)).mtimeMs } catch { /* deleted counts as changed */ }
307
- if (stamps[file] !== now) return file
331
+ const was = stamps[file]
332
+ const now = await configFingerprint(file)
333
+ if (was?.mtime === now.mtime) continue // nothing touched it
334
+ if (was?.hash === now.hash) {
335
+ // Touched and identical — a checkout, a rebase, a sync. Not a
336
+ // change, and re-stamped so this file is a stat again next time.
337
+ stamps[file] = now
338
+ continue
339
+ }
340
+ return file
308
341
  }
309
342
  return null
310
343
  }