pinokiod 8.0.41 → 8.0.44

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.
@@ -1,7 +1,7 @@
1
1
  const fs = require('fs')
2
2
  const path = require('path')
3
3
  const crypto = require('crypto')
4
- const { SHA256_RE, CANDIDATE_SIZE_OPTIONS } = require('./constants')
4
+ const { SHA256_RE } = require('./constants')
5
5
 
6
6
  const isMissingError = (error) => !!(error && (error.code === "ENOENT" || error.code === "ENOTDIR"))
7
7
  const isRecord = (value) => !!(value && typeof value === "object" && !Array.isArray(value))
@@ -42,15 +42,13 @@ class Registry {
42
42
  }
43
43
  reset() {
44
44
  this.blobs = new Map() // hash -> { size, first_seen, source_urls, verified_at, orphan }
45
- this.links = new Map() // path -> { hash, app, source_id, dev, ino, created, batch_id? }
45
+ this.links = new Map() // path -> { hash, app, source_id, dev, ino, created, mode, batch_id? }
46
46
  this.scanIndex = new Map() // path -> { size, mtime, dev, ino, hash, source_id }
47
47
  this.duplicates = new Map() // path -> { hash, size, app, source_id, discovered } pending user conversion
48
48
  this.excluded = new Map() // path -> { ts, source_id, size } user chose "keep independent"
49
- this.lastScan = null // last complete global scan
50
- this.lastScanAttempt = null // last global scan attempt, complete or partial
49
+ this.lastScan = null // scan totals, duration, and hash work
51
50
  this.sourceScans = new Map() // source id -> last complete scoped scan
52
- this.sourceScanAttempts = new Map() // source id -> last scoped scan attempt
53
- this.settings = { external_sources: [] } // durable user preferences, not derived scan state
51
+ this.totals = { lifetime_bytes_saved: 0 }
54
52
  this.byIno = new Map() // "dev:ino" -> hash
55
53
  this.pathsByIno = new Map() // "dev:ino" -> Set(paths), for bounded group updates
56
54
  this.eventsCache = null
@@ -170,24 +168,9 @@ class Registry {
170
168
  scanFor(sourceId = null) {
171
169
  return sourceId ? (this.sourceScans.get(sourceId) || null) : this.lastScan
172
170
  }
173
- scanAttemptFor(sourceId = null) {
174
- return sourceId
175
- ? (this.sourceScanAttempts.get(sourceId) || null)
176
- : this.lastScanAttempt
177
- }
178
171
  setLastScan(meta, sourceId = null) {
179
- if (sourceId) {
180
- this.sourceScans.set(sourceId, meta)
181
- this.sourceScanAttempts.set(sourceId, meta)
182
- } else {
183
- this.lastScan = meta
184
- this.lastScanAttempt = meta
185
- }
186
- this.schedulePersist()
187
- }
188
- setScanAttempt(meta, sourceId = null) {
189
- if (sourceId) this.sourceScanAttempts.set(sourceId, meta)
190
- else this.lastScanAttempt = meta
172
+ if (sourceId) this.sourceScans.set(sourceId, meta)
173
+ else this.lastScan = meta
191
174
  this.schedulePersist()
192
175
  }
193
176
  inoKey(dev, ino) {
@@ -217,10 +200,9 @@ class Registry {
217
200
  // A path has exactly one derived classification. Registering a physical
218
201
  // name always clears an older pending-duplicate classification first.
219
202
  this.duplicates.delete(filePath)
220
- const next = Object.assign({ created: Date.now() }, entry)
221
- delete next.mode
203
+ const next = Object.assign({ created: Date.now(), mode: "link" }, entry)
222
204
  const sameTrackedFile = previous && previous.hash === next.hash &&
223
- previous.dev === next.dev && previous.ino === next.ino
205
+ previous.mode === next.mode && previous.dev === next.dev && previous.ino === next.ino
224
206
  if (next.batch_id === undefined && sameTrackedFile && previous.batch_id) {
225
207
  next.batch_id = previous.batch_id
226
208
  }
@@ -303,46 +285,22 @@ class Registry {
303
285
  }
304
286
  this.schedulePersist()
305
287
  }
306
- snapshotState() {
307
- const cloneMap = (source) => new Map([...source].map(([key, value]) => [
308
- key,
309
- value && typeof value === "object"
310
- ? Object.assign({}, value, Array.isArray(value.source_urls)
311
- ? { source_urls: [...value.source_urls] }
312
- : {})
313
- : value
314
- ]))
315
- return {
316
- blobs: cloneMap(this.blobs),
317
- links: cloneMap(this.links),
318
- scanIndex: cloneMap(this.scanIndex),
319
- duplicates: cloneMap(this.duplicates),
320
- excluded: cloneMap(this.excluded),
321
- lastScan: this.lastScan ? Object.assign({}, this.lastScan) : null,
322
- lastScanAttempt: this.lastScanAttempt ? Object.assign({}, this.lastScanAttempt) : null,
323
- sourceScans: cloneMap(this.sourceScans),
324
- sourceScanAttempts: cloneMap(this.sourceScanAttempts),
325
- settings: Object.assign({}, this.settings, {
326
- external_sources: [...(this.settings.external_sources || [])]
327
- }),
328
- byIno: new Map(this.byIno),
329
- pathsByIno: new Map([...this.pathsByIno].map(([key, paths]) => [key, new Set(paths)]))
330
- }
331
- }
332
- replaceState(state, options = {}) {
288
+ addSaved(bytes) {
289
+ this.totals.lifetime_bytes_saved += bytes
290
+ this.schedulePersist()
291
+ }
292
+ replaceState(state) {
333
293
  this.blobs = state.blobs
334
294
  this.links = state.links
335
295
  this.scanIndex = state.scanIndex
336
296
  this.duplicates = state.duplicates
337
297
  this.excluded = state.excluded
338
298
  this.lastScan = state.lastScan
339
- this.lastScanAttempt = state.lastScanAttempt || null
340
299
  this.sourceScans = state.sourceScans
341
- this.sourceScanAttempts = state.sourceScanAttempts || new Map()
342
- this.settings = Object.assign({ external_sources: [] }, state.settings)
300
+ this.totals = state.totals
343
301
  this.byIno = state.byIno
344
302
  this.pathsByIno = state.pathsByIno
345
- if (options.persist !== false) this.schedulePersist()
303
+ this.schedulePersist()
346
304
  }
347
305
  // Returns { corrupt: true } when the snapshot exists but cannot be parsed,
348
306
  // so the caller can rebuild from disk instead of blocking startup.
@@ -375,16 +333,10 @@ class Registry {
375
333
  }))
376
334
  }
377
335
  }
378
- const legacyCopies = []
379
336
  for (const [k, v] of Object.entries(json.links || {})) {
380
337
  if (!isRecord(v) || !SHA256_RE.test(v.hash) ||
381
338
  !Number.isFinite(v.dev) || !Number.isFinite(v.ino)) continue
382
- if (v.mode === "copy") {
383
- legacyCopies.push([k, v])
384
- continue
385
- }
386
- const link = Object.assign({}, v)
387
- delete link.mode
339
+ const link = Object.assign({}, v, { mode: v.mode === "copy" ? "copy" : "link" })
388
340
  this.links.set(k, link)
389
341
  if (link.dev !== undefined && link.ino !== undefined) {
390
342
  const key = this.inoKey(link.dev, link.ino)
@@ -406,26 +358,6 @@ class Registry {
406
358
  }))
407
359
  }
408
360
  }
409
- // Older builds recorded independent files as mode:"copy" links. They were
410
- // never shared. Migrate only their review metadata and never recreate that
411
- // unsupported state.
412
- for (const [filePath, link] of legacyCopies) {
413
- if (this.duplicates.has(filePath) || !this.blobs.has(link.hash)) continue
414
- const indexed = this.scanIndex.get(filePath)
415
- if (!indexed || indexed.hash !== link.hash) continue
416
- this.duplicates.set(filePath, {
417
- hash: link.hash,
418
- size: indexed.size,
419
- app: link.app || null,
420
- source_id: link.source_id || indexed.source_id || null,
421
- discovered: link.created || Date.now(),
422
- dev: indexed.dev,
423
- ino: indexed.ino,
424
- mtime: indexed.mtime,
425
- ctime: indexed.ctime,
426
- unavailable_reason: "unsupported_disk"
427
- })
428
- }
429
361
  for (const [k, v] of Object.entries(json.excluded || {})) {
430
362
  if (!isRecord(v)) continue
431
363
  const linked = this.links.get(k)
@@ -439,45 +371,14 @@ class Registry {
439
371
  size: Number.isFinite(v.size) && v.size >= 0 ? v.size : 0
440
372
  }))
441
373
  }
442
- if (isRecord(json.scans)) {
443
- for (const [scopeId, value] of Object.entries(json.scans)) {
444
- if (!isRecord(value)) continue
445
- const complete = isRecord(value.last_complete) ? value.last_complete : null
446
- const attempt = isRecord(value.last_attempt) ? value.last_attempt : null
447
- if (scopeId === "global") {
448
- this.lastScan = complete
449
- this.lastScanAttempt = attempt
450
- } else {
451
- if (complete) this.sourceScans.set(scopeId, complete)
452
- if (attempt) this.sourceScanAttempts.set(scopeId, attempt)
453
- }
454
- }
455
- } else {
456
- if (isRecord(json.last_scan)) {
457
- this.lastScan = json.last_scan
458
- this.lastScanAttempt = json.last_scan
459
- }
460
- for (const [sourceId, scan] of Object.entries(json.source_scans || {})) {
461
- if (!isRecord(scan)) continue
462
- this.sourceScans.set(sourceId, scan)
463
- this.sourceScanAttempts.set(sourceId, scan)
464
- }
374
+ if (isRecord(json.last_scan)) {
375
+ this.lastScan = json.last_scan
465
376
  }
466
- if (isRecord(json.settings)) {
467
- if (CANDIDATE_SIZE_OPTIONS.includes(json.settings.candidate_min_bytes)) {
468
- this.settings.candidate_min_bytes = json.settings.candidate_min_bytes
469
- }
470
- if (Array.isArray(json.settings.external_sources)) {
471
- const seen = new Set()
472
- for (const value of json.settings.external_sources) {
473
- if (typeof value !== "string" || !path.isAbsolute(value)) continue
474
- const resolved = path.resolve(value)
475
- const key = process.platform === "win32" ? resolved.toLowerCase() : resolved
476
- if (seen.has(key)) continue
477
- seen.add(key)
478
- this.settings.external_sources.push(resolved)
479
- }
480
- }
377
+ for (const [sourceId, scan] of Object.entries(json.source_scans || {})) {
378
+ if (isRecord(scan)) this.sourceScans.set(sourceId, scan)
379
+ }
380
+ if (json.totals && Number.isFinite(json.totals.lifetime_bytes_saved)) {
381
+ this.totals = { lifetime_bytes_saved: Math.max(0, json.totals.lifetime_bytes_saved) }
481
382
  }
482
383
  return { corrupt: false, existed: true }
483
384
  }
@@ -491,12 +392,7 @@ class Registry {
491
392
  }
492
393
  async endBatch(options = {}) {
493
394
  if (this.persistDepth > 0) this.persistDepth -= 1
494
- if (this.persistDepth) return
495
- if (options.discard) {
496
- this.persistDirty = false
497
- return
498
- }
499
- if (!this.persistDirty) return
395
+ if (this.persistDepth || !this.persistDirty) return
500
396
  if (options.flush === false) this.schedulePersist()
501
397
  else await this.flush()
502
398
  }
@@ -518,21 +414,6 @@ class Registry {
518
414
  this.persistTimer = null
519
415
  }
520
416
  this.persistDirty = false
521
- const scans = {
522
- global: {
523
- last_complete: this.lastScan,
524
- last_attempt: this.lastScanAttempt
525
- }
526
- }
527
- for (const sourceId of new Set([
528
- ...this.sourceScans.keys(),
529
- ...this.sourceScanAttempts.keys()
530
- ])) {
531
- scans[sourceId] = {
532
- last_complete: this.sourceScans.get(sourceId) || null,
533
- last_attempt: this.sourceScanAttempts.get(sourceId) || null
534
- }
535
- }
536
417
  const json = {
537
418
  version: 1,
538
419
  blobs: Object.fromEntries(this.blobs),
@@ -540,8 +421,9 @@ class Registry {
540
421
  scan_index: Object.fromEntries(this.scanIndex),
541
422
  duplicates: Object.fromEntries(this.duplicates),
542
423
  excluded: Object.fromEntries(this.excluded),
543
- scans,
544
- settings: this.settings
424
+ last_scan: this.lastScan,
425
+ source_scans: Object.fromEntries(this.sourceScans),
426
+ totals: this.totals
545
427
  }
546
428
  const write = async () => {
547
429
  await this.atomicWrite(this.snapshotPath, JSON.stringify(json))
@@ -561,25 +443,21 @@ class Registry {
561
443
  }
562
444
  }
563
445
  async appendEvent(event) {
564
- return this.appendEvents([event])
565
- }
566
- async appendEvents(events) {
567
- if (!events.length) return
568
- const entries = events.map((event) => Object.assign({ ts: Date.now() }, event))
569
- const contents = entries.map((event) => JSON.stringify(event)).join("\n") + "\n"
446
+ const line = JSON.stringify(Object.assign({ ts: Date.now() }, event)) + "\n"
570
447
  const append = async () => {
571
- await this.appendFile(this.eventsPath, contents)
572
- if (this.eventsCache) this.eventsCache.push(...entries)
573
- this.eventsSinceCompact += entries.length
448
+ await this.appendFile(this.eventsPath, line)
449
+ if (this.eventsCache) this.eventsCache.push(JSON.parse(line))
450
+ this.eventsSinceCompact += 1
574
451
  if ((this.eventsCache && this.eventsCache.length > this.maxEvents + this.compactEvery) ||
575
452
  this.eventsSinceCompact >= this.compactEvery) {
576
453
  await this.compactEventsNow()
577
454
  }
578
455
  }
579
456
  const pending = this.eventPromise.then(append, append)
580
- // One ordered writer keeps action and scan batches ordered. Keep it usable
581
- // after a failure while returning the real append result so the dashboard
582
- // can expose missing activity without misreporting the action.
457
+ // One ordered writer prevents a scan with many matches from opening an
458
+ // unbounded number of concurrent append operations. Keep the writer
459
+ // usable after a failure while returning the real append result so the
460
+ // dashboard can expose missing activity without misreporting the action.
583
461
  this.eventPromise = pending.then(
584
462
  () => { this.eventError = null },
585
463
  (error) => {