pinokiod 8.0.69 → 8.0.72

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/kernel/index.js CHANGED
@@ -1062,15 +1062,15 @@ class Kernel {
1062
1062
  kill() {
1063
1063
  process.kill(process.pid, "SIGTERM")
1064
1064
  }
1065
- async disposeVaultAutomaticScans() {
1065
+ async disposeVault() {
1066
1066
  const vault = this.vault
1067
- if (!vault || !vault.automaticScans) return false
1067
+ if (!vault) return false
1068
1068
  if (vault.ready) await Promise.resolve(vault.ready).catch(() => {})
1069
1069
  try {
1070
- await vault.automaticScans.dispose()
1070
+ await vault.dispose()
1071
1071
  return true
1072
1072
  } catch (error) {
1073
- console.warn("Vault automatic cleanup error:",
1073
+ console.warn("Vault cleanup error:",
1074
1074
  error && error.message ? error.message : error)
1075
1075
  return false
1076
1076
  }
@@ -1092,7 +1092,7 @@ class Kernel {
1092
1092
  })
1093
1093
 
1094
1094
  let home = this.store.get("home") || process.env.PINOKIO_HOME
1095
- await this.disposeVaultAutomaticScans()
1095
+ await this.disposeVault()
1096
1096
  this.vault = null
1097
1097
  this.homedir = home
1098
1098
 
package/kernel/shell.js CHANGED
@@ -290,6 +290,7 @@ class Shell {
290
290
  }
291
291
  }
292
292
 
293
+ setDefaultEnvValue(this.env, "UV_HTTP_TIMEOUT", "60")
293
294
  setDefaultEnvValue(this.env, "HF_HUB_DISABLE_UPDATE_CHECK", "1")
294
295
  setDefaultEnvValue(this.env, "HF_TOKEN_PATH", this.kernel.envs?.HF_TOKEN_PATH || path.resolve(this.kernel.homedir, "cache", "HF_AUTH", "token"))
295
296
 
@@ -2,9 +2,12 @@ const fs = require("fs")
2
2
  const path = require("path")
3
3
  const {
4
4
  SIZE_THRESHOLD,
5
- isCandidateFileSize
5
+ isCandidateFileSize,
6
+ SHA256_RE,
7
+ ENTRY_BATCH_SIZE
6
8
  } = require("./constants")
7
9
  const { statMany } = require("./walker")
10
+ const { fileSnapshot, sameSnapshot } = require("./snapshot")
8
11
  const {
9
12
  cancelledError,
10
13
  isPathError
@@ -24,6 +27,11 @@ const inside = (root, candidate) => {
24
27
  )
25
28
  }
26
29
 
30
+ const pathKey = (value) => {
31
+ const resolved = path.resolve(value)
32
+ return process.platform === "win32" ? resolved.toLowerCase() : resolved
33
+ }
34
+
27
35
  class AutomaticScans {
28
36
  constructor(vault) {
29
37
  this.vault = vault
@@ -44,6 +52,7 @@ class AutomaticScans {
44
52
  this.manualDepth = 0
45
53
  this.observedApps = new Set()
46
54
  this.changedPaths = new Map()
55
+ this.changedPathIndex = new Map()
47
56
  this.pendingStops = new Map()
48
57
  this.lifecycleWork = new Set()
49
58
  this.stopSettleMs = STOP_SETTLE_MS
@@ -128,6 +137,7 @@ class AutomaticScans {
128
137
  }
129
138
  this.observedApps.clear()
130
139
  this.changedPaths.clear()
140
+ this.changedPathIndex.clear()
131
141
  if (watcherError) throw watcherError
132
142
  }
133
143
 
@@ -144,7 +154,8 @@ class AutomaticScans {
144
154
  if (!event || !["create", "update", "delete"].includes(event.type)) {
145
155
  continue
146
156
  }
147
- this.recordChangedPath(event.path)
157
+ if (event.type === "delete") this.removeChangedPath(event.path)
158
+ else this.recordChangedPath(event.path)
148
159
  }
149
160
  }
150
161
 
@@ -163,19 +174,95 @@ class AutomaticScans {
163
174
  paths = new Set()
164
175
  this.changedPaths.set(app, paths)
165
176
  }
177
+ let index = this.changedPathIndex.get(app)
178
+ if (!index) {
179
+ index = { exact: new Map(), descendants: new Map() }
180
+ this.changedPathIndex.set(app, index)
181
+ }
182
+ const key = pathKey(resolved)
183
+ if (index.exact.has(key)) return true
166
184
  paths.add(resolved)
185
+ index.exact.set(key, resolved)
186
+ this.addChangedPathToIndex(index.descendants, appRoot, resolved)
187
+ return true
188
+ }
189
+
190
+ changedPathParentKeys(appRoot, filePath) {
191
+ const rootKey = pathKey(appRoot)
192
+ const keys = []
193
+ let current = path.dirname(path.resolve(filePath))
194
+ while (true) {
195
+ const key = pathKey(current)
196
+ keys.push(key)
197
+ if (key === rootKey) return keys
198
+ current = path.dirname(current)
199
+ }
200
+ }
201
+
202
+ addChangedPathToIndex(index, appRoot, filePath) {
203
+ for (const key of this.changedPathParentKeys(appRoot, filePath)) {
204
+ let descendants = index.get(key)
205
+ if (!descendants) {
206
+ descendants = new Set()
207
+ index.set(key, descendants)
208
+ }
209
+ descendants.add(filePath)
210
+ }
211
+ }
212
+
213
+ removeChangedPathFromIndex(index, appRoot, filePath) {
214
+ index.exact.delete(pathKey(filePath))
215
+ for (const key of this.changedPathParentKeys(appRoot, filePath)) {
216
+ const descendants = index.descendants.get(key)
217
+ if (!descendants) continue
218
+ descendants.delete(filePath)
219
+ if (!descendants.size) index.descendants.delete(key)
220
+ }
221
+ }
222
+
223
+ removeChangedPath(filePath) {
224
+ if (this.disposed || !this.supported ||
225
+ typeof filePath !== "string" || !filePath) {
226
+ return false
227
+ }
228
+ const app = this.appForLaunchPath(filePath)
229
+ if (!app || !this.observedApps.has(app)) return false
230
+ const paths = this.changedPaths.get(app)
231
+ if (!paths) return false
232
+ const resolved = path.resolve(filePath)
233
+ const appRoot = path.resolve(this.vault.kernel.homedir, "api", app)
234
+ const index = this.changedPathIndex.get(app)
235
+ if (!index) return false
236
+ const key = pathKey(resolved)
237
+ const exact = index.exact.get(key)
238
+ const descendants = index.descendants.get(key)
239
+ if (!exact && !descendants) return false
240
+ const matches = new Set(descendants || [])
241
+ if (exact) matches.add(exact)
242
+ for (const candidate of matches) {
243
+ paths.delete(candidate)
244
+ this.removeChangedPathFromIndex(index, appRoot, candidate)
245
+ }
246
+ if (!paths.size) this.discardChangedPaths(app)
167
247
  return true
168
248
  }
169
249
 
170
250
  discardChangedPaths(app) {
171
251
  this.changedPaths.delete(app)
252
+ this.changedPathIndex.delete(app)
172
253
  }
173
254
 
174
255
  consumeChangedPaths(app, paths) {
175
256
  const collected = this.changedPaths.get(app)
176
257
  if (!collected) return
177
- for (const filePath of paths || []) collected.delete(filePath)
178
- if (!collected.size) this.changedPaths.delete(app)
258
+ const appRoot = path.resolve(this.vault.kernel.homedir, "api", app)
259
+ const index = this.changedPathIndex.get(app)
260
+ for (const filePath of paths || []) {
261
+ const resolved = path.resolve(filePath)
262
+ if (!collected.delete(resolved) || !index) continue
263
+ this.removeChangedPathFromIndex(index, appRoot, resolved)
264
+ }
265
+ if (!collected.size) this.discardChangedPaths(app)
179
266
  }
180
267
 
181
268
  async candidateThreshold() {
@@ -745,6 +832,7 @@ class AutomaticScans {
745
832
  }
746
833
  this.active.cancelled = true
747
834
  this.active.reason = reason
835
+ if (this.active.controller) this.active.controller.abort()
748
836
  this.log("check-cancel-requested", { app, reason })
749
837
  return true
750
838
  }
@@ -755,6 +843,111 @@ class AutomaticScans {
755
843
  }
756
844
  }
757
845
 
846
+ automaticHashKey(entry) {
847
+ return [
848
+ this.automaticIdentityKey(entry),
849
+ entry.size,
850
+ entry.mtime,
851
+ entry.ctime
852
+ ].join("\0")
853
+ }
854
+
855
+ automaticIdentityKey(entry) {
856
+ return entry.ino !== 0
857
+ ? `inode:${entry.dev}:${entry.ino}`
858
+ : `path:${pathKey(entry.path)}`
859
+ }
860
+
861
+ logVerificationSkip(app, entry, reason, counts, error = null) {
862
+ if (error) counts.path_errors += 1
863
+ else counts.unstable_hashes += 1
864
+ this.log("verification-skipped", {
865
+ app,
866
+ path: entry.path,
867
+ kind: entry.kind,
868
+ reason,
869
+ code: error && error.code,
870
+ message: error && error.message
871
+ })
872
+ }
873
+
874
+ async verifiedAutomaticHash(active, entry, memory, verifiedHashes, counts) {
875
+ this.checkpoint(active)
876
+ let current
877
+ try {
878
+ current = await fs.promises.lstat(entry.path)
879
+ } catch (error) {
880
+ if (!isPathError(error)) throw error
881
+ if (entry.kind === "changed" && isMissing(error)) {
882
+ counts.path_errors += 1
883
+ return null
884
+ }
885
+ this.logVerificationSkip(active.app, entry, "unreadable", counts, error)
886
+ return null
887
+ }
888
+ this.checkpoint(active)
889
+ if (!current.isFile() || current.isSymbolicLink() ||
890
+ current.dev !== entry.dev || current.size !== entry.size) {
891
+ this.logVerificationSkip(
892
+ active.app, entry, "snapshot-changed", counts)
893
+ return null
894
+ }
895
+ const currentEntry = Object.assign({}, entry, fileSnapshot(current))
896
+ const key = this.automaticHashKey(currentEntry)
897
+ const cachedSnapshot = {
898
+ size: entry.cached_size,
899
+ mtime: entry.cached_mtime,
900
+ ctime: entry.cached_ctime,
901
+ dev: entry.cached_dev,
902
+ ino: entry.cached_ino
903
+ }
904
+ const reusable = memory.get(key) ||
905
+ (sameSnapshot(entry, current) && SHA256_RE.test(entry.hash || "")
906
+ ? entry.hash
907
+ : null) ||
908
+ (sameSnapshot(cachedSnapshot, current) &&
909
+ SHA256_RE.test(entry.cached_hash || "")
910
+ ? entry.cached_hash
911
+ : null)
912
+ if (reusable) {
913
+ counts.hash_reuses += 1
914
+ memory.set(key, reusable)
915
+ verifiedHashes.set(pathKey(entry.path), Object.assign({}, currentEntry, {
916
+ hash: reusable
917
+ }))
918
+ return reusable
919
+ }
920
+ let verified
921
+ try {
922
+ verified = await this.vault.scanner.hashStable(currentEntry, {
923
+ signal: active.controller && active.controller.signal
924
+ })
925
+ } catch (error) {
926
+ if (error && error.code === "EVAULTCANCELLED") throw error
927
+ if (!isPathError(error)) throw error
928
+ counts.hash_failures += 1
929
+ if (entry.kind === "changed" && isMissing(error)) {
930
+ counts.path_errors += 1
931
+ return null
932
+ }
933
+ this.logVerificationSkip(active.app, entry, "hash-failed", counts, error)
934
+ return null
935
+ }
936
+ this.checkpoint(active)
937
+ if (!verified.stable || !SHA256_RE.test(verified.result.hash || "")) {
938
+ this.logVerificationSkip(
939
+ active.app, entry, "changed-during-hash", counts)
940
+ return null
941
+ }
942
+ counts.hashed += 1
943
+ counts.hash_bytes += Math.max(0, Number(verified.result.size) || 0)
944
+ memory.set(key, verified.result.hash)
945
+ verifiedHashes.set(pathKey(entry.path), Object.assign({}, currentEntry, {
946
+ hash: verified.result.hash
947
+ }))
948
+ return verified.result.hash
949
+ }
950
+
758
951
  async runPrecheck(active) {
759
952
  const app = active.app
760
953
  const root = path.resolve(this.vault.kernel.homedir, "api", app)
@@ -768,18 +961,24 @@ class AutomaticScans {
768
961
  files: 0,
769
962
  bytes: 0,
770
963
  candidates: 0,
771
- path_errors: 0
964
+ path_errors: 0,
965
+ hashed: 0,
966
+ hash_bytes: 0,
967
+ hash_reuses: 0,
968
+ hash_failures: 0,
969
+ unstable_hashes: 0
772
970
  }
773
971
  const threshold = await this.candidateThreshold()
774
972
  const paths = [...new Set(active.paths || [])].filter((filePath) =>
775
973
  typeof filePath === "string" && inside(root, filePath))
974
+ const verifiedHashes = new Map()
776
975
  await this.vault.registry.beginAutomaticPrecheck(app)
777
976
  this.log("check-started", {
778
977
  app,
779
978
  root,
780
979
  threshold_bytes: threshold,
781
980
  changed_paths: paths.length,
782
- policy: "metadata-only"
981
+ policy: "metadata-prefilter-sha256"
783
982
  })
784
983
  try {
785
984
  this.checkpoint(active)
@@ -789,6 +988,7 @@ class AutomaticScans {
789
988
  onError: (error, filePath) => {
790
989
  if (!isPathError(error)) return false
791
990
  counts.path_errors += 1
991
+ if (isMissing(error)) return true
792
992
  this.log("path-skipped", {
793
993
  app,
794
994
  path: filePath,
@@ -817,8 +1017,14 @@ class AutomaticScans {
817
1017
  candidates.push({
818
1018
  path: paths[index],
819
1019
  size: stat.size,
1020
+ mtime: stat.mtimeMs,
1021
+ ctime: stat.ctimeMs,
820
1022
  dev: stat.dev,
821
- ino: stat.ino
1023
+ ino: stat.ino,
1024
+ nlink: stat.nlink,
1025
+ mode: stat.mode,
1026
+ uid: stat.uid,
1027
+ gid: stat.gid
822
1028
  })
823
1029
  }
824
1030
  counts.candidates = candidates.length
@@ -831,15 +1037,123 @@ class AutomaticScans {
831
1037
  error.code = "ENOENT"
832
1038
  throw error
833
1039
  }
834
- const result = await this.vault.registry.automaticPrecheckResult(app)
1040
+ const verifiedMatches = []
1041
+ let group = null
1042
+ const preparePeers = () => {
1043
+ if (!group || group.unmatched) return
1044
+ group.unmatched = new Map()
1045
+ group.unmatchedIdentities = new Map()
1046
+ for (const [hash, value] of group.candidatesByHash) {
1047
+ if (value.identities.size > 1) {
1048
+ for (const candidate of value.candidates) {
1049
+ group.matched.add(pathKey(candidate.path))
1050
+ }
1051
+ } else {
1052
+ const identity = value.identities.values().next().value
1053
+ group.unmatched.set(hash, {
1054
+ identity,
1055
+ candidates: value.candidates
1056
+ })
1057
+ group.unmatchedIdentities.set(
1058
+ identity,
1059
+ (group.unmatchedIdentities.get(identity) || 0) + 1)
1060
+ }
1061
+ }
1062
+ }
1063
+ const finishGroup = () => {
1064
+ if (!group) return
1065
+ preparePeers()
1066
+ for (const [hash, value] of group.candidatesByHash) {
1067
+ for (const candidate of value.candidates) {
1068
+ if (!group.matched.has(pathKey(candidate.path))) continue
1069
+ verifiedMatches.push({ path: candidate.path, hash })
1070
+ }
1071
+ }
1072
+ group = null
1073
+ }
1074
+ let cursor = null
1075
+ do {
1076
+ const page = await this.vault.registry.automaticPrecheckEntries(
1077
+ app, cursor, ENTRY_BATCH_SIZE)
1078
+ this.checkpoint(active)
1079
+ for (const entry of page.entries) {
1080
+ const key = `${entry.dev}\0${entry.size}`
1081
+ if (!group || group.key !== key) {
1082
+ finishGroup()
1083
+ group = {
1084
+ key,
1085
+ candidatesByHash: new Map(),
1086
+ matched: new Set(),
1087
+ unmatched: null,
1088
+ unmatchedIdentities: null,
1089
+ memory: new Map()
1090
+ }
1091
+ }
1092
+ if (entry.kind === "changed") {
1093
+ const candidate = entry
1094
+ this.checkpoint(active)
1095
+ const hash = await this.verifiedAutomaticHash(
1096
+ active, candidate, group.memory, verifiedHashes, counts)
1097
+ if (!hash) continue
1098
+ let value = group.candidatesByHash.get(hash)
1099
+ if (!value) {
1100
+ value = { candidates: [], identities: new Set() }
1101
+ group.candidatesByHash.set(hash, value)
1102
+ }
1103
+ value.candidates.push(candidate)
1104
+ value.identities.add(this.automaticIdentityKey(candidate))
1105
+ continue
1106
+ }
1107
+ preparePeers()
1108
+ if (!group.unmatched.size) continue
1109
+ this.checkpoint(active)
1110
+ const identity = this.automaticIdentityKey(entry)
1111
+ if (group.unmatchedIdentities.size === 1 &&
1112
+ group.unmatchedIdentities.has(identity)) continue
1113
+ const hash = await this.verifiedAutomaticHash(
1114
+ active, entry, group.memory, verifiedHashes, counts)
1115
+ const wanted = hash && group.unmatched.get(hash)
1116
+ if (!wanted || wanted.identity === identity) {
1117
+ continue
1118
+ }
1119
+ for (const candidate of wanted.candidates) {
1120
+ group.matched.add(pathKey(candidate.path))
1121
+ }
1122
+ group.unmatched.delete(hash)
1123
+ const remaining = group.unmatchedIdentities.get(wanted.identity) - 1
1124
+ if (remaining) {
1125
+ group.unmatchedIdentities.set(wanted.identity, remaining)
1126
+ } else {
1127
+ group.unmatchedIdentities.delete(wanted.identity)
1128
+ }
1129
+ }
1130
+ if (verifiedHashes.size >= ENTRY_BATCH_SIZE) {
1131
+ await this.vault.registry.rememberHashCache(
1132
+ [...verifiedHashes.values()])
1133
+ verifiedHashes.clear()
1134
+ this.checkpoint(active)
1135
+ }
1136
+ cursor = page.next_cursor
1137
+ } while (cursor)
1138
+ finishGroup()
1139
+ this.checkpoint(active)
1140
+ const result = await this.vault.registry.automaticPrecheckResult(
1141
+ app, verifiedMatches)
835
1142
  this.checkpoint(active)
836
1143
  return Object.assign({}, counts, {
837
1144
  signature: result.signature,
838
- possible_files: Math.max(0, Number(result.files) || 0),
1145
+ verified_files: Math.max(0, Number(result.files) || 0),
839
1146
  duration_ms: Date.now() - startedAt
840
1147
  })
841
1148
  } finally {
842
- await this.vault.registry.abortAutomaticPrecheck(app)
1149
+ try {
1150
+ if (verifiedHashes.size) {
1151
+ await this.vault.registry.rememberHashCache(
1152
+ [...verifiedHashes.values()])
1153
+ }
1154
+ } finally {
1155
+ await this.vault.registry.abortAutomaticPrecheck(app)
1156
+ }
843
1157
  }
844
1158
  }
845
1159
 
@@ -894,6 +1208,7 @@ class AutomaticScans {
894
1208
  paths: [...new Set(entry.paths || [])],
895
1209
  cancelled: false,
896
1210
  reason: null,
1211
+ controller: new AbortController(),
897
1212
  promise: null
898
1213
  }
899
1214
  this.active = active
@@ -933,7 +1248,12 @@ class AutomaticScans {
933
1248
  files: result && result.files,
934
1249
  bytes: result && result.bytes,
935
1250
  candidates: result && result.candidates,
936
- possible_files: result && result.possible_files,
1251
+ verified_files: result && result.verified_files,
1252
+ hashed: result && result.hashed,
1253
+ hash_bytes: result && result.hash_bytes,
1254
+ hash_reuses: result && result.hash_reuses,
1255
+ hash_failures: result && result.hash_failures,
1256
+ unstable_hashes: result && result.unstable_hashes,
937
1257
  path_errors: result && result.path_errors,
938
1258
  error: error && error.message
939
1259
  })
@@ -1037,8 +1357,8 @@ class AutomaticScans {
1037
1357
  }
1038
1358
 
1039
1359
  async publishResultNow(app, result = {}) {
1040
- const possibleFiles = Math.max(0, Number(result.possible_files) || 0)
1041
- if (possibleFiles > 0 && result.signature) {
1360
+ const verifiedFiles = Math.max(0, Number(result.verified_files) || 0)
1361
+ if (verifiedFiles > 0 && result.signature) {
1042
1362
  const acknowledged = (this.settings.get(app) || {})
1043
1363
  .acknowledged_signature
1044
1364
  const options = { signature: result.signature }
@@ -1059,9 +1379,9 @@ class AutomaticScans {
1059
1379
  previous: null,
1060
1380
  hidden: acknowledged === result.signature
1061
1381
  })
1062
- this.log("possible-matches", {
1382
+ this.log("verified-matches", {
1063
1383
  app,
1064
- possible_files: possibleFiles,
1384
+ verified_files: verifiedFiles,
1065
1385
  acknowledged: acknowledged === result.signature
1066
1386
  })
1067
1387
  return "result"
@@ -1076,7 +1396,7 @@ class AutomaticScans {
1076
1396
  await this.vault.registry.setAutomaticAppScanState(app, null)
1077
1397
  }
1078
1398
  this.entries.delete(app)
1079
- this.log("no-possible-matches", { app })
1399
+ this.log("no-verified-matches", { app })
1080
1400
  return "empty"
1081
1401
  }
1082
1402
 
@@ -1246,17 +1566,17 @@ class AutomaticScans {
1246
1566
 
1247
1567
  async acknowledge(app, signature) {
1248
1568
  if (this.disposed) {
1249
- return { error: "That possible-match result is no longer available." }
1569
+ return { error: "That automatic duplicate result is no longer available." }
1250
1570
  }
1251
1571
  return this.withAppTransition(app, async () => {
1252
1572
  await this.hydrate()
1253
1573
  if (this.disposed) {
1254
- return { error: "That possible-match result is no longer available." }
1574
+ return { error: "That automatic duplicate result is no longer available." }
1255
1575
  }
1256
1576
  const current = this.entries.get(app)
1257
1577
  const entry = this.publicResult(current)
1258
1578
  if (!entry) {
1259
- return { error: "That possible-match result is no longer available." }
1579
+ return { error: "That automatic duplicate result is no longer available." }
1260
1580
  }
1261
1581
  if (typeof signature !== "string" || signature !== entry.signature) {
1262
1582
  return { stale: true, app }
@@ -7,14 +7,27 @@ const fs = require('fs')
7
7
  // same sha256 digest, one file at a time.
8
8
  const HASH_READ_SIZE = 1024 * 1024
9
9
  const HASH_PROGRESS_INTERVAL_MS = 1000
10
+ const jobs = new Map()
10
11
 
11
- parentPort.on('message', ({ id, filePath }) => {
12
+ parentPort.on('message', ({ id, filePath, cancel }) => {
13
+ if (cancel) {
14
+ const job = jobs.get(id)
15
+ if (!job) return
16
+ const error = new Error('Hashing cancelled.')
17
+ error.code = 'EVAULTCANCELLED'
18
+ job.stream.destroy(error)
19
+ return
20
+ }
12
21
  const hash = crypto.createHash('sha256')
13
22
  let size = 0
14
23
  let lastProgressAt = 0
15
24
  let complete = false
16
25
  const flags = fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0)
17
- const stream = fs.createReadStream(filePath, { flags, highWaterMark: HASH_READ_SIZE })
26
+ const stream = fs.createReadStream(filePath, {
27
+ flags,
28
+ highWaterMark: HASH_READ_SIZE
29
+ })
30
+ jobs.set(id, { stream })
18
31
  stream.on('data', (chunk) => {
19
32
  size += chunk.length
20
33
  hash.update(chunk)
@@ -27,11 +40,13 @@ parentPort.on('message', ({ id, filePath }) => {
27
40
  stream.on('error', (error) => {
28
41
  if (complete) return
29
42
  complete = true
43
+ jobs.delete(id)
30
44
  parentPort.postMessage({ id, error: error.message, code: error.code })
31
45
  })
32
46
  stream.on('end', () => {
33
47
  if (complete) return
34
48
  complete = true
49
+ jobs.delete(id)
35
50
  parentPort.postMessage({ id, hash: hash.digest('hex'), size })
36
51
  })
37
52
  })