@threadedstack/wmd-web 0.0.0-stage → 0.1.12
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/README.md +2601 -2
- package/RELEASE.json +123 -0
- package/export-page/coi-sw.js +20 -0
- package/export-page/index.html +17 -0
- package/export-page/run.css +10 -0
- package/export-page/run.js +236 -0
- package/guest/arm64/Image +4 -0
- package/guest/arm64/Image-4k +4 -0
- package/guest/arm64/initramfs.cpio.gz +0 -0
- package/guest/arm64/vm.wasm +0 -0
- package/package.json +36 -4
- package/page/sw.js +381 -0
- package/relay/control.ts +139 -0
- package/relay/engine.ts +787 -0
- package/relay/hosted.ts +660 -0
- package/relay/keys.ts +53 -0
- package/relay/local-tls.ts +121 -0
- package/relay/lock.ts +81 -0
- package/relay/logfile.ts +138 -0
- package/relay/main.ts +642 -0
- package/relay/policy.ts +241 -0
- package/relay/relay.ts +266 -0
- package/relay/token.ts +116 -0
- package/relay/v1.ts +107 -0
- package/relay/wisp.ts +615 -0
- package/src/base.js +325 -0
- package/src/boot.js +2037 -0
- package/src/calib-worker.js +40 -0
- package/src/fs/9p/codec.js +303 -0
- package/src/fs/9p/server.js +521 -0
- package/src/fs/errno.js +43 -0
- package/src/fs/graph.js +380 -0
- package/src/fs/overlay.js +1018 -0
- package/src/fs/pack-write.js +165 -0
- package/src/fs/pack.js +427 -0
- package/src/fs/seed.js +22 -0
- package/src/fs/svc-snapshot.js +65 -0
- package/src/fs/upper-pack.js +112 -0
- package/src/host/base-cache.js +195 -0
- package/src/host/base-file.js +328 -0
- package/src/host/base-io.js +93 -0
- package/src/host/boot-events.js +209 -0
- package/src/host/chan-service.js +300 -0
- package/src/host/chan-services.js +689 -0
- package/src/host/chan.js +655 -0
- package/src/host/ckpt-file.js +395 -0
- package/src/host/ckpt-io.js +207 -0
- package/src/host/ckpt-point.js +160 -0
- package/src/host/ckpt-save.js +64 -0
- package/src/host/ckpt.js +434 -0
- package/src/host/clock.js +111 -0
- package/src/host/ctl.js +778 -0
- package/src/host/errno.js +18 -0
- package/src/host/fs.js +167 -0
- package/src/host/gcnudge.js +83 -0
- package/src/host/i64shim.js +81 -0
- package/src/host/jit-cache.js +341 -0
- package/src/host/jit-hub-core.js +208 -0
- package/src/host/jit-hub.js +93 -0
- package/src/host/jit-snap.js +226 -0
- package/src/host/jit.js +1903 -0
- package/src/host/layout.js +100 -0
- package/src/host/monitor.js +110 -0
- package/src/host/p9mount.js +27 -0
- package/src/host/ram-mem.js +125 -0
- package/src/host/stdio.js +143 -0
- package/src/host/threads.js +109 -0
- package/src/host/wasi.js +335 -0
- package/src/host/wmd-imports.js +71 -0
- package/src/host/yamux.js +387 -0
- package/src/image-files.js +154 -0
- package/src/net/dhcp-dns.js +126 -0
- package/src/net/net-worker.js +172 -0
- package/src/net/packet.js +227 -0
- package/src/net/peer.js +230 -0
- package/src/net/proto.js +189 -0
- package/src/net/relay-client.js +226 -0
- package/src/net/stack.js +867 -0
- package/src/net/switch.js +160 -0
- package/src/net/wisp-client.js +570 -0
- package/src/net/wisp-transport.js +196 -0
- package/src/net/wisp.js +511 -0
- package/src/registry/client.js +176 -0
- package/src/registry/converter.js +68 -0
- package/src/registry/hub.js +221 -0
- package/src/registry/oci.js +76 -0
- package/src/registry/pack-worker.js +70 -0
- package/src/registry/pull.js +258 -0
- package/src/registry/ref.js +110 -0
- package/src/registry/wasi-mem.js +355 -0
- package/src/worker.js +315 -0
- package/wmdpack.wasm +0 -0
package/src/base.js
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
// P-B6: base snapshots, the page's side. openBase() starts fetching (or reading from the OPFS cache) an image's base
|
|
2
|
+
// snapshot at once and returns the lazy restore bootWmd takes as `restoreLazy`: the boot downloads the kernel and packs
|
|
3
|
+
// meanwhile, checks the snapshot's header against its own vm.wasm and boot key as soon as it has them (a different one is
|
|
4
|
+
// refused, the download cancelled, and the boot goes on cold with the reason in timings.baseFallback), and restores from the
|
|
5
|
+
// snapshot once it is whole and verified.
|
|
6
|
+
//
|
|
7
|
+
// const lazy = openBase({ manifestUrl: '/fixtures/images/python/base.json', cache: opfsBaseCache(), vmKey: vmKeyOf('/vm.wasm'), log })
|
|
8
|
+
// const vm = await bootWmd({ ...options, checkpoint: true, restoreLazy: lazy, restoreFallback: true })
|
|
9
|
+
// vm.timings.restore ? 'restored from the base snapshot' : vm.timings.baseFallback // {code, message}
|
|
10
|
+
//
|
|
11
|
+
// The manifest (base.json next to the packs): {version: 1, image, entries: [{file, vmKey, bootKey, headerSha256, bytes, codec,
|
|
12
|
+
// createdAt}]}. With `vmKey` the entry is chosen by vm.wasm key (the last entry of that build); a manifest with none for this
|
|
13
|
+
// build is refused at once, without downloading the file. A cache entry is named after the header's SHA-256, so a new
|
|
14
|
+
// snapshot never reads an old copy; the manifest's headerSha256 pins the header, the header pins every chunk and the packs.
|
|
15
|
+
//
|
|
16
|
+
// Lifecycle (what bootWmd does with the lazy object):
|
|
17
|
+
// header resolves when the snapshot's header is in; the reader then WAITS for proceed() (or abort) before it decodes
|
|
18
|
+
// a chunk, so a header the boot refuses (another vm.wasm) costs only what the pump had queued (a few MB)
|
|
19
|
+
// proceed() the vm key matched: the reader may go on
|
|
20
|
+
// ckpt resolves with a box {take(), drop(), stats}: take() hands the snapshot over once
|
|
21
|
+
// accept() the restore SUCCEEDED: the cache copy (if this was a download) is committed
|
|
22
|
+
// abort(reason) the boot does not want it (any exit before accept): the download stops, the box is dropped, the pending
|
|
23
|
+
// cache copy is discarded; an entry read from the cache is deleted when the reason is a refusal of its content
|
|
24
|
+
// reject(err) the restore failed after the snapshot was taken over: abort, and the cache entry is removed whatever its source
|
|
25
|
+
// A download that stops making progress for `stallMs`, or runs longer than `maxMs`, is aborted with code 'stalled'.
|
|
26
|
+
import { opfsBaseCache } from './host/base-cache.js'
|
|
27
|
+
import { readBase } from './host/base-file.js'
|
|
28
|
+
import { CheckpointError, sha256Hex } from './host/ckpt.js'
|
|
29
|
+
|
|
30
|
+
export { opfsBaseCache, memoryBaseCache } from './host/base-cache.js'
|
|
31
|
+
export { readBase, encodeBase } from './host/base-file.js'
|
|
32
|
+
|
|
33
|
+
/** A cache file name: the image with the characters a file name may not have replaced (no '-' in it), '-', the header hash. */
|
|
34
|
+
export const safeImage = image => String(image || 'base').replace(/[^A-Za-z0-9_.]/g, '_')
|
|
35
|
+
const cacheName = (image, entry) => `${safeImage(image)}-${entry.headerSha256.slice(0, 24)}.wmdbase`
|
|
36
|
+
const REFUSALS = ['stale', 'version', 'malformed', 'corrupt', 'foreign', 'incomplete']
|
|
37
|
+
|
|
38
|
+
/** The SHA-256 of a vm.wasm URL's bytes (what bootWmd's vmKey is), for openBase's `vmKey`. */
|
|
39
|
+
export async function vmKeyOf(url, fetchFn = (...a) => fetch(...a)) {
|
|
40
|
+
const r = await fetchFn(url)
|
|
41
|
+
if (!r.ok) throw new Error(`GET ${url}: ${r.status}`)
|
|
42
|
+
return sha256Hex(new Uint8Array(await r.arrayBuffer()))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param {object} o {manifestUrl, image?, cache?, query? (appended to the file's URL: test hooks), log?, fetchFn?, vmKey? (a key or a promise of one),
|
|
47
|
+
* pick? (entries -> entry), stallMs? (20000), maxMs? (600000), gate? (true: the reader waits for proceed() after the header)}
|
|
48
|
+
*/
|
|
49
|
+
export function openBase({ manifestUrl, image, cache = null, query = '', log = () => {}, fetchFn = (...a) => fetch(...a), vmKey = null, pick = es => es.at(-1), stallMs = 20_000, maxMs = 600_000, gate = true } = {}) {
|
|
50
|
+
const ac = new AbortController()
|
|
51
|
+
const info = { source: null, manifestMs: null, cacheWrite: null, attempts: [], netBytes: 0, stalled: null }
|
|
52
|
+
let resolveHeader
|
|
53
|
+
let rejectHeader
|
|
54
|
+
const header = new Promise((res, rej) => ((resolveHeader = res), (rejectHeader = rej)))
|
|
55
|
+
header.catch(() => {})
|
|
56
|
+
let headerSettled = false
|
|
57
|
+
const t0 = performance.now()
|
|
58
|
+
let lastProgress = t0
|
|
59
|
+
let gateWaiting = false
|
|
60
|
+
let openGate
|
|
61
|
+
const gateP = gate ? new Promise(r => (openGate = r)) : Promise.resolve()
|
|
62
|
+
let pending = null // {tee, name, image}: a verified download whose cache copy waits for accept()
|
|
63
|
+
let box = null
|
|
64
|
+
let aborted = false
|
|
65
|
+
let cur = { name: null, source: null }
|
|
66
|
+
let committed = null // the cache file name once accept() wrote it
|
|
67
|
+
const touch = () => (lastProgress = performance.now())
|
|
68
|
+
|
|
69
|
+
const reasonOf = () => {
|
|
70
|
+
const r = ac.signal.reason
|
|
71
|
+
return r instanceof CheckpointError ? r : new CheckpointError('aborted', `the base snapshot was abandoned (${(r && r.message) || 'aborted'})`)
|
|
72
|
+
}
|
|
73
|
+
const fail = err => {
|
|
74
|
+
if (ac.signal.aborted) return
|
|
75
|
+
if (err.code === 'stalled') info.stalled = err.message
|
|
76
|
+
ac.abort(err)
|
|
77
|
+
openGate?.()
|
|
78
|
+
}
|
|
79
|
+
// no bytes for stallMs (while the reader is not waiting for the boot), or more than maxMs in all: refuse, so the boot goes on cold
|
|
80
|
+
const dog = setInterval(() => {
|
|
81
|
+
const now = performance.now()
|
|
82
|
+
if (now - t0 > maxMs) fail(new CheckpointError('stalled', `the base snapshot did not finish in ${Math.round(maxMs / 1000)} s (${info.netBytes} bytes received): giving up on it`))
|
|
83
|
+
else if (!gateWaiting && now - lastProgress > stallMs) fail(new CheckpointError('stalled', `the base snapshot stalled: no bytes for ${Math.round(stallMs / 1000)} s after ${info.netBytes} bytes (the host stopped answering?): giving up on it`))
|
|
84
|
+
}, Math.max(20, Math.min(1000, stallMs / 4)))
|
|
85
|
+
const stopDog = () => clearInterval(dog)
|
|
86
|
+
|
|
87
|
+
async function dropCacheEntry(why) {
|
|
88
|
+
const name = committed || cur.name
|
|
89
|
+
if (!cache || !name) return
|
|
90
|
+
await cache.delete(name).catch(() => {})
|
|
91
|
+
log(`base snapshot: cache entry ${name} removed (${why})`)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function accept() {
|
|
95
|
+
const p = pending
|
|
96
|
+
pending = null
|
|
97
|
+
if (!p || aborted) return
|
|
98
|
+
const tc = performance.now()
|
|
99
|
+
try {
|
|
100
|
+
const w = await p.tee.commit()
|
|
101
|
+
committed = p.name
|
|
102
|
+
info.cacheWrite = { ok: true, bytes: w.bytes, ms: performance.now() - tc }
|
|
103
|
+
pruneOthers(cache, p.image, p.name).catch(() => {})
|
|
104
|
+
} catch (e) {
|
|
105
|
+
p.tee.abort().catch(() => {}) // never leave a half-committed file
|
|
106
|
+
info.cacheWrite = { ok: false, name: e && e.name, error: `${e && e.name}: ${e && e.message}` }
|
|
107
|
+
log(`base snapshot: the cache write did not finish (${e && e.message})`)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function abort(reason) {
|
|
112
|
+
if (aborted) return
|
|
113
|
+
aborted = true
|
|
114
|
+
const err = reason instanceof CheckpointError ? reason : reason ? new CheckpointError('aborted', `the base snapshot was abandoned (${reason.message || reason})`) : new CheckpointError('aborted', 'the base snapshot was abandoned')
|
|
115
|
+
if (!ac.signal.aborted) ac.abort(err)
|
|
116
|
+
openGate?.()
|
|
117
|
+
stopDog()
|
|
118
|
+
const p = pending
|
|
119
|
+
pending = null
|
|
120
|
+
p?.tee.abort().catch(() => {})
|
|
121
|
+
box?.drop()
|
|
122
|
+
// a cached file this page refuses on its content (another vm.wasm, a damaged chunk) is removed, so it is not read again
|
|
123
|
+
if (cur.source === 'opfs' && err.code && REFUSALS.includes(err.code)) dropCacheEntry(err.code)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function reject(err) {
|
|
127
|
+
abort(err)
|
|
128
|
+
await dropCacheEntry(`the restore failed: ${(err && err.message) || err}`)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function run() {
|
|
132
|
+
const tm = performance.now()
|
|
133
|
+
let manifest
|
|
134
|
+
try {
|
|
135
|
+
const r = await fetchFn(manifestUrl, { cache: 'no-store', signal: ac.signal })
|
|
136
|
+
if (!r.ok) throw new CheckpointError('unavailable', `no base snapshot: GET ${manifestUrl} answered ${r.status}`)
|
|
137
|
+
manifest = await r.json()
|
|
138
|
+
} catch (e) {
|
|
139
|
+
if (ac.signal.aborted) throw reasonOf()
|
|
140
|
+
if (e instanceof CheckpointError) throw e
|
|
141
|
+
throw new CheckpointError('unavailable', `no base snapshot: ${manifestUrl} could not be read (${e && e.name}: ${e && e.message})`)
|
|
142
|
+
}
|
|
143
|
+
touch()
|
|
144
|
+
info.manifestMs = performance.now() - tm
|
|
145
|
+
let entries = (manifest.entries || []).filter(e => !image || e.image === image || !e.image)
|
|
146
|
+
if (vmKey) {
|
|
147
|
+
let vk = null
|
|
148
|
+
// the page's vm key may be slow or never settle: the wait is bounded (stallMs), ends at once on an abort, and the watchdog does not
|
|
149
|
+
// count it as a download stall (like the gate)
|
|
150
|
+
gateWaiting = true
|
|
151
|
+
let timer
|
|
152
|
+
try {
|
|
153
|
+
vk = await Promise.race([
|
|
154
|
+
vmKey,
|
|
155
|
+
new Promise((_, rej) => {
|
|
156
|
+
timer = setTimeout(() => rej(new CheckpointError('stalled', `the page's vm.wasm key did not arrive in ${Math.round(stallMs / 1000)} s: giving up on the base snapshot`)), stallMs)
|
|
157
|
+
}),
|
|
158
|
+
new Promise((_, rej) => {
|
|
159
|
+
if (ac.signal.aborted) rej(reasonOf())
|
|
160
|
+
else ac.signal.addEventListener('abort', () => rej(reasonOf()), { once: true })
|
|
161
|
+
}),
|
|
162
|
+
])
|
|
163
|
+
} catch (e) {
|
|
164
|
+
if (e instanceof CheckpointError) throw e
|
|
165
|
+
log(`base snapshot: this page's vm.wasm key is unknown (${e && e.message}); the header decides`)
|
|
166
|
+
} finally {
|
|
167
|
+
clearTimeout(timer)
|
|
168
|
+
gateWaiting = false
|
|
169
|
+
touch()
|
|
170
|
+
}
|
|
171
|
+
if (vk) {
|
|
172
|
+
const mine = entries.filter(e => e.vmKey === vk)
|
|
173
|
+
if (entries.length && !mine.length) {
|
|
174
|
+
throw new CheckpointError('stale', `no base snapshot for this vm.wasm (${vk.slice(0, 12)}): the manifest ${manifestUrl} lists snapshots of vm.wasm ${[...new Set(entries.map(e => String(e.vmKey).slice(0, 12)))].join(', ')} only (its memory image is another emulator's); take a new one`)
|
|
175
|
+
}
|
|
176
|
+
entries = mine
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const entry = entries.length ? pick(entries) : null
|
|
180
|
+
if (!entry || !entry.file || typeof entry.headerSha256 !== 'string') throw new CheckpointError('unavailable', `no base snapshot: the manifest ${manifestUrl} lists none${image ? ` for ${image}` : ''}`)
|
|
181
|
+
info.entry = entry
|
|
182
|
+
const name = cacheName(image || manifest.image, entry)
|
|
183
|
+
const url = new URL(entry.file, new URL(manifestUrl, globalThis.location?.href || 'http://localhost/')).href + query
|
|
184
|
+
const opts = {
|
|
185
|
+
expect: { headerSha256: entry.headerSha256 },
|
|
186
|
+
signal: ac.signal,
|
|
187
|
+
onProgress: touch,
|
|
188
|
+
onHeader: async h => {
|
|
189
|
+
if (!headerSettled) {
|
|
190
|
+
headerSettled = true
|
|
191
|
+
resolveHeader(h)
|
|
192
|
+
}
|
|
193
|
+
if (gate) {
|
|
194
|
+
gateWaiting = true
|
|
195
|
+
await gateP // the boot checks the vm key; a refusal aborts (abort() opens the gate)
|
|
196
|
+
gateWaiting = false
|
|
197
|
+
touch()
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
}
|
|
201
|
+
// attempt 1: the cache (if it holds the file); attempt 2 (or 1): the network. A cached file that fails verification is deleted and the network is tried.
|
|
202
|
+
for (const source of cache ? ['opfs', 'network'] : ['network']) {
|
|
203
|
+
let stream = null
|
|
204
|
+
let tee = null
|
|
205
|
+
const att = { source }
|
|
206
|
+
info.attempts.push(att)
|
|
207
|
+
const ta = performance.now()
|
|
208
|
+
info.netBytes = 0
|
|
209
|
+
cur = { name, source }
|
|
210
|
+
try {
|
|
211
|
+
if (source === 'opfs') {
|
|
212
|
+
const c = await cache.open(name).catch(e => {
|
|
213
|
+
att.cacheError = String(e && e.message)
|
|
214
|
+
return null
|
|
215
|
+
})
|
|
216
|
+
if (!c) {
|
|
217
|
+
att.miss = true
|
|
218
|
+
continue
|
|
219
|
+
}
|
|
220
|
+
stream = c.stream
|
|
221
|
+
att.size = c.size
|
|
222
|
+
} else {
|
|
223
|
+
const r = await fetchFn(url, { signal: ac.signal })
|
|
224
|
+
if (!r.ok || !r.body) throw new CheckpointError('unavailable', `the base snapshot ${url} answered ${r.status}`)
|
|
225
|
+
touch()
|
|
226
|
+
att.headers = r.headers.get('content-length')
|
|
227
|
+
att.contentEncoding = r.headers.get('content-encoding')
|
|
228
|
+
if (cache) {
|
|
229
|
+
try {
|
|
230
|
+
tee = await cache.writer(name)
|
|
231
|
+
} catch (e) {
|
|
232
|
+
info.cacheWrite = { ok: false, name: e && e.name, error: `${e && e.name}: ${e && e.message}` }
|
|
233
|
+
log(`base snapshot: not cached (${e && e.message})`)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
stream = r.body
|
|
237
|
+
}
|
|
238
|
+
const res = await readBase(stream, {
|
|
239
|
+
...opts,
|
|
240
|
+
onBytes: async u8 => {
|
|
241
|
+
info.netBytes += u8.length // live: a page can watch how far the download is
|
|
242
|
+
touch()
|
|
243
|
+
if (!tee) return
|
|
244
|
+
try {
|
|
245
|
+
await tee.write(u8)
|
|
246
|
+
} catch (e) {
|
|
247
|
+
// the cache failed (cleared, full, another tab): the download goes on without it
|
|
248
|
+
info.cacheWrite = { ok: false, name: e && e.name, error: `${e && e.name}: ${e && e.message}` }
|
|
249
|
+
log(`base snapshot: the cache write failed, going on without it (${e && e.message})`)
|
|
250
|
+
tee.abort().catch(() => {})
|
|
251
|
+
tee = null
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
})
|
|
255
|
+
att.ms = performance.now() - ta
|
|
256
|
+
info.source = source
|
|
257
|
+
info.stats = res.stats
|
|
258
|
+
info.totalMs = performance.now() - t0
|
|
259
|
+
stopDog()
|
|
260
|
+
// the cache copy is committed only when the restore has succeeded (accept()): a snapshot this page refuses, or fails to restore, is not kept
|
|
261
|
+
if (tee) {
|
|
262
|
+
if (aborted) {
|
|
263
|
+
tee.abort().catch(() => {})
|
|
264
|
+
throw new CheckpointError('aborted', 'the base snapshot was refused before it finished downloading')
|
|
265
|
+
}
|
|
266
|
+
pending = { tee, name, image: image || manifest.image }
|
|
267
|
+
}
|
|
268
|
+
if (aborted) throw reasonOf()
|
|
269
|
+
let held = res
|
|
270
|
+
box = {
|
|
271
|
+
take() {
|
|
272
|
+
const r = held
|
|
273
|
+
held = null
|
|
274
|
+
return r
|
|
275
|
+
},
|
|
276
|
+
drop() {
|
|
277
|
+
held = null
|
|
278
|
+
},
|
|
279
|
+
stats: res.stats,
|
|
280
|
+
}
|
|
281
|
+
return box
|
|
282
|
+
} catch (e) {
|
|
283
|
+
att.error = `${e && e.name}: ${e && e.message}`
|
|
284
|
+
att.code = e && e.code
|
|
285
|
+
if (tee) tee.abort().catch(() => {}) // never leave a partial file that looks whole (its magic is not in)
|
|
286
|
+
if (ac.signal.aborted) throw reasonOf()
|
|
287
|
+
if (source === 'opfs') {
|
|
288
|
+
log(`base snapshot: the cached copy was not usable (${e && e.message}); deleting it and downloading`)
|
|
289
|
+
await cache.delete(name).catch(() => {})
|
|
290
|
+
continue
|
|
291
|
+
}
|
|
292
|
+
throw e
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
throw new CheckpointError('unavailable', 'no base snapshot could be read')
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const ckpt = run().then(
|
|
299
|
+
r => r,
|
|
300
|
+
e => {
|
|
301
|
+
stopDog()
|
|
302
|
+
if (!headerSettled) {
|
|
303
|
+
headerSettled = true
|
|
304
|
+
rejectHeader(e)
|
|
305
|
+
}
|
|
306
|
+
throw e
|
|
307
|
+
},
|
|
308
|
+
)
|
|
309
|
+
ckpt.catch(() => {})
|
|
310
|
+
return {
|
|
311
|
+
header,
|
|
312
|
+
ckpt,
|
|
313
|
+
info,
|
|
314
|
+
/** The vm key matched: the reader may decode (it waits here after the header). */
|
|
315
|
+
proceed: () => openGate?.(),
|
|
316
|
+
accept,
|
|
317
|
+
abort,
|
|
318
|
+
reject,
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function pruneOthers(cache, image, keep) {
|
|
323
|
+
const prefix = `${safeImage(image)}-`
|
|
324
|
+
for (const f of await cache.list()) if (f.name !== keep && f.name.startsWith(prefix)) await cache.delete(f.name)
|
|
325
|
+
}
|