@instantdb/platform 1.0.64 → 1.0.65
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/.turbo/turbo-build.log +8 -8
- package/__tests__/src/backupDownload.test.ts +218 -0
- package/dist/commonjs/backupDownload.d.ts +27 -0
- package/dist/commonjs/backupDownload.d.ts.map +1 -1
- package/dist/commonjs/backupDownload.js +222 -44
- package/dist/commonjs/backupDownload.js.map +1 -1
- package/dist/esm/backupDownload.d.ts +27 -0
- package/dist/esm/backupDownload.d.ts.map +1 -1
- package/dist/esm/backupDownload.js +222 -44
- package/dist/esm/backupDownload.js.map +1 -1
- package/dist/standalone/index.js +1414 -1301
- package/dist/standalone/index.umd.cjs +23 -23
- package/package.json +4 -4
- package/src/backupDownload.ts +300 -49
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"backupDownload.js","sourceRoot":"","sources":["../../src/backupDownload.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n AppBackup,\n AppBackupStorageFile,\n BackupsManager,\n} from './backups.ts';\n\nexport type BackupDownloadProgress = {\n entitiesCompleted: number;\n entitiesTotal: number | null;\n filesCompleted: number;\n filesTotal: number | null;\n // Compressed bytes written to the sink so far (the zip's on-disk size).\n zipBytes: number;\n // Uncompressed bytes read from source bodies, and the backup's known\n // uncompressed total — the numerator/denominator for a progress bar.\n // bytesTotal is null when the backup row carries no sizes.\n bytesRead: number;\n bytesTotal: number | null;\n // The entry currently being fetched in each phase; empty while that phase\n // isn't actively fetching, so a finished phase stops claiming a file.\n currentEntity: string;\n currentFile: string;\n};\n\nexport type BackupDownloadResult = {\n entities: number;\n files: number;\n zipBytes: number;\n};\n\n/**\n * The archive encoder {@link downloadBackupArchive} writes entries through,\n * supplied by the caller so this package doesn't depend on a zip\n * implementation. zip.js's `ZipWriter` satisfies it structurally, so\n * `new ZipWriter(sink, { zip64: true, signal })` works without an adapter.\n *\n * Implementations must handle archives past 4GB — for zip that means zip64,\n * without which the central-directory offsets wrap and the archive is\n * silently unreadable.\n */\nexport type BackupArchiveWriter = {\n add(\n name: string,\n input: ReadableStream<Uint8Array>,\n opts: { lastModDate: Date },\n ): Promise<unknown>;\n close(): Promise<unknown>;\n};\n\nexport type DownloadBackupArchiveOpts = {\n backup: AppBackup;\n /**\n * Fetches a presigned URL, resolving with the response body and rejecting\n * on a non-200 status. Put the status in the message (e.g. `HTTP 403`) —\n * it's surfaced to the user alongside the failing entry's name. The entity\n * files are served with `Content-Encoding: zstd`; browser fetch decodes\n * that transparently, other runtimes must decompress explicitly.\n */\n fetchBody: (\n url: string,\n signal: AbortSignal,\n ) => Promise<ReadableStream<Uint8Array>>;\n /**\n * Where the archive's bytes go. Closed after the last entry is written;\n * aborted when the download fails or is cancelled, so the caller can\n * discard partial output.\n */\n sink: WritableStream<Uint8Array>;\n /**\n * Builds the archive encoder over a sink that already counts progress and\n * carries the caller's sink's backpressure.\n */\n createWriter: (\n sink: WritableStream<Uint8Array>,\n signal: AbortSignal,\n ) => Promise<BackupArchiveWriter>;\n signal?: AbortSignal;\n onProgress?: (progress: BackupDownloadProgress) => void;\n};\n\nconst isAbortError = (e: unknown): boolean =>\n (e as { name?: string })?.name === 'AbortError';\n\nconst errorMessage = (e: unknown): string =>\n e instanceof Error ? e.message : String(e);\n\n/**\n * Downloads a backup into a single archive written to `opts.sink`: entries\n * in the canonical restore order (`config.json`, then the\n * `entities/<etype>.jsonl` shards, then `files/<locationId>` storage blobs —\n * all entity files before any storage file), with the encoder writing\n * through a counting sink that awaits the caller's sink, so a fast source\n * can't outrun it and balloon memory.\n *\n * The runtime-specific pieces are injected: how to fetch a presigned URL\n * (`fetchBody`), where the bytes go (`sink`), and the archive encoder\n * (`createWriter`). Most callers reach this via\n * {@link BackupsManager.downloadArchive}.\n */\nexport async function downloadBackupArchive(\n opts: DownloadBackupArchiveOpts & {\n manager: Pick<\n BackupsManager,\n 'listFiles' | 'getFileUrl' | 'streamStorageFiles'\n >;\n },\n): Promise<BackupDownloadResult> {\n const { manager, backup, fetchBody, createWriter, onProgress } = opts;\n\n // Internal controller so a pipeline failure also tears down the\n // storage-files discovery stream and any in-flight body fetches.\n const abortController = new AbortController();\n if (opts.signal?.aborted) {\n abortController.abort();\n } else {\n opts.signal?.addEventListener('abort', () => abortController.abort(), {\n once: true,\n });\n }\n const signal = abortController.signal;\n\n let entitiesCompleted = 0;\n let entitiesTotal: number | null = null;\n let filesCompleted = 0;\n let filesTotal: number | null = null;\n let zipBytes = 0;\n let bytesRead = 0;\n let currentEntity = '';\n let currentFile = '';\n const bytesTotal =\n backup.uncompressedSize != null\n ? backup.uncompressedSize + (backup.filesSize ?? 0)\n : null;\n\n const tick = () =>\n onProgress?.({\n entitiesCompleted,\n entitiesTotal,\n filesCompleted,\n filesTotal,\n zipBytes,\n bytesRead,\n bytesTotal,\n currentEntity,\n currentFile,\n });\n\n // Throttle by time: a large backup pushes many small chunks and ticking on\n // every one is wasted work. Phase changes tick() directly so they're still\n // immediate.\n const TICK_INTERVAL_MS = 100;\n let lastTickAt = 0;\n const throttledTick = () => {\n const now = Date.now();\n if (now - lastTickAt >= TICK_INTERVAL_MS) {\n lastTickAt = now;\n tick();\n }\n };\n\n // Count the uncompressed bytes of a source body for progress as it streams\n // into the archive.\n const countBytes = (\n body: ReadableStream<Uint8Array>,\n ): ReadableStream<Uint8Array> =>\n body.pipeThrough(\n new TransformStream<Uint8Array, Uint8Array>({\n transform(chunk, controller) {\n bytesRead += chunk.byteLength;\n throttledTick();\n controller.enqueue(chunk);\n },\n }),\n );\n\n // Storage-files discovery runs concurrently with the entity phase and is\n // drained eagerly into a queue. That isn't just overlap: it closes the\n // NDJSON connection quickly instead of holding it open (and at the mercy of\n // idle timeouts) while multi-GB blobs download. `queueHead` walks the array\n // in place, freeing each slot as it's consumed.\n const queue: (AppBackupStorageFile | undefined)[] = [];\n let queueHead = 0;\n let storageDone = false;\n let storageError: Error | null = null;\n let waitResolve: (() => void) | null = null;\n const notify = () => {\n const w = waitResolve;\n waitResolve = null;\n w?.();\n };\n\n // Never rejects: failures land in storageError for the drain loop to throw.\n const discovery = (async () => {\n let discoveryComplete = false;\n try {\n for await (const file of manager.streamStorageFiles(backup.id, {\n signal,\n })) {\n queue.push(file);\n filesTotal = (filesTotal ?? 0) + 1;\n throttledTick();\n notify();\n }\n discoveryComplete = true;\n } catch (e) {\n // The abort path is expected when the pipeline failed and we tore the\n // discovery down.\n if (!isAbortError(e)) {\n storageError = e as Error;\n }\n } finally {\n // A failed listing keeps the total unknown rather than reading as an\n // empty-but-complete storage phase.\n if (discoveryComplete && filesTotal == null) filesTotal = 0;\n storageDone = true;\n tick();\n notify();\n }\n })();\n\n // Entry write order is significant for restore: config.json first, then the\n // entities/*.jsonl shards, then files/<locationId>. In particular ALL entity\n // files must be written before ANY storage file. listFiles returns the\n // entity files in write order; this generator yields them to completion,\n // then drains the storage queue.\n type ArchiveEntry = {\n name: string;\n input: ReadableStream<Uint8Array>;\n // Fired after the writer finishes consuming the entry, so completion\n // counters reflect fully-written files rather than started fetches.\n onAdded: () => void;\n };\n const entries = (async function* (): AsyncGenerator<ArchiveEntry> {\n const files = await manager.listFiles(backup.id, { signal });\n if (files.length === 0) {\n throw new Error('No files found for this backup.');\n }\n // We write entries in the order the server returns them, and restore\n // requires config.json to be the first entry. Fail loudly rather than\n // build a zip that can't be restored.\n if (files[0].name !== 'config.json') {\n throw new Error(\n `Backup files came back in an unexpected order (expected config.json first, got \"${files[0].name}\").`,\n );\n }\n // config.json isn't a namespace — count only the entities/*.jsonl shards.\n entitiesTotal = files.filter((f) => f.name !== 'config.json').length;\n tick();\n\n for (const f of files) {\n currentEntity = f.name;\n tick();\n const url = await manager.getFileUrl(backup.id, f.name, { signal });\n let body: ReadableStream<Uint8Array>;\n try {\n body = await fetchBody(url, signal);\n } catch (e) {\n if (isAbortError(e)) throw e;\n throw new Error(`Failed to fetch ${f.name}: ${errorMessage(e)}.`);\n }\n yield {\n name: f.name,\n input: countBytes(body),\n onAdded: () => {\n if (f.name !== 'config.json') entitiesCompleted++;\n tick();\n },\n };\n }\n currentEntity = '';\n tick();\n\n while (true) {\n if (storageError) throw storageError;\n // A caller abort while no fetch is in flight surfaces only in the\n // discovery stream, which swallows it as expected teardown — check\n // explicitly so a cancellation can't read as a complete storage phase\n // with files still undiscovered.\n signal.throwIfAborted();\n let file: AppBackupStorageFile | undefined;\n if (queueHead < queue.length) {\n file = queue[queueHead];\n queue[queueHead] = undefined;\n queueHead++;\n }\n if (file) {\n const label = file.path || file.locationId;\n currentFile = label;\n tick();\n let body: ReadableStream<Uint8Array>;\n try {\n body = await fetchBody(file.url, signal);\n } catch (e) {\n if (isAbortError(e)) throw e;\n throw new Error(\n `Couldn't download storage file \"${label}\" (${errorMessage(e)}).`,\n );\n }\n yield {\n name: `files/${file.locationId}`,\n input: countBytes(body),\n onAdded: () => {\n filesCompleted++;\n tick();\n },\n };\n } else if (storageDone) {\n break;\n } else {\n await new Promise<void>((resolve) => {\n waitResolve = resolve;\n });\n }\n }\n currentFile = '';\n tick();\n\n if (storageError) throw storageError;\n })();\n\n const sinkWriter = opts.sink.getWriter();\n try {\n // Sink the archive encoder writes into: it tallies the encoded size for\n // progress, then forwards to the caller's sink. Awaiting the downstream\n // write propagates backpressure up into the encoder, so a fast source\n // can't outrun the sink and balloon memory.\n const countingSink = new WritableStream<Uint8Array>({\n async write(chunk) {\n zipBytes += chunk.byteLength;\n throttledTick();\n await sinkWriter.write(chunk);\n },\n async close() {\n await sinkWriter.close();\n tick();\n },\n async abort(reason) {\n await sinkWriter.abort(reason);\n },\n });\n\n const writer = await createWriter(countingSink, signal);\n for await (const entry of entries) {\n await writer.add(entry.name, entry.input, {\n lastModDate: backup.backupAt,\n });\n entry.onAdded();\n }\n // A caller abort that lands after the last entry lets the generator\n // finish cleanly; don't close and return a complete-looking archive.\n signal.throwIfAborted();\n await writer.close();\n await discovery;\n tick();\n return { entities: entitiesCompleted, files: filesCompleted, zipBytes };\n } catch (e) {\n // Tear down the discovery stream and any in-flight body fetches so we\n // don't keep pulling from S3, and abort the caller's sink so it can\n // discard whatever it wrote.\n abortController.abort();\n await sinkWriter.abort(e).catch(() => {});\n throw e;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"backupDownload.js","sourceRoot":"","sources":["../../src/backupDownload.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n AppBackup,\n AppBackupStorageFile,\n BackupsManager,\n} from './backups.ts';\n\nexport type BackupDownloadProgress = {\n entitiesCompleted: number;\n entitiesTotal: number | null;\n filesCompleted: number;\n filesTotal: number | null;\n // Compressed bytes written to the sink so far (the zip's on-disk size).\n zipBytes: number;\n // Uncompressed bytes read from source bodies, and the backup's known\n // uncompressed total — the numerator/denominator for a progress bar.\n // bytesTotal is null when the backup row carries no sizes.\n bytesRead: number;\n bytesTotal: number | null;\n // The entry currently being fetched in each phase; empty while that phase\n // isn't actively fetching, so a finished phase stops claiming a file.\n currentEntity: string;\n currentFile: string;\n};\n\nexport type BackupDownloadResult = {\n entities: number;\n files: number;\n zipBytes: number;\n};\n\n/**\n * The archive encoder {@link downloadBackupArchive} writes entries through,\n * supplied by the caller so this package doesn't depend on a zip\n * implementation. zip.js's `ZipWriter` satisfies it structurally, so\n * `new ZipWriter(sink, { zip64: true, signal })` works without an adapter.\n *\n * Implementations must handle archives past 4GB — for zip that means zip64,\n * without which the central-directory offsets wrap and the archive is\n * silently unreadable.\n */\nexport type BackupArchiveWriter = {\n add(\n name: string,\n input: ReadableStream<Uint8Array>,\n opts: { lastModDate: Date },\n ): Promise<unknown>;\n close(): Promise<unknown>;\n};\n\nexport type DownloadBackupArchiveOpts = {\n backup: AppBackup;\n /**\n * Fetches a presigned URL, resolving with the response body and rejecting\n * on a non-200 status. Put the status in the message (e.g. `HTTP 403`) —\n * it's surfaced to the user alongside the failing entry's name. The entity\n * files are served with `Content-Encoding: zstd`; browser fetch decodes\n * that transparently, other runtimes must decompress explicitly.\n */\n fetchBody: (\n url: string,\n signal: AbortSignal,\n ) => Promise<ReadableStream<Uint8Array>>;\n /**\n * Where the archive's bytes go. Closed after the last entry is written;\n * aborted when the download fails or is cancelled, so the caller can\n * discard partial output.\n */\n sink: WritableStream<Uint8Array>;\n /**\n * Builds the archive encoder over a sink that already counts progress and\n * carries the caller's sink's backpressure.\n */\n createWriter: (\n sink: WritableStream<Uint8Array>,\n signal: AbortSignal,\n ) => Promise<BackupArchiveWriter>;\n signal?: AbortSignal;\n onProgress?: (progress: BackupDownloadProgress) => void;\n /**\n * How many entries to fetch ahead of the one currently being written into\n * the archive.\n *\n * Only fetch *initiation* is parallelised, the bodies are still written in\n * order and consumed one at a time, so an in-flight prefetched body buffers\n * only to its stream's high-water mark and memory stays bounded. Defaults to\n * {@link DEFAULT_PREFETCH}.\n */\n prefetch?: number;\n /**\n * Retry policy for *opening* an entry's body (fetching its presigned URL and\n * getting a live response). Prefetching keeps body connections open, paused,\n * while earlier entries write, so a queued connection can be reset before we\n * read it.\n * `attempts` is the total number of tries (default {@link DEFAULT_FETCH_ATTEMPTS});\n * `delayMs` is the base for exponential backoff (default {@link DEFAULT_RETRY_DELAY_MS}).\n *\n * This only covers failures before the writer starts consuming the body.\n * Once bytes have been written into the archive entry there's no way to\n * restart it without HTTP range/resume support, so a mid-stream failure\n * still fails the download.\n */\n retry?: { attempts?: number; delayMs?: number };\n};\n\nconst DEFAULT_PREFETCH = 4;\nconst DEFAULT_FETCH_ATTEMPTS = 3;\nconst DEFAULT_RETRY_DELAY_MS = 500;\nconst MAX_RETRY_DELAY_MS = 5000;\n\n// Normalize caller-supplied numeric options so NaN/Infinity/non-integers can't\n// alter retry counts or pipeline bounds — fall back to the default instead.\nconst finitePositiveInt = (v: number | undefined, fallback: number): number =>\n v != null && Number.isInteger(v) && v > 0 ? v : fallback;\nconst finiteNonNegative = (v: number | undefined, fallback: number): number =>\n v != null && Number.isFinite(v) && v >= 0 ? v : fallback;\n\nconst isAbortError = (e: unknown): boolean =>\n (e as { name?: string })?.name === 'AbortError';\n\nconst errorMessage = (e: unknown): string =>\n e instanceof Error ? e.message : String(e);\n\n// A cancellable sleep: resolves after `ms`, or rejects if the signal aborts\n// first so backoff between retries doesn't outlive a cancelled download.\nconst delay = (ms: number, signal: AbortSignal): Promise<void> =>\n new Promise<void>((resolve, reject) => {\n const onAbort = () => {\n clearTimeout(timer);\n reject(signal.reason ?? new DOMException('Aborted', 'AbortError'));\n };\n const timer = setTimeout(() => {\n signal.removeEventListener('abort', onAbort);\n resolve();\n }, ms);\n signal.addEventListener('abort', onAbort, { once: true });\n });\n\n// Re-emits an already-read first chunk, then streams the rest from `reader`.\n// Past that first chunk read errors propagate to the consumer unchanged — by\n// then bytes are in the archive entry and it can't be restarted.\nfunction replayFrom(\n first: ReadableStreamReadResult<Uint8Array>,\n reader: ReadableStreamDefaultReader<Uint8Array>,\n): ReadableStream<Uint8Array> {\n let replayed = false;\n return new ReadableStream<Uint8Array>({\n async pull(controller) {\n if (!replayed) {\n replayed = true;\n if (first.done) {\n controller.close();\n return;\n }\n controller.enqueue(first.value);\n return;\n }\n const { done, value } = await reader.read();\n if (done) controller.close();\n else controller.enqueue(value);\n },\n async cancel(reason) {\n await reader.cancel(reason);\n },\n });\n}\n\n/**\n * Opens a body via `open`, retrying on transient failure with abortable\n * exponential backoff. `open` is re-invoked from scratch each attempt, so an\n * entity file re-mints its presigned URL. An abort propagates immediately; any\n * other final failure is wrapped by `describe` into a user-facing message\n * naming the entry.\n *\n * The first chunk is read inside the retry scope, so a body that connects but\n * fails on its first read — the shape of a prefetched connection reset while it\n * sat idle — is re-fetched too, since nothing has been written to the archive\n * yet. Only failures once bytes are flowing are treated as unrecoverable.\n */\nasync function openWithRetry(\n open: () => Promise<ReadableStream<Uint8Array>>,\n opts: {\n signal: AbortSignal;\n attempts: number;\n delayMs: number;\n describe: (e: unknown) => string;\n },\n): Promise<ReadableStream<Uint8Array>> {\n let lastError: unknown;\n for (let attempt = 1; attempt <= opts.attempts; attempt++) {\n opts.signal.throwIfAborted();\n let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;\n try {\n const body = await open();\n reader = body.getReader();\n const first = await reader.read();\n return replayFrom(first, reader);\n } catch (e) {\n // Release the failed connection before retrying (or giving up).\n if (reader) reader.cancel().catch(() => {});\n if (isAbortError(e)) throw e;\n lastError = e;\n if (attempt < opts.attempts) {\n const backoff = Math.min(\n opts.delayMs * 2 ** (attempt - 1),\n MAX_RETRY_DELAY_MS,\n );\n await delay(backoff, opts.signal);\n }\n }\n }\n throw new Error(opts.describe(lastError));\n}\n\n/**\n * One archive entry whose body fetch has already been started. `onWriting`\n * runs when the encoder begins consuming it (so progress reflects the entry\n * actually streaming, not one prefetched ahead); `onAdded` runs once it's\n * fully written.\n */\ntype PreparedEntry = {\n name: string;\n input: ReadableStream<Uint8Array>;\n onWriting: () => void;\n onAdded: () => void;\n};\n\n/**\n * A thunk that starts fetching one entry (presigned URL + body) and resolves\n * once the body stream is available — not once it's fully downloaded.\n */\ntype EntryThunk = () => Promise<PreparedEntry>;\n\n/**\n * Wraps an ordered stream of entry thunks, keeping up to `lookahead` fetches\n * in flight while yielding the prepared entries in their original order. A\n * background producer pulls thunks and starts their fetches as space frees up;\n * the consumer awaits each in turn. Preserves order and backpressure: at most\n * `lookahead` bodies are ever in flight, and the producer parks when the\n * pipeline is full or the source is waiting for more work.\n */\nasync function* prefetchEntries(\n thunks: AsyncIterable<EntryThunk>,\n lookahead: number,\n): AsyncGenerator<PreparedEntry> {\n const pipeline: Promise<PreparedEntry>[] = [];\n const state: {\n done: boolean;\n producerError: unknown;\n // Woken when the producer pushes an entry (or finishes).\n onItem: (() => void) | null;\n // Woken when the consumer frees a pipeline slot.\n onSpace: (() => void) | null;\n } = { done: false, producerError: null, onItem: null, onSpace: null };\n\n const wakeItem = () => {\n const w = state.onItem;\n state.onItem = null;\n if (w) w();\n };\n const wakeSpace = () => {\n const w = state.onSpace;\n state.onSpace = null;\n if (w) w();\n };\n\n // Never rejects: a failure to produce the next thunk (or start its fetch)\n // lands in state.producerError for the consumer to throw in order.\n const producer = (async () => {\n try {\n for await (const thunk of thunks) {\n while (pipeline.length >= lookahead) {\n await new Promise<void>((resolve) => {\n state.onSpace = resolve;\n });\n }\n const started = thunk();\n // The consumer awaits `started` in order; attach a no-op catch so a\n // fetch that rejects before then isn't reported as unhandled.\n started.catch(() => {});\n pipeline.push(started);\n wakeItem();\n }\n } catch (e) {\n state.producerError = e;\n } finally {\n state.done = true;\n wakeItem();\n }\n })();\n\n try {\n while (true) {\n if (pipeline.length === 0) {\n if (state.done) {\n if (state.producerError) throw state.producerError;\n break;\n }\n await new Promise<void>((resolve) => {\n state.onItem = resolve;\n });\n continue;\n }\n const entry = await pipeline.shift()!;\n wakeSpace();\n yield entry;\n }\n } finally {\n // On early exit (abort/error), let the producer unwind — its own signal\n // teardown resolves the source's waits — without blocking here.\n wakeSpace();\n producer.catch(() => {});\n }\n}\n\n/**\n * Downloads a backup into a single archive written to `opts.sink`: entries\n * in the canonical restore order (`config.json`, then the\n * `entities/<etype>.jsonl` shards, then `files/<locationId>` storage blobs —\n * all entity files before any storage file), with the encoder writing\n * through a counting sink that awaits the caller's sink, so a fast source\n * can't outrun it and balloon memory.\n *\n * The runtime-specific pieces are injected: how to fetch a presigned URL\n * (`fetchBody`), where the bytes go (`sink`), and the archive encoder\n * (`createWriter`). Most callers reach this via\n * {@link BackupsManager.downloadArchive}.\n */\nexport async function downloadBackupArchive(\n opts: DownloadBackupArchiveOpts & {\n manager: Pick<\n BackupsManager,\n 'listFiles' | 'getFileUrl' | 'streamStorageFiles'\n >;\n },\n): Promise<BackupDownloadResult> {\n const { manager, backup, fetchBody, createWriter, onProgress } = opts;\n\n // Internal controller so a pipeline failure also tears down the\n // storage-files discovery stream and any in-flight body fetches.\n const abortController = new AbortController();\n if (opts.signal?.aborted) {\n abortController.abort();\n } else {\n opts.signal?.addEventListener('abort', () => abortController.abort(), {\n once: true,\n });\n }\n const signal = abortController.signal;\n\n let entitiesCompleted = 0;\n let entitiesTotal: number | null = null;\n let filesCompleted = 0;\n let filesTotal: number | null = null;\n let zipBytes = 0;\n let bytesRead = 0;\n let currentEntity = '';\n let currentFile = '';\n const bytesTotal =\n backup.uncompressedSize != null\n ? backup.uncompressedSize + (backup.filesSize ?? 0)\n : null;\n\n const retryAttempts = finitePositiveInt(\n opts.retry?.attempts,\n DEFAULT_FETCH_ATTEMPTS,\n );\n const retryDelayMs = finiteNonNegative(\n opts.retry?.delayMs,\n DEFAULT_RETRY_DELAY_MS,\n );\n\n const tick = () =>\n onProgress?.({\n entitiesCompleted,\n entitiesTotal,\n filesCompleted,\n filesTotal,\n zipBytes,\n bytesRead,\n bytesTotal,\n currentEntity,\n currentFile,\n });\n\n // Throttle by time: a large backup pushes many small chunks and ticking on\n // every one is wasted work. Phase changes tick() directly so they're still\n // immediate.\n const TICK_INTERVAL_MS = 100;\n let lastTickAt = 0;\n const throttledTick = () => {\n const now = Date.now();\n if (now - lastTickAt >= TICK_INTERVAL_MS) {\n lastTickAt = now;\n tick();\n }\n };\n\n // Count the uncompressed bytes of a source body for progress as it streams\n // into the archive.\n const countBytes = (\n body: ReadableStream<Uint8Array>,\n ): ReadableStream<Uint8Array> =>\n body.pipeThrough(\n new TransformStream<Uint8Array, Uint8Array>({\n transform(chunk, controller) {\n bytesRead += chunk.byteLength;\n throttledTick();\n controller.enqueue(chunk);\n },\n }),\n );\n\n // Storage-files discovery runs concurrently with the entity phase and is\n // drained eagerly into a queue. That isn't just overlap: it closes the\n // NDJSON connection quickly instead of holding it open (and at the mercy of\n // idle timeouts) while multi-GB blobs download. `queueHead` walks the array\n // in place, freeing each slot as it's consumed.\n const queue: (AppBackupStorageFile | undefined)[] = [];\n let queueHead = 0;\n let storageDone = false;\n let storageError: Error | null = null;\n let waitResolve: (() => void) | null = null;\n const notify = () => {\n const w = waitResolve;\n waitResolve = null;\n w?.();\n };\n\n // Never rejects: failures land in storageError for the drain loop to throw.\n const discovery = (async () => {\n let discoveryComplete = false;\n try {\n for await (const file of manager.streamStorageFiles(backup.id, {\n signal,\n })) {\n queue.push(file);\n filesTotal = (filesTotal ?? 0) + 1;\n throttledTick();\n notify();\n }\n discoveryComplete = true;\n } catch (e) {\n // The abort path is expected when the pipeline failed and we tore the\n // discovery down.\n if (!isAbortError(e)) {\n storageError = e as Error;\n }\n } finally {\n // A failed listing keeps the total unknown rather than reading as an\n // empty-but-complete storage phase.\n if (discoveryComplete && filesTotal == null) filesTotal = 0;\n storageDone = true;\n tick();\n notify();\n }\n })();\n\n // Entry write order is significant for restore: config.json first, then the\n // entities/*.jsonl shards, then files/<locationId>. In particular ALL entity\n // files must be written before ANY storage file. listFiles returns the\n // entity files in write order; this generator yields thunks for them in\n // order, then drains the storage queue. The thunks are consumed through\n // prefetchEntries, which starts a bounded number of the fetches ahead of the\n // encoder while keeping this order and one-at-a-time writing.\n const thunks = (async function* (): AsyncGenerator<EntryThunk> {\n const files = await manager.listFiles(backup.id, { signal });\n if (files.length === 0) {\n throw new Error('No files found for this backup.');\n }\n // We write entries in the order the server returns them, and restore\n // requires config.json to be the first entry. Fail loudly rather than\n // build a zip that can't be restored.\n if (files[0].name !== 'config.json') {\n throw new Error(\n `Backup files came back in an unexpected order (expected config.json first, got \"${files[0].name}\").`,\n );\n }\n // config.json isn't a namespace — count only the entities/*.jsonl shards.\n entitiesTotal = files.filter((f) => f.name !== 'config.json').length;\n tick();\n\n for (const f of files) {\n yield async () => {\n const body = await openWithRetry(\n async () => {\n const url = await manager.getFileUrl(backup.id, f.name, { signal });\n return fetchBody(url, signal);\n },\n {\n signal,\n attempts: retryAttempts,\n delayMs: retryDelayMs,\n describe: (e) => `Failed to fetch ${f.name}: ${errorMessage(e)}.`,\n },\n );\n return {\n name: f.name,\n input: countBytes(body),\n onWriting: () => {\n currentEntity = f.name;\n currentFile = '';\n tick();\n },\n onAdded: () => {\n if (f.name !== 'config.json') entitiesCompleted++;\n tick();\n },\n };\n };\n }\n\n while (true) {\n if (storageError) throw storageError;\n // A caller abort while no fetch is in flight surfaces only in the\n // discovery stream, which swallows it as expected teardown — check\n // explicitly so a cancellation can't read as a complete storage phase\n // with files still undiscovered.\n signal.throwIfAborted();\n let file: AppBackupStorageFile | undefined;\n if (queueHead < queue.length) {\n file = queue[queueHead];\n queue[queueHead] = undefined;\n queueHead++;\n }\n if (file) {\n const storageFile = file;\n const label = storageFile.path || storageFile.locationId;\n yield async () => {\n const body = await openWithRetry(\n () => fetchBody(storageFile.url, signal),\n {\n signal,\n attempts: retryAttempts,\n delayMs: retryDelayMs,\n describe: (e) =>\n `Couldn't download storage file \"${label}\" (${errorMessage(e)}).`,\n },\n );\n return {\n name: `files/${storageFile.locationId}`,\n input: countBytes(body),\n onWriting: () => {\n currentEntity = '';\n currentFile = label;\n tick();\n },\n onAdded: () => {\n filesCompleted++;\n tick();\n },\n };\n };\n } else if (storageDone) {\n break;\n } else {\n await new Promise<void>((resolve) => {\n waitResolve = resolve;\n });\n }\n }\n\n if (storageError) throw storageError;\n })();\n\n const prefetch = finitePositiveInt(opts.prefetch, DEFAULT_PREFETCH);\n const entries = prefetchEntries(thunks, prefetch);\n\n const sinkWriter = opts.sink.getWriter();\n try {\n // Sink the archive encoder writes into: it tallies the encoded size for\n // progress, then forwards to the caller's sink. Awaiting the downstream\n // write propagates backpressure up into the encoder, so a fast source\n // can't outrun the sink and balloon memory.\n const countingSink = new WritableStream<Uint8Array>({\n async write(chunk) {\n zipBytes += chunk.byteLength;\n throttledTick();\n await sinkWriter.write(chunk);\n },\n async close() {\n await sinkWriter.close();\n tick();\n },\n async abort(reason) {\n await sinkWriter.abort(reason);\n },\n });\n\n const writer = await createWriter(countingSink, signal);\n for await (const entry of entries) {\n entry.onWriting();\n await writer.add(entry.name, entry.input, {\n lastModDate: backup.backupAt,\n });\n entry.onAdded();\n }\n currentEntity = '';\n currentFile = '';\n tick();\n // A caller abort that lands after the last entry lets the generator\n // finish cleanly; don't close and return a complete-looking archive.\n signal.throwIfAborted();\n await writer.close();\n await discovery;\n tick();\n return { entities: entitiesCompleted, files: filesCompleted, zipBytes };\n } catch (e) {\n // Tear down the discovery stream and any in-flight body fetches so we\n // don't keep pulling from S3, and abort the caller's sink so it can\n // discard whatever it wrote.\n abortController.abort();\n await sinkWriter.abort(e).catch(() => {});\n throw e;\n }\n}\n"]}
|
|
@@ -54,6 +54,33 @@ export type DownloadBackupArchiveOpts = {
|
|
|
54
54
|
createWriter: (sink: WritableStream<Uint8Array>, signal: AbortSignal) => Promise<BackupArchiveWriter>;
|
|
55
55
|
signal?: AbortSignal;
|
|
56
56
|
onProgress?: (progress: BackupDownloadProgress) => void;
|
|
57
|
+
/**
|
|
58
|
+
* How many entries to fetch ahead of the one currently being written into
|
|
59
|
+
* the archive.
|
|
60
|
+
*
|
|
61
|
+
* Only fetch *initiation* is parallelised, the bodies are still written in
|
|
62
|
+
* order and consumed one at a time, so an in-flight prefetched body buffers
|
|
63
|
+
* only to its stream's high-water mark and memory stays bounded. Defaults to
|
|
64
|
+
* {@link DEFAULT_PREFETCH}.
|
|
65
|
+
*/
|
|
66
|
+
prefetch?: number;
|
|
67
|
+
/**
|
|
68
|
+
* Retry policy for *opening* an entry's body (fetching its presigned URL and
|
|
69
|
+
* getting a live response). Prefetching keeps body connections open, paused,
|
|
70
|
+
* while earlier entries write, so a queued connection can be reset before we
|
|
71
|
+
* read it.
|
|
72
|
+
* `attempts` is the total number of tries (default {@link DEFAULT_FETCH_ATTEMPTS});
|
|
73
|
+
* `delayMs` is the base for exponential backoff (default {@link DEFAULT_RETRY_DELAY_MS}).
|
|
74
|
+
*
|
|
75
|
+
* This only covers failures before the writer starts consuming the body.
|
|
76
|
+
* Once bytes have been written into the archive entry there's no way to
|
|
77
|
+
* restart it without HTTP range/resume support, so a mid-stream failure
|
|
78
|
+
* still fails the download.
|
|
79
|
+
*/
|
|
80
|
+
retry?: {
|
|
81
|
+
attempts?: number;
|
|
82
|
+
delayMs?: number;
|
|
83
|
+
};
|
|
57
84
|
};
|
|
58
85
|
/**
|
|
59
86
|
* Downloads a backup into a single archive written to `opts.sink`: entries
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"backupDownload.d.ts","sourceRoot":"","sources":["../../src/backupDownload.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,SAAS,EAET,cAAc,EACf,MAAM,cAAc,CAAC;AAEtB,MAAM,MAAM,sBAAsB,GAAG;IACnC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAE1B,QAAQ,EAAE,MAAM,CAAC;IAIjB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAG1B,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF;;;;;;;;;GASG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAChC,GAAG,CACD,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,cAAc,CAAC,UAAU,CAAC,EACjC,IAAI,EAAE;QAAE,WAAW,EAAE,IAAI,CAAA;KAAE,GAC1B,OAAO,CAAC,OAAO,CAAC,CAAC;IACpB,KAAK,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG;IACtC,MAAM,EAAE,SAAS,CAAC;IAClB;;;;;;OAMG;IACH,SAAS,EAAE,CACT,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,WAAW,KAChB,OAAO,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC,CAAC;IACzC;;;;OAIG;IACH,IAAI,EAAE,cAAc,CAAC,UAAU,CAAC,CAAC;IACjC;;;OAGG;IACH,YAAY,EAAE,CACZ,IAAI,EAAE,cAAc,CAAC,UAAU,CAAC,EAChC,MAAM,EAAE,WAAW,KAChB,OAAO,CAAC,mBAAmB,CAAC,CAAC;IAClC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,sBAAsB,KAAK,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"backupDownload.d.ts","sourceRoot":"","sources":["../../src/backupDownload.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,SAAS,EAET,cAAc,EACf,MAAM,cAAc,CAAC;AAEtB,MAAM,MAAM,sBAAsB,GAAG;IACnC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAE1B,QAAQ,EAAE,MAAM,CAAC;IAIjB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAG1B,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF;;;;;;;;;GASG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAChC,GAAG,CACD,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,cAAc,CAAC,UAAU,CAAC,EACjC,IAAI,EAAE;QAAE,WAAW,EAAE,IAAI,CAAA;KAAE,GAC1B,OAAO,CAAC,OAAO,CAAC,CAAC;IACpB,KAAK,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG;IACtC,MAAM,EAAE,SAAS,CAAC;IAClB;;;;;;OAMG;IACH,SAAS,EAAE,CACT,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,WAAW,KAChB,OAAO,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC,CAAC;IACzC;;;;OAIG;IACH,IAAI,EAAE,cAAc,CAAC,UAAU,CAAC,CAAC;IACjC;;;OAGG;IACH,YAAY,EAAE,CACZ,IAAI,EAAE,cAAc,CAAC,UAAU,CAAC,EAChC,MAAM,EAAE,WAAW,KAChB,OAAO,CAAC,mBAAmB,CAAC,CAAC;IAClC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,sBAAsB,KAAK,IAAI,CAAC;IACxD;;;;;;;;OAQG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;;;;;;;;OAYG;IACH,KAAK,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACjD,CAAC;AAoNF;;;;;;;;;;;;GAYG;AACH,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,yBAAyB,GAAG;IAChC,OAAO,EAAE,IAAI,CACX,cAAc,EACd,WAAW,GAAG,YAAY,GAAG,oBAAoB,CAClD,CAAC;CACH,GACA,OAAO,CAAC,oBAAoB,CAAC,CAwR/B"}
|
|
@@ -1,5 +1,165 @@
|
|
|
1
|
+
const DEFAULT_PREFETCH = 4;
|
|
2
|
+
const DEFAULT_FETCH_ATTEMPTS = 3;
|
|
3
|
+
const DEFAULT_RETRY_DELAY_MS = 500;
|
|
4
|
+
const MAX_RETRY_DELAY_MS = 5000;
|
|
5
|
+
// Normalize caller-supplied numeric options so NaN/Infinity/non-integers can't
|
|
6
|
+
// alter retry counts or pipeline bounds — fall back to the default instead.
|
|
7
|
+
const finitePositiveInt = (v, fallback) => v != null && Number.isInteger(v) && v > 0 ? v : fallback;
|
|
8
|
+
const finiteNonNegative = (v, fallback) => v != null && Number.isFinite(v) && v >= 0 ? v : fallback;
|
|
1
9
|
const isAbortError = (e) => e?.name === 'AbortError';
|
|
2
10
|
const errorMessage = (e) => e instanceof Error ? e.message : String(e);
|
|
11
|
+
// A cancellable sleep: resolves after `ms`, or rejects if the signal aborts
|
|
12
|
+
// first so backoff between retries doesn't outlive a cancelled download.
|
|
13
|
+
const delay = (ms, signal) => new Promise((resolve, reject) => {
|
|
14
|
+
const onAbort = () => {
|
|
15
|
+
clearTimeout(timer);
|
|
16
|
+
reject(signal.reason ?? new DOMException('Aborted', 'AbortError'));
|
|
17
|
+
};
|
|
18
|
+
const timer = setTimeout(() => {
|
|
19
|
+
signal.removeEventListener('abort', onAbort);
|
|
20
|
+
resolve();
|
|
21
|
+
}, ms);
|
|
22
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
23
|
+
});
|
|
24
|
+
// Re-emits an already-read first chunk, then streams the rest from `reader`.
|
|
25
|
+
// Past that first chunk read errors propagate to the consumer unchanged — by
|
|
26
|
+
// then bytes are in the archive entry and it can't be restarted.
|
|
27
|
+
function replayFrom(first, reader) {
|
|
28
|
+
let replayed = false;
|
|
29
|
+
return new ReadableStream({
|
|
30
|
+
async pull(controller) {
|
|
31
|
+
if (!replayed) {
|
|
32
|
+
replayed = true;
|
|
33
|
+
if (first.done) {
|
|
34
|
+
controller.close();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
controller.enqueue(first.value);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const { done, value } = await reader.read();
|
|
41
|
+
if (done)
|
|
42
|
+
controller.close();
|
|
43
|
+
else
|
|
44
|
+
controller.enqueue(value);
|
|
45
|
+
},
|
|
46
|
+
async cancel(reason) {
|
|
47
|
+
await reader.cancel(reason);
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Opens a body via `open`, retrying on transient failure with abortable
|
|
53
|
+
* exponential backoff. `open` is re-invoked from scratch each attempt, so an
|
|
54
|
+
* entity file re-mints its presigned URL. An abort propagates immediately; any
|
|
55
|
+
* other final failure is wrapped by `describe` into a user-facing message
|
|
56
|
+
* naming the entry.
|
|
57
|
+
*
|
|
58
|
+
* The first chunk is read inside the retry scope, so a body that connects but
|
|
59
|
+
* fails on its first read — the shape of a prefetched connection reset while it
|
|
60
|
+
* sat idle — is re-fetched too, since nothing has been written to the archive
|
|
61
|
+
* yet. Only failures once bytes are flowing are treated as unrecoverable.
|
|
62
|
+
*/
|
|
63
|
+
async function openWithRetry(open, opts) {
|
|
64
|
+
let lastError;
|
|
65
|
+
for (let attempt = 1; attempt <= opts.attempts; attempt++) {
|
|
66
|
+
opts.signal.throwIfAborted();
|
|
67
|
+
let reader;
|
|
68
|
+
try {
|
|
69
|
+
const body = await open();
|
|
70
|
+
reader = body.getReader();
|
|
71
|
+
const first = await reader.read();
|
|
72
|
+
return replayFrom(first, reader);
|
|
73
|
+
}
|
|
74
|
+
catch (e) {
|
|
75
|
+
// Release the failed connection before retrying (or giving up).
|
|
76
|
+
if (reader)
|
|
77
|
+
reader.cancel().catch(() => { });
|
|
78
|
+
if (isAbortError(e))
|
|
79
|
+
throw e;
|
|
80
|
+
lastError = e;
|
|
81
|
+
if (attempt < opts.attempts) {
|
|
82
|
+
const backoff = Math.min(opts.delayMs * 2 ** (attempt - 1), MAX_RETRY_DELAY_MS);
|
|
83
|
+
await delay(backoff, opts.signal);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
throw new Error(opts.describe(lastError));
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Wraps an ordered stream of entry thunks, keeping up to `lookahead` fetches
|
|
91
|
+
* in flight while yielding the prepared entries in their original order. A
|
|
92
|
+
* background producer pulls thunks and starts their fetches as space frees up;
|
|
93
|
+
* the consumer awaits each in turn. Preserves order and backpressure: at most
|
|
94
|
+
* `lookahead` bodies are ever in flight, and the producer parks when the
|
|
95
|
+
* pipeline is full or the source is waiting for more work.
|
|
96
|
+
*/
|
|
97
|
+
async function* prefetchEntries(thunks, lookahead) {
|
|
98
|
+
const pipeline = [];
|
|
99
|
+
const state = { done: false, producerError: null, onItem: null, onSpace: null };
|
|
100
|
+
const wakeItem = () => {
|
|
101
|
+
const w = state.onItem;
|
|
102
|
+
state.onItem = null;
|
|
103
|
+
if (w)
|
|
104
|
+
w();
|
|
105
|
+
};
|
|
106
|
+
const wakeSpace = () => {
|
|
107
|
+
const w = state.onSpace;
|
|
108
|
+
state.onSpace = null;
|
|
109
|
+
if (w)
|
|
110
|
+
w();
|
|
111
|
+
};
|
|
112
|
+
// Never rejects: a failure to produce the next thunk (or start its fetch)
|
|
113
|
+
// lands in state.producerError for the consumer to throw in order.
|
|
114
|
+
const producer = (async () => {
|
|
115
|
+
try {
|
|
116
|
+
for await (const thunk of thunks) {
|
|
117
|
+
while (pipeline.length >= lookahead) {
|
|
118
|
+
await new Promise((resolve) => {
|
|
119
|
+
state.onSpace = resolve;
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
const started = thunk();
|
|
123
|
+
// The consumer awaits `started` in order; attach a no-op catch so a
|
|
124
|
+
// fetch that rejects before then isn't reported as unhandled.
|
|
125
|
+
started.catch(() => { });
|
|
126
|
+
pipeline.push(started);
|
|
127
|
+
wakeItem();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
catch (e) {
|
|
131
|
+
state.producerError = e;
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
state.done = true;
|
|
135
|
+
wakeItem();
|
|
136
|
+
}
|
|
137
|
+
})();
|
|
138
|
+
try {
|
|
139
|
+
while (true) {
|
|
140
|
+
if (pipeline.length === 0) {
|
|
141
|
+
if (state.done) {
|
|
142
|
+
if (state.producerError)
|
|
143
|
+
throw state.producerError;
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
await new Promise((resolve) => {
|
|
147
|
+
state.onItem = resolve;
|
|
148
|
+
});
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const entry = await pipeline.shift();
|
|
152
|
+
wakeSpace();
|
|
153
|
+
yield entry;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
// On early exit (abort/error), let the producer unwind — its own signal
|
|
158
|
+
// teardown resolves the source's waits — without blocking here.
|
|
159
|
+
wakeSpace();
|
|
160
|
+
producer.catch(() => { });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
3
163
|
/**
|
|
4
164
|
* Downloads a backup into a single archive written to `opts.sink`: entries
|
|
5
165
|
* in the canonical restore order (`config.json`, then the
|
|
@@ -38,6 +198,8 @@ export async function downloadBackupArchive(opts) {
|
|
|
38
198
|
const bytesTotal = backup.uncompressedSize != null
|
|
39
199
|
? backup.uncompressedSize + (backup.filesSize ?? 0)
|
|
40
200
|
: null;
|
|
201
|
+
const retryAttempts = finitePositiveInt(opts.retry?.attempts, DEFAULT_FETCH_ATTEMPTS);
|
|
202
|
+
const retryDelayMs = finiteNonNegative(opts.retry?.delayMs, DEFAULT_RETRY_DELAY_MS);
|
|
41
203
|
const tick = () => onProgress?.({
|
|
42
204
|
entitiesCompleted,
|
|
43
205
|
entitiesTotal,
|
|
@@ -116,7 +278,14 @@ export async function downloadBackupArchive(opts) {
|
|
|
116
278
|
notify();
|
|
117
279
|
}
|
|
118
280
|
})();
|
|
119
|
-
|
|
281
|
+
// Entry write order is significant for restore: config.json first, then the
|
|
282
|
+
// entities/*.jsonl shards, then files/<locationId>. In particular ALL entity
|
|
283
|
+
// files must be written before ANY storage file. listFiles returns the
|
|
284
|
+
// entity files in write order; this generator yields thunks for them in
|
|
285
|
+
// order, then drains the storage queue. The thunks are consumed through
|
|
286
|
+
// prefetchEntries, which starts a bounded number of the fetches ahead of the
|
|
287
|
+
// encoder while keeping this order and one-at-a-time writing.
|
|
288
|
+
const thunks = (async function* () {
|
|
120
289
|
const files = await manager.listFiles(backup.id, { signal });
|
|
121
290
|
if (files.length === 0) {
|
|
122
291
|
throw new Error('No files found for this backup.');
|
|
@@ -131,30 +300,32 @@ export async function downloadBackupArchive(opts) {
|
|
|
131
300
|
entitiesTotal = files.filter((f) => f.name !== 'config.json').length;
|
|
132
301
|
tick();
|
|
133
302
|
for (const f of files) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
303
|
+
yield async () => {
|
|
304
|
+
const body = await openWithRetry(async () => {
|
|
305
|
+
const url = await manager.getFileUrl(backup.id, f.name, { signal });
|
|
306
|
+
return fetchBody(url, signal);
|
|
307
|
+
}, {
|
|
308
|
+
signal,
|
|
309
|
+
attempts: retryAttempts,
|
|
310
|
+
delayMs: retryDelayMs,
|
|
311
|
+
describe: (e) => `Failed to fetch ${f.name}: ${errorMessage(e)}.`,
|
|
312
|
+
});
|
|
313
|
+
return {
|
|
314
|
+
name: f.name,
|
|
315
|
+
input: countBytes(body),
|
|
316
|
+
onWriting: () => {
|
|
317
|
+
currentEntity = f.name;
|
|
318
|
+
currentFile = '';
|
|
319
|
+
tick();
|
|
320
|
+
},
|
|
321
|
+
onAdded: () => {
|
|
322
|
+
if (f.name !== 'config.json')
|
|
323
|
+
entitiesCompleted++;
|
|
324
|
+
tick();
|
|
325
|
+
},
|
|
326
|
+
};
|
|
154
327
|
};
|
|
155
328
|
}
|
|
156
|
-
currentEntity = '';
|
|
157
|
-
tick();
|
|
158
329
|
while (true) {
|
|
159
330
|
if (storageError)
|
|
160
331
|
throw storageError;
|
|
@@ -170,25 +341,28 @@ export async function downloadBackupArchive(opts) {
|
|
|
170
341
|
queueHead++;
|
|
171
342
|
}
|
|
172
343
|
if (file) {
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
344
|
+
const storageFile = file;
|
|
345
|
+
const label = storageFile.path || storageFile.locationId;
|
|
346
|
+
yield async () => {
|
|
347
|
+
const body = await openWithRetry(() => fetchBody(storageFile.url, signal), {
|
|
348
|
+
signal,
|
|
349
|
+
attempts: retryAttempts,
|
|
350
|
+
delayMs: retryDelayMs,
|
|
351
|
+
describe: (e) => `Couldn't download storage file "${label}" (${errorMessage(e)}).`,
|
|
352
|
+
});
|
|
353
|
+
return {
|
|
354
|
+
name: `files/${storageFile.locationId}`,
|
|
355
|
+
input: countBytes(body),
|
|
356
|
+
onWriting: () => {
|
|
357
|
+
currentEntity = '';
|
|
358
|
+
currentFile = label;
|
|
359
|
+
tick();
|
|
360
|
+
},
|
|
361
|
+
onAdded: () => {
|
|
362
|
+
filesCompleted++;
|
|
363
|
+
tick();
|
|
364
|
+
},
|
|
365
|
+
};
|
|
192
366
|
};
|
|
193
367
|
}
|
|
194
368
|
else if (storageDone) {
|
|
@@ -200,11 +374,11 @@ export async function downloadBackupArchive(opts) {
|
|
|
200
374
|
});
|
|
201
375
|
}
|
|
202
376
|
}
|
|
203
|
-
currentFile = '';
|
|
204
|
-
tick();
|
|
205
377
|
if (storageError)
|
|
206
378
|
throw storageError;
|
|
207
379
|
})();
|
|
380
|
+
const prefetch = finitePositiveInt(opts.prefetch, DEFAULT_PREFETCH);
|
|
381
|
+
const entries = prefetchEntries(thunks, prefetch);
|
|
208
382
|
const sinkWriter = opts.sink.getWriter();
|
|
209
383
|
try {
|
|
210
384
|
// Sink the archive encoder writes into: it tallies the encoded size for
|
|
@@ -227,11 +401,15 @@ export async function downloadBackupArchive(opts) {
|
|
|
227
401
|
});
|
|
228
402
|
const writer = await createWriter(countingSink, signal);
|
|
229
403
|
for await (const entry of entries) {
|
|
404
|
+
entry.onWriting();
|
|
230
405
|
await writer.add(entry.name, entry.input, {
|
|
231
406
|
lastModDate: backup.backupAt,
|
|
232
407
|
});
|
|
233
408
|
entry.onAdded();
|
|
234
409
|
}
|
|
410
|
+
currentEntity = '';
|
|
411
|
+
currentFile = '';
|
|
412
|
+
tick();
|
|
235
413
|
// A caller abort that lands after the last entry lets the generator
|
|
236
414
|
// finish cleanly; don't close and return a complete-looking archive.
|
|
237
415
|
signal.throwIfAborted();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"backupDownload.js","sourceRoot":"","sources":["../../src/backupDownload.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n AppBackup,\n AppBackupStorageFile,\n BackupsManager,\n} from './backups.ts';\n\nexport type BackupDownloadProgress = {\n entitiesCompleted: number;\n entitiesTotal: number | null;\n filesCompleted: number;\n filesTotal: number | null;\n // Compressed bytes written to the sink so far (the zip's on-disk size).\n zipBytes: number;\n // Uncompressed bytes read from source bodies, and the backup's known\n // uncompressed total — the numerator/denominator for a progress bar.\n // bytesTotal is null when the backup row carries no sizes.\n bytesRead: number;\n bytesTotal: number | null;\n // The entry currently being fetched in each phase; empty while that phase\n // isn't actively fetching, so a finished phase stops claiming a file.\n currentEntity: string;\n currentFile: string;\n};\n\nexport type BackupDownloadResult = {\n entities: number;\n files: number;\n zipBytes: number;\n};\n\n/**\n * The archive encoder {@link downloadBackupArchive} writes entries through,\n * supplied by the caller so this package doesn't depend on a zip\n * implementation. zip.js's `ZipWriter` satisfies it structurally, so\n * `new ZipWriter(sink, { zip64: true, signal })` works without an adapter.\n *\n * Implementations must handle archives past 4GB — for zip that means zip64,\n * without which the central-directory offsets wrap and the archive is\n * silently unreadable.\n */\nexport type BackupArchiveWriter = {\n add(\n name: string,\n input: ReadableStream<Uint8Array>,\n opts: { lastModDate: Date },\n ): Promise<unknown>;\n close(): Promise<unknown>;\n};\n\nexport type DownloadBackupArchiveOpts = {\n backup: AppBackup;\n /**\n * Fetches a presigned URL, resolving with the response body and rejecting\n * on a non-200 status. Put the status in the message (e.g. `HTTP 403`) —\n * it's surfaced to the user alongside the failing entry's name. The entity\n * files are served with `Content-Encoding: zstd`; browser fetch decodes\n * that transparently, other runtimes must decompress explicitly.\n */\n fetchBody: (\n url: string,\n signal: AbortSignal,\n ) => Promise<ReadableStream<Uint8Array>>;\n /**\n * Where the archive's bytes go. Closed after the last entry is written;\n * aborted when the download fails or is cancelled, so the caller can\n * discard partial output.\n */\n sink: WritableStream<Uint8Array>;\n /**\n * Builds the archive encoder over a sink that already counts progress and\n * carries the caller's sink's backpressure.\n */\n createWriter: (\n sink: WritableStream<Uint8Array>,\n signal: AbortSignal,\n ) => Promise<BackupArchiveWriter>;\n signal?: AbortSignal;\n onProgress?: (progress: BackupDownloadProgress) => void;\n};\n\nconst isAbortError = (e: unknown): boolean =>\n (e as { name?: string })?.name === 'AbortError';\n\nconst errorMessage = (e: unknown): string =>\n e instanceof Error ? e.message : String(e);\n\n/**\n * Downloads a backup into a single archive written to `opts.sink`: entries\n * in the canonical restore order (`config.json`, then the\n * `entities/<etype>.jsonl` shards, then `files/<locationId>` storage blobs —\n * all entity files before any storage file), with the encoder writing\n * through a counting sink that awaits the caller's sink, so a fast source\n * can't outrun it and balloon memory.\n *\n * The runtime-specific pieces are injected: how to fetch a presigned URL\n * (`fetchBody`), where the bytes go (`sink`), and the archive encoder\n * (`createWriter`). Most callers reach this via\n * {@link BackupsManager.downloadArchive}.\n */\nexport async function downloadBackupArchive(\n opts: DownloadBackupArchiveOpts & {\n manager: Pick<\n BackupsManager,\n 'listFiles' | 'getFileUrl' | 'streamStorageFiles'\n >;\n },\n): Promise<BackupDownloadResult> {\n const { manager, backup, fetchBody, createWriter, onProgress } = opts;\n\n // Internal controller so a pipeline failure also tears down the\n // storage-files discovery stream and any in-flight body fetches.\n const abortController = new AbortController();\n if (opts.signal?.aborted) {\n abortController.abort();\n } else {\n opts.signal?.addEventListener('abort', () => abortController.abort(), {\n once: true,\n });\n }\n const signal = abortController.signal;\n\n let entitiesCompleted = 0;\n let entitiesTotal: number | null = null;\n let filesCompleted = 0;\n let filesTotal: number | null = null;\n let zipBytes = 0;\n let bytesRead = 0;\n let currentEntity = '';\n let currentFile = '';\n const bytesTotal =\n backup.uncompressedSize != null\n ? backup.uncompressedSize + (backup.filesSize ?? 0)\n : null;\n\n const tick = () =>\n onProgress?.({\n entitiesCompleted,\n entitiesTotal,\n filesCompleted,\n filesTotal,\n zipBytes,\n bytesRead,\n bytesTotal,\n currentEntity,\n currentFile,\n });\n\n // Throttle by time: a large backup pushes many small chunks and ticking on\n // every one is wasted work. Phase changes tick() directly so they're still\n // immediate.\n const TICK_INTERVAL_MS = 100;\n let lastTickAt = 0;\n const throttledTick = () => {\n const now = Date.now();\n if (now - lastTickAt >= TICK_INTERVAL_MS) {\n lastTickAt = now;\n tick();\n }\n };\n\n // Count the uncompressed bytes of a source body for progress as it streams\n // into the archive.\n const countBytes = (\n body: ReadableStream<Uint8Array>,\n ): ReadableStream<Uint8Array> =>\n body.pipeThrough(\n new TransformStream<Uint8Array, Uint8Array>({\n transform(chunk, controller) {\n bytesRead += chunk.byteLength;\n throttledTick();\n controller.enqueue(chunk);\n },\n }),\n );\n\n // Storage-files discovery runs concurrently with the entity phase and is\n // drained eagerly into a queue. That isn't just overlap: it closes the\n // NDJSON connection quickly instead of holding it open (and at the mercy of\n // idle timeouts) while multi-GB blobs download. `queueHead` walks the array\n // in place, freeing each slot as it's consumed.\n const queue: (AppBackupStorageFile | undefined)[] = [];\n let queueHead = 0;\n let storageDone = false;\n let storageError: Error | null = null;\n let waitResolve: (() => void) | null = null;\n const notify = () => {\n const w = waitResolve;\n waitResolve = null;\n w?.();\n };\n\n // Never rejects: failures land in storageError for the drain loop to throw.\n const discovery = (async () => {\n let discoveryComplete = false;\n try {\n for await (const file of manager.streamStorageFiles(backup.id, {\n signal,\n })) {\n queue.push(file);\n filesTotal = (filesTotal ?? 0) + 1;\n throttledTick();\n notify();\n }\n discoveryComplete = true;\n } catch (e) {\n // The abort path is expected when the pipeline failed and we tore the\n // discovery down.\n if (!isAbortError(e)) {\n storageError = e as Error;\n }\n } finally {\n // A failed listing keeps the total unknown rather than reading as an\n // empty-but-complete storage phase.\n if (discoveryComplete && filesTotal == null) filesTotal = 0;\n storageDone = true;\n tick();\n notify();\n }\n })();\n\n // Entry write order is significant for restore: config.json first, then the\n // entities/*.jsonl shards, then files/<locationId>. In particular ALL entity\n // files must be written before ANY storage file. listFiles returns the\n // entity files in write order; this generator yields them to completion,\n // then drains the storage queue.\n type ArchiveEntry = {\n name: string;\n input: ReadableStream<Uint8Array>;\n // Fired after the writer finishes consuming the entry, so completion\n // counters reflect fully-written files rather than started fetches.\n onAdded: () => void;\n };\n const entries = (async function* (): AsyncGenerator<ArchiveEntry> {\n const files = await manager.listFiles(backup.id, { signal });\n if (files.length === 0) {\n throw new Error('No files found for this backup.');\n }\n // We write entries in the order the server returns them, and restore\n // requires config.json to be the first entry. Fail loudly rather than\n // build a zip that can't be restored.\n if (files[0].name !== 'config.json') {\n throw new Error(\n `Backup files came back in an unexpected order (expected config.json first, got \"${files[0].name}\").`,\n );\n }\n // config.json isn't a namespace — count only the entities/*.jsonl shards.\n entitiesTotal = files.filter((f) => f.name !== 'config.json').length;\n tick();\n\n for (const f of files) {\n currentEntity = f.name;\n tick();\n const url = await manager.getFileUrl(backup.id, f.name, { signal });\n let body: ReadableStream<Uint8Array>;\n try {\n body = await fetchBody(url, signal);\n } catch (e) {\n if (isAbortError(e)) throw e;\n throw new Error(`Failed to fetch ${f.name}: ${errorMessage(e)}.`);\n }\n yield {\n name: f.name,\n input: countBytes(body),\n onAdded: () => {\n if (f.name !== 'config.json') entitiesCompleted++;\n tick();\n },\n };\n }\n currentEntity = '';\n tick();\n\n while (true) {\n if (storageError) throw storageError;\n // A caller abort while no fetch is in flight surfaces only in the\n // discovery stream, which swallows it as expected teardown — check\n // explicitly so a cancellation can't read as a complete storage phase\n // with files still undiscovered.\n signal.throwIfAborted();\n let file: AppBackupStorageFile | undefined;\n if (queueHead < queue.length) {\n file = queue[queueHead];\n queue[queueHead] = undefined;\n queueHead++;\n }\n if (file) {\n const label = file.path || file.locationId;\n currentFile = label;\n tick();\n let body: ReadableStream<Uint8Array>;\n try {\n body = await fetchBody(file.url, signal);\n } catch (e) {\n if (isAbortError(e)) throw e;\n throw new Error(\n `Couldn't download storage file \"${label}\" (${errorMessage(e)}).`,\n );\n }\n yield {\n name: `files/${file.locationId}`,\n input: countBytes(body),\n onAdded: () => {\n filesCompleted++;\n tick();\n },\n };\n } else if (storageDone) {\n break;\n } else {\n await new Promise<void>((resolve) => {\n waitResolve = resolve;\n });\n }\n }\n currentFile = '';\n tick();\n\n if (storageError) throw storageError;\n })();\n\n const sinkWriter = opts.sink.getWriter();\n try {\n // Sink the archive encoder writes into: it tallies the encoded size for\n // progress, then forwards to the caller's sink. Awaiting the downstream\n // write propagates backpressure up into the encoder, so a fast source\n // can't outrun the sink and balloon memory.\n const countingSink = new WritableStream<Uint8Array>({\n async write(chunk) {\n zipBytes += chunk.byteLength;\n throttledTick();\n await sinkWriter.write(chunk);\n },\n async close() {\n await sinkWriter.close();\n tick();\n },\n async abort(reason) {\n await sinkWriter.abort(reason);\n },\n });\n\n const writer = await createWriter(countingSink, signal);\n for await (const entry of entries) {\n await writer.add(entry.name, entry.input, {\n lastModDate: backup.backupAt,\n });\n entry.onAdded();\n }\n // A caller abort that lands after the last entry lets the generator\n // finish cleanly; don't close and return a complete-looking archive.\n signal.throwIfAborted();\n await writer.close();\n await discovery;\n tick();\n return { entities: entitiesCompleted, files: filesCompleted, zipBytes };\n } catch (e) {\n // Tear down the discovery stream and any in-flight body fetches so we\n // don't keep pulling from S3, and abort the caller's sink so it can\n // discard whatever it wrote.\n abortController.abort();\n await sinkWriter.abort(e).catch(() => {});\n throw e;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"backupDownload.js","sourceRoot":"","sources":["../../src/backupDownload.ts"],"names":[],"mappings":"AAwGA,MAAM,gBAAgB,GAAG,CAAC,CAAC;AAC3B,MAAM,sBAAsB,GAAG,CAAC,CAAC;AACjC,MAAM,sBAAsB,GAAG,GAAG,CAAC;AACnC,MAAM,kBAAkB,GAAG,IAAI,CAAC;AAEhC,+EAA+E;AAC/E,4EAA4E;AAC5E,MAAM,iBAAiB,GAAG,CAAC,CAAqB,EAAE,QAAgB,EAAU,EAAE,CAC5E,CAAC,IAAI,IAAI,IAAI,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;AAC3D,MAAM,iBAAiB,GAAG,CAAC,CAAqB,EAAE,QAAgB,EAAU,EAAE,CAC5E,CAAC,IAAI,IAAI,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;AAE3D,MAAM,YAAY,GAAG,CAAC,CAAU,EAAW,EAAE,CAC1C,CAAuB,EAAE,IAAI,KAAK,YAAY,CAAC;AAElD,MAAM,YAAY,GAAG,CAAC,CAAU,EAAU,EAAE,CAC1C,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;AAE7C,4EAA4E;AAC5E,yEAAyE;AACzE,MAAM,KAAK,GAAG,CAAC,EAAU,EAAE,MAAmB,EAAiB,EAAE,CAC/D,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;IACpC,MAAM,OAAO,GAAG,GAAG,EAAE;QACnB,YAAY,CAAC,KAAK,CAAC,CAAC;QACpB,MAAM,CAAC,MAAM,CAAC,MAAM,IAAI,IAAI,YAAY,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC,CAAC;IACrE,CAAC,CAAC;IACF,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;QAC5B,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAC7C,OAAO,EAAE,CAAC;IACZ,CAAC,EAAE,EAAE,CAAC,CAAC;IACP,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;AAC5D,CAAC,CAAC,CAAC;AAEL,6EAA6E;AAC7E,6EAA6E;AAC7E,iEAAiE;AACjE,SAAS,UAAU,CACjB,KAA2C,EAC3C,MAA+C;IAE/C,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,OAAO,IAAI,cAAc,CAAa;QACpC,KAAK,CAAC,IAAI,CAAC,UAAU;YACnB,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACd,QAAQ,GAAG,IAAI,CAAC;gBAChB,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC;oBACf,UAAU,CAAC,KAAK,EAAE,CAAC;oBACnB,OAAO;gBACT,CAAC;gBACD,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;gBAChC,OAAO;YACT,CAAC;YACD,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;YAC5C,IAAI,IAAI;gBAAE,UAAU,CAAC,KAAK,EAAE,CAAC;;gBACxB,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QACjC,CAAC;QACD,KAAK,CAAC,MAAM,CAAC,MAAM;YACjB,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAC9B,CAAC;KACF,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;;;GAWG;AACH,KAAK,UAAU,aAAa,CAC1B,IAA+C,EAC/C,IAKC;IAED,IAAI,SAAkB,CAAC;IACvB,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,IAAI,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,CAAC;QAC1D,IAAI,CAAC,MAAM,CAAC,cAAc,EAAE,CAAC;QAC7B,IAAI,MAA2D,CAAC;QAChE,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,MAAM,IAAI,EAAE,CAAC;YAC1B,MAAM,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;YAC1B,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;YAClC,OAAO,UAAU,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;QACnC,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,gEAAgE;YAChE,IAAI,MAAM;gBAAE,MAAM,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;YAC5C,IAAI,YAAY,CAAC,CAAC,CAAC;gBAAE,MAAM,CAAC,CAAC;YAC7B,SAAS,GAAG,CAAC,CAAC;YACd,IAAI,OAAO,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC;gBAC5B,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CACtB,IAAI,CAAC,OAAO,GAAG,CAAC,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC,EACjC,kBAAkB,CACnB,CAAC;gBACF,MAAM,KAAK,CAAC,OAAO,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;YACpC,CAAC;QACH,CAAC;IACH,CAAC;IACD,MAAM,IAAI,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC;AAC5C,CAAC;AAqBD;;;;;;;GAOG;AACH,KAAK,SAAS,CAAC,CAAC,eAAe,CAC7B,MAAiC,EACjC,SAAiB;IAEjB,MAAM,QAAQ,GAA6B,EAAE,CAAC;IAC9C,MAAM,KAAK,GAOP,EAAE,IAAI,EAAE,KAAK,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAEtE,MAAM,QAAQ,GAAG,GAAG,EAAE;QACpB,MAAM,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC;QACvB,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC;QACpB,IAAI,CAAC;YAAE,CAAC,EAAE,CAAC;IACb,CAAC,CAAC;IACF,MAAM,SAAS,GAAG,GAAG,EAAE;QACrB,MAAM,CAAC,GAAG,KAAK,CAAC,OAAO,CAAC;QACxB,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC;QACrB,IAAI,CAAC;YAAE,CAAC,EAAE,CAAC;IACb,CAAC,CAAC;IAEF,0EAA0E;IAC1E,mEAAmE;IACnE,MAAM,QAAQ,GAAG,CAAC,KAAK,IAAI,EAAE;QAC3B,IAAI,CAAC;YACH,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;gBACjC,OAAO,QAAQ,CAAC,MAAM,IAAI,SAAS,EAAE,CAAC;oBACpC,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;wBAClC,KAAK,CAAC,OAAO,GAAG,OAAO,CAAC;oBAC1B,CAAC,CAAC,CAAC;gBACL,CAAC;gBACD,MAAM,OAAO,GAAG,KAAK,EAAE,CAAC;gBACxB,oEAAoE;gBACpE,8DAA8D;gBAC9D,OAAO,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;gBACxB,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;gBACvB,QAAQ,EAAE,CAAC;YACb,CAAC;QACH,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,KAAK,CAAC,aAAa,GAAG,CAAC,CAAC;QAC1B,CAAC;gBAAS,CAAC;YACT,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC;YAClB,QAAQ,EAAE,CAAC;QACb,CAAC;IACH,CAAC,CAAC,EAAE,CAAC;IAEL,IAAI,CAAC;QACH,OAAO,IAAI,EAAE,CAAC;YACZ,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBAC1B,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC;oBACf,IAAI,KAAK,CAAC,aAAa;wBAAE,MAAM,KAAK,CAAC,aAAa,CAAC;oBACnD,MAAM;gBACR,CAAC;gBACD,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;oBAClC,KAAK,CAAC,MAAM,GAAG,OAAO,CAAC;gBACzB,CAAC,CAAC,CAAC;gBACH,SAAS;YACX,CAAC;YACD,MAAM,KAAK,GAAG,MAAM,QAAQ,CAAC,KAAK,EAAG,CAAC;YACtC,SAAS,EAAE,CAAC;YACZ,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;YAAS,CAAC;QACT,wEAAwE;QACxE,gEAAgE;QAChE,SAAS,EAAE,CAAC;QACZ,QAAQ,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IAC3B,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACzC,IAKC;IAED,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,YAAY,EAAE,UAAU,EAAE,GAAG,IAAI,CAAC;IAEtE,gEAAgE;IAChE,iEAAiE;IACjE,MAAM,eAAe,GAAG,IAAI,eAAe,EAAE,CAAC;IAC9C,IAAI,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC;QACzB,eAAe,CAAC,KAAK,EAAE,CAAC;IAC1B,CAAC;SAAM,CAAC;QACN,IAAI,CAAC,MAAM,EAAE,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,eAAe,CAAC,KAAK,EAAE,EAAE;YACpE,IAAI,EAAE,IAAI;SACX,CAAC,CAAC;IACL,CAAC;IACD,MAAM,MAAM,GAAG,eAAe,CAAC,MAAM,CAAC;IAEtC,IAAI,iBAAiB,GAAG,CAAC,CAAC;IAC1B,IAAI,aAAa,GAAkB,IAAI,CAAC;IACxC,IAAI,cAAc,GAAG,CAAC,CAAC;IACvB,IAAI,UAAU,GAAkB,IAAI,CAAC;IACrC,IAAI,QAAQ,GAAG,CAAC,CAAC;IACjB,IAAI,SAAS,GAAG,CAAC,CAAC;IAClB,IAAI,aAAa,GAAG,EAAE,CAAC;IACvB,IAAI,WAAW,GAAG,EAAE,CAAC;IACrB,MAAM,UAAU,GACd,MAAM,CAAC,gBAAgB,IAAI,IAAI;QAC7B,CAAC,CAAC,MAAM,CAAC,gBAAgB,GAAG,CAAC,MAAM,CAAC,SAAS,IAAI,CAAC,CAAC;QACnD,CAAC,CAAC,IAAI,CAAC;IAEX,MAAM,aAAa,GAAG,iBAAiB,CACrC,IAAI,CAAC,KAAK,EAAE,QAAQ,EACpB,sBAAsB,CACvB,CAAC;IACF,MAAM,YAAY,GAAG,iBAAiB,CACpC,IAAI,CAAC,KAAK,EAAE,OAAO,EACnB,sBAAsB,CACvB,CAAC;IAEF,MAAM,IAAI,GAAG,GAAG,EAAE,CAChB,UAAU,EAAE,CAAC;QACX,iBAAiB;QACjB,aAAa;QACb,cAAc;QACd,UAAU;QACV,QAAQ;QACR,SAAS;QACT,UAAU;QACV,aAAa;QACb,WAAW;KACZ,CAAC,CAAC;IAEL,2EAA2E;IAC3E,2EAA2E;IAC3E,aAAa;IACb,MAAM,gBAAgB,GAAG,GAAG,CAAC;IAC7B,IAAI,UAAU,GAAG,CAAC,CAAC;IACnB,MAAM,aAAa,GAAG,GAAG,EAAE;QACzB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,IAAI,GAAG,GAAG,UAAU,IAAI,gBAAgB,EAAE,CAAC;YACzC,UAAU,GAAG,GAAG,CAAC;YACjB,IAAI,EAAE,CAAC;QACT,CAAC;IACH,CAAC,CAAC;IAEF,2EAA2E;IAC3E,oBAAoB;IACpB,MAAM,UAAU,GAAG,CACjB,IAAgC,EACJ,EAAE,CAC9B,IAAI,CAAC,WAAW,CACd,IAAI,eAAe,CAAyB;QAC1C,SAAS,CAAC,KAAK,EAAE,UAAU;YACzB,SAAS,IAAI,KAAK,CAAC,UAAU,CAAC;YAC9B,aAAa,EAAE,CAAC;YAChB,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QAC5B,CAAC;KACF,CAAC,CACH,CAAC;IAEJ,yEAAyE;IACzE,uEAAuE;IACvE,4EAA4E;IAC5E,4EAA4E;IAC5E,gDAAgD;IAChD,MAAM,KAAK,GAAyC,EAAE,CAAC;IACvD,IAAI,SAAS,GAAG,CAAC,CAAC;IAClB,IAAI,WAAW,GAAG,KAAK,CAAC;IACxB,IAAI,YAAY,GAAiB,IAAI,CAAC;IACtC,IAAI,WAAW,GAAwB,IAAI,CAAC;IAC5C,MAAM,MAAM,GAAG,GAAG,EAAE;QAClB,MAAM,CAAC,GAAG,WAAW,CAAC;QACtB,WAAW,GAAG,IAAI,CAAC;QACnB,CAAC,EAAE,EAAE,CAAC;IACR,CAAC,CAAC;IAEF,4EAA4E;IAC5E,MAAM,SAAS,GAAG,CAAC,KAAK,IAAI,EAAE;QAC5B,IAAI,iBAAiB,GAAG,KAAK,CAAC;QAC9B,IAAI,CAAC;YACH,IAAI,KAAK,EAAE,MAAM,IAAI,IAAI,OAAO,CAAC,kBAAkB,CAAC,MAAM,CAAC,EAAE,EAAE;gBAC7D,MAAM;aACP,CAAC,EAAE,CAAC;gBACH,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBACjB,UAAU,GAAG,CAAC,UAAU,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;gBACnC,aAAa,EAAE,CAAC;gBAChB,MAAM,EAAE,CAAC;YACX,CAAC;YACD,iBAAiB,GAAG,IAAI,CAAC;QAC3B,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,sEAAsE;YACtE,kBAAkB;YAClB,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,CAAC;gBACrB,YAAY,GAAG,CAAU,CAAC;YAC5B,CAAC;QACH,CAAC;gBAAS,CAAC;YACT,qEAAqE;YACrE,oCAAoC;YACpC,IAAI,iBAAiB,IAAI,UAAU,IAAI,IAAI;gBAAE,UAAU,GAAG,CAAC,CAAC;YAC5D,WAAW,GAAG,IAAI,CAAC;YACnB,IAAI,EAAE,CAAC;YACP,MAAM,EAAE,CAAC;QACX,CAAC;IACH,CAAC,CAAC,EAAE,CAAC;IAEL,4EAA4E;IAC5E,6EAA6E;IAC7E,uEAAuE;IACvE,wEAAwE;IACxE,wEAAwE;IACxE,6EAA6E;IAC7E,8DAA8D;IAC9D,MAAM,MAAM,GAAG,CAAC,KAAK,SAAS,CAAC;QAC7B,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;QAC7D,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACvB,MAAM,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAC;QACrD,CAAC;QACD,qEAAqE;QACrE,sEAAsE;QACtE,sCAAsC;QACtC,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,aAAa,EAAE,CAAC;YACpC,MAAM,IAAI,KAAK,CACb,mFAAmF,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,CACtG,CAAC;QACJ,CAAC;QACD,0EAA0E;QAC1E,aAAa,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,aAAa,CAAC,CAAC,MAAM,CAAC;QACrE,IAAI,EAAE,CAAC;QAEP,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;YACtB,MAAM,KAAK,IAAI,EAAE;gBACf,MAAM,IAAI,GAAG,MAAM,aAAa,CAC9B,KAAK,IAAI,EAAE;oBACT,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;oBACpE,OAAO,SAAS,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;gBAChC,CAAC,EACD;oBACE,MAAM;oBACN,QAAQ,EAAE,aAAa;oBACvB,OAAO,EAAE,YAAY;oBACrB,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,mBAAmB,CAAC,CAAC,IAAI,KAAK,YAAY,CAAC,CAAC,CAAC,GAAG;iBAClE,CACF,CAAC;gBACF,OAAO;oBACL,IAAI,EAAE,CAAC,CAAC,IAAI;oBACZ,KAAK,EAAE,UAAU,CAAC,IAAI,CAAC;oBACvB,SAAS,EAAE,GAAG,EAAE;wBACd,aAAa,GAAG,CAAC,CAAC,IAAI,CAAC;wBACvB,WAAW,GAAG,EAAE,CAAC;wBACjB,IAAI,EAAE,CAAC;oBACT,CAAC;oBACD,OAAO,EAAE,GAAG,EAAE;wBACZ,IAAI,CAAC,CAAC,IAAI,KAAK,aAAa;4BAAE,iBAAiB,EAAE,CAAC;wBAClD,IAAI,EAAE,CAAC;oBACT,CAAC;iBACF,CAAC;YACJ,CAAC,CAAC;QACJ,CAAC;QAED,OAAO,IAAI,EAAE,CAAC;YACZ,IAAI,YAAY;gBAAE,MAAM,YAAY,CAAC;YACrC,kEAAkE;YAClE,mEAAmE;YACnE,sEAAsE;YACtE,iCAAiC;YACjC,MAAM,CAAC,cAAc,EAAE,CAAC;YACxB,IAAI,IAAsC,CAAC;YAC3C,IAAI,SAAS,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC;gBAC7B,IAAI,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC;gBACxB,KAAK,CAAC,SAAS,CAAC,GAAG,SAAS,CAAC;gBAC7B,SAAS,EAAE,CAAC;YACd,CAAC;YACD,IAAI,IAAI,EAAE,CAAC;gBACT,MAAM,WAAW,GAAG,IAAI,CAAC;gBACzB,MAAM,KAAK,GAAG,WAAW,CAAC,IAAI,IAAI,WAAW,CAAC,UAAU,CAAC;gBACzD,MAAM,KAAK,IAAI,EAAE;oBACf,MAAM,IAAI,GAAG,MAAM,aAAa,CAC9B,GAAG,EAAE,CAAC,SAAS,CAAC,WAAW,CAAC,GAAG,EAAE,MAAM,CAAC,EACxC;wBACE,MAAM;wBACN,QAAQ,EAAE,aAAa;wBACvB,OAAO,EAAE,YAAY;wBACrB,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CACd,mCAAmC,KAAK,MAAM,YAAY,CAAC,CAAC,CAAC,IAAI;qBACpE,CACF,CAAC;oBACF,OAAO;wBACL,IAAI,EAAE,SAAS,WAAW,CAAC,UAAU,EAAE;wBACvC,KAAK,EAAE,UAAU,CAAC,IAAI,CAAC;wBACvB,SAAS,EAAE,GAAG,EAAE;4BACd,aAAa,GAAG,EAAE,CAAC;4BACnB,WAAW,GAAG,KAAK,CAAC;4BACpB,IAAI,EAAE,CAAC;wBACT,CAAC;wBACD,OAAO,EAAE,GAAG,EAAE;4BACZ,cAAc,EAAE,CAAC;4BACjB,IAAI,EAAE,CAAC;wBACT,CAAC;qBACF,CAAC;gBACJ,CAAC,CAAC;YACJ,CAAC;iBAAM,IAAI,WAAW,EAAE,CAAC;gBACvB,MAAM;YACR,CAAC;iBAAM,CAAC;gBACN,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;oBAClC,WAAW,GAAG,OAAO,CAAC;gBACxB,CAAC,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAED,IAAI,YAAY;YAAE,MAAM,YAAY,CAAC;IACvC,CAAC,CAAC,EAAE,CAAC;IAEL,MAAM,QAAQ,GAAG,iBAAiB,CAAC,IAAI,CAAC,QAAQ,EAAE,gBAAgB,CAAC,CAAC;IACpE,MAAM,OAAO,GAAG,eAAe,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IAElD,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;IACzC,IAAI,CAAC;QACH,wEAAwE;QACxE,wEAAwE;QACxE,sEAAsE;QACtE,4CAA4C;QAC5C,MAAM,YAAY,GAAG,IAAI,cAAc,CAAa;YAClD,KAAK,CAAC,KAAK,CAAC,KAAK;gBACf,QAAQ,IAAI,KAAK,CAAC,UAAU,CAAC;gBAC7B,aAAa,EAAE,CAAC;gBAChB,MAAM,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YAChC,CAAC;YACD,KAAK,CAAC,KAAK;gBACT,MAAM,UAAU,CAAC,KAAK,EAAE,CAAC;gBACzB,IAAI,EAAE,CAAC;YACT,CAAC;YACD,KAAK,CAAC,KAAK,CAAC,MAAM;gBAChB,MAAM,UAAU,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;YACjC,CAAC;SACF,CAAC,CAAC;QAEH,MAAM,MAAM,GAAG,MAAM,YAAY,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;QACxD,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAClC,KAAK,CAAC,SAAS,EAAE,CAAC;YAClB,MAAM,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,KAAK,EAAE;gBACxC,WAAW,EAAE,MAAM,CAAC,QAAQ;aAC7B,CAAC,CAAC;YACH,KAAK,CAAC,OAAO,EAAE,CAAC;QAClB,CAAC;QACD,aAAa,GAAG,EAAE,CAAC;QACnB,WAAW,GAAG,EAAE,CAAC;QACjB,IAAI,EAAE,CAAC;QACP,oEAAoE;QACpE,qEAAqE;QACrE,MAAM,CAAC,cAAc,EAAE,CAAC;QACxB,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;QACrB,MAAM,SAAS,CAAC;QAChB,IAAI,EAAE,CAAC;QACP,OAAO,EAAE,QAAQ,EAAE,iBAAiB,EAAE,KAAK,EAAE,cAAc,EAAE,QAAQ,EAAE,CAAC;IAC1E,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,sEAAsE;QACtE,oEAAoE;QACpE,6BAA6B;QAC7B,eAAe,CAAC,KAAK,EAAE,CAAC;QACxB,MAAM,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QAC1C,MAAM,CAAC,CAAC;IACV,CAAC;AACH,CAAC","sourcesContent":["import type {\n AppBackup,\n AppBackupStorageFile,\n BackupsManager,\n} from './backups.ts';\n\nexport type BackupDownloadProgress = {\n entitiesCompleted: number;\n entitiesTotal: number | null;\n filesCompleted: number;\n filesTotal: number | null;\n // Compressed bytes written to the sink so far (the zip's on-disk size).\n zipBytes: number;\n // Uncompressed bytes read from source bodies, and the backup's known\n // uncompressed total — the numerator/denominator for a progress bar.\n // bytesTotal is null when the backup row carries no sizes.\n bytesRead: number;\n bytesTotal: number | null;\n // The entry currently being fetched in each phase; empty while that phase\n // isn't actively fetching, so a finished phase stops claiming a file.\n currentEntity: string;\n currentFile: string;\n};\n\nexport type BackupDownloadResult = {\n entities: number;\n files: number;\n zipBytes: number;\n};\n\n/**\n * The archive encoder {@link downloadBackupArchive} writes entries through,\n * supplied by the caller so this package doesn't depend on a zip\n * implementation. zip.js's `ZipWriter` satisfies it structurally, so\n * `new ZipWriter(sink, { zip64: true, signal })` works without an adapter.\n *\n * Implementations must handle archives past 4GB — for zip that means zip64,\n * without which the central-directory offsets wrap and the archive is\n * silently unreadable.\n */\nexport type BackupArchiveWriter = {\n add(\n name: string,\n input: ReadableStream<Uint8Array>,\n opts: { lastModDate: Date },\n ): Promise<unknown>;\n close(): Promise<unknown>;\n};\n\nexport type DownloadBackupArchiveOpts = {\n backup: AppBackup;\n /**\n * Fetches a presigned URL, resolving with the response body and rejecting\n * on a non-200 status. Put the status in the message (e.g. `HTTP 403`) —\n * it's surfaced to the user alongside the failing entry's name. The entity\n * files are served with `Content-Encoding: zstd`; browser fetch decodes\n * that transparently, other runtimes must decompress explicitly.\n */\n fetchBody: (\n url: string,\n signal: AbortSignal,\n ) => Promise<ReadableStream<Uint8Array>>;\n /**\n * Where the archive's bytes go. Closed after the last entry is written;\n * aborted when the download fails or is cancelled, so the caller can\n * discard partial output.\n */\n sink: WritableStream<Uint8Array>;\n /**\n * Builds the archive encoder over a sink that already counts progress and\n * carries the caller's sink's backpressure.\n */\n createWriter: (\n sink: WritableStream<Uint8Array>,\n signal: AbortSignal,\n ) => Promise<BackupArchiveWriter>;\n signal?: AbortSignal;\n onProgress?: (progress: BackupDownloadProgress) => void;\n /**\n * How many entries to fetch ahead of the one currently being written into\n * the archive.\n *\n * Only fetch *initiation* is parallelised, the bodies are still written in\n * order and consumed one at a time, so an in-flight prefetched body buffers\n * only to its stream's high-water mark and memory stays bounded. Defaults to\n * {@link DEFAULT_PREFETCH}.\n */\n prefetch?: number;\n /**\n * Retry policy for *opening* an entry's body (fetching its presigned URL and\n * getting a live response). Prefetching keeps body connections open, paused,\n * while earlier entries write, so a queued connection can be reset before we\n * read it.\n * `attempts` is the total number of tries (default {@link DEFAULT_FETCH_ATTEMPTS});\n * `delayMs` is the base for exponential backoff (default {@link DEFAULT_RETRY_DELAY_MS}).\n *\n * This only covers failures before the writer starts consuming the body.\n * Once bytes have been written into the archive entry there's no way to\n * restart it without HTTP range/resume support, so a mid-stream failure\n * still fails the download.\n */\n retry?: { attempts?: number; delayMs?: number };\n};\n\nconst DEFAULT_PREFETCH = 4;\nconst DEFAULT_FETCH_ATTEMPTS = 3;\nconst DEFAULT_RETRY_DELAY_MS = 500;\nconst MAX_RETRY_DELAY_MS = 5000;\n\n// Normalize caller-supplied numeric options so NaN/Infinity/non-integers can't\n// alter retry counts or pipeline bounds — fall back to the default instead.\nconst finitePositiveInt = (v: number | undefined, fallback: number): number =>\n v != null && Number.isInteger(v) && v > 0 ? v : fallback;\nconst finiteNonNegative = (v: number | undefined, fallback: number): number =>\n v != null && Number.isFinite(v) && v >= 0 ? v : fallback;\n\nconst isAbortError = (e: unknown): boolean =>\n (e as { name?: string })?.name === 'AbortError';\n\nconst errorMessage = (e: unknown): string =>\n e instanceof Error ? e.message : String(e);\n\n// A cancellable sleep: resolves after `ms`, or rejects if the signal aborts\n// first so backoff between retries doesn't outlive a cancelled download.\nconst delay = (ms: number, signal: AbortSignal): Promise<void> =>\n new Promise<void>((resolve, reject) => {\n const onAbort = () => {\n clearTimeout(timer);\n reject(signal.reason ?? new DOMException('Aborted', 'AbortError'));\n };\n const timer = setTimeout(() => {\n signal.removeEventListener('abort', onAbort);\n resolve();\n }, ms);\n signal.addEventListener('abort', onAbort, { once: true });\n });\n\n// Re-emits an already-read first chunk, then streams the rest from `reader`.\n// Past that first chunk read errors propagate to the consumer unchanged — by\n// then bytes are in the archive entry and it can't be restarted.\nfunction replayFrom(\n first: ReadableStreamReadResult<Uint8Array>,\n reader: ReadableStreamDefaultReader<Uint8Array>,\n): ReadableStream<Uint8Array> {\n let replayed = false;\n return new ReadableStream<Uint8Array>({\n async pull(controller) {\n if (!replayed) {\n replayed = true;\n if (first.done) {\n controller.close();\n return;\n }\n controller.enqueue(first.value);\n return;\n }\n const { done, value } = await reader.read();\n if (done) controller.close();\n else controller.enqueue(value);\n },\n async cancel(reason) {\n await reader.cancel(reason);\n },\n });\n}\n\n/**\n * Opens a body via `open`, retrying on transient failure with abortable\n * exponential backoff. `open` is re-invoked from scratch each attempt, so an\n * entity file re-mints its presigned URL. An abort propagates immediately; any\n * other final failure is wrapped by `describe` into a user-facing message\n * naming the entry.\n *\n * The first chunk is read inside the retry scope, so a body that connects but\n * fails on its first read — the shape of a prefetched connection reset while it\n * sat idle — is re-fetched too, since nothing has been written to the archive\n * yet. Only failures once bytes are flowing are treated as unrecoverable.\n */\nasync function openWithRetry(\n open: () => Promise<ReadableStream<Uint8Array>>,\n opts: {\n signal: AbortSignal;\n attempts: number;\n delayMs: number;\n describe: (e: unknown) => string;\n },\n): Promise<ReadableStream<Uint8Array>> {\n let lastError: unknown;\n for (let attempt = 1; attempt <= opts.attempts; attempt++) {\n opts.signal.throwIfAborted();\n let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;\n try {\n const body = await open();\n reader = body.getReader();\n const first = await reader.read();\n return replayFrom(first, reader);\n } catch (e) {\n // Release the failed connection before retrying (or giving up).\n if (reader) reader.cancel().catch(() => {});\n if (isAbortError(e)) throw e;\n lastError = e;\n if (attempt < opts.attempts) {\n const backoff = Math.min(\n opts.delayMs * 2 ** (attempt - 1),\n MAX_RETRY_DELAY_MS,\n );\n await delay(backoff, opts.signal);\n }\n }\n }\n throw new Error(opts.describe(lastError));\n}\n\n/**\n * One archive entry whose body fetch has already been started. `onWriting`\n * runs when the encoder begins consuming it (so progress reflects the entry\n * actually streaming, not one prefetched ahead); `onAdded` runs once it's\n * fully written.\n */\ntype PreparedEntry = {\n name: string;\n input: ReadableStream<Uint8Array>;\n onWriting: () => void;\n onAdded: () => void;\n};\n\n/**\n * A thunk that starts fetching one entry (presigned URL + body) and resolves\n * once the body stream is available — not once it's fully downloaded.\n */\ntype EntryThunk = () => Promise<PreparedEntry>;\n\n/**\n * Wraps an ordered stream of entry thunks, keeping up to `lookahead` fetches\n * in flight while yielding the prepared entries in their original order. A\n * background producer pulls thunks and starts their fetches as space frees up;\n * the consumer awaits each in turn. Preserves order and backpressure: at most\n * `lookahead` bodies are ever in flight, and the producer parks when the\n * pipeline is full or the source is waiting for more work.\n */\nasync function* prefetchEntries(\n thunks: AsyncIterable<EntryThunk>,\n lookahead: number,\n): AsyncGenerator<PreparedEntry> {\n const pipeline: Promise<PreparedEntry>[] = [];\n const state: {\n done: boolean;\n producerError: unknown;\n // Woken when the producer pushes an entry (or finishes).\n onItem: (() => void) | null;\n // Woken when the consumer frees a pipeline slot.\n onSpace: (() => void) | null;\n } = { done: false, producerError: null, onItem: null, onSpace: null };\n\n const wakeItem = () => {\n const w = state.onItem;\n state.onItem = null;\n if (w) w();\n };\n const wakeSpace = () => {\n const w = state.onSpace;\n state.onSpace = null;\n if (w) w();\n };\n\n // Never rejects: a failure to produce the next thunk (or start its fetch)\n // lands in state.producerError for the consumer to throw in order.\n const producer = (async () => {\n try {\n for await (const thunk of thunks) {\n while (pipeline.length >= lookahead) {\n await new Promise<void>((resolve) => {\n state.onSpace = resolve;\n });\n }\n const started = thunk();\n // The consumer awaits `started` in order; attach a no-op catch so a\n // fetch that rejects before then isn't reported as unhandled.\n started.catch(() => {});\n pipeline.push(started);\n wakeItem();\n }\n } catch (e) {\n state.producerError = e;\n } finally {\n state.done = true;\n wakeItem();\n }\n })();\n\n try {\n while (true) {\n if (pipeline.length === 0) {\n if (state.done) {\n if (state.producerError) throw state.producerError;\n break;\n }\n await new Promise<void>((resolve) => {\n state.onItem = resolve;\n });\n continue;\n }\n const entry = await pipeline.shift()!;\n wakeSpace();\n yield entry;\n }\n } finally {\n // On early exit (abort/error), let the producer unwind — its own signal\n // teardown resolves the source's waits — without blocking here.\n wakeSpace();\n producer.catch(() => {});\n }\n}\n\n/**\n * Downloads a backup into a single archive written to `opts.sink`: entries\n * in the canonical restore order (`config.json`, then the\n * `entities/<etype>.jsonl` shards, then `files/<locationId>` storage blobs —\n * all entity files before any storage file), with the encoder writing\n * through a counting sink that awaits the caller's sink, so a fast source\n * can't outrun it and balloon memory.\n *\n * The runtime-specific pieces are injected: how to fetch a presigned URL\n * (`fetchBody`), where the bytes go (`sink`), and the archive encoder\n * (`createWriter`). Most callers reach this via\n * {@link BackupsManager.downloadArchive}.\n */\nexport async function downloadBackupArchive(\n opts: DownloadBackupArchiveOpts & {\n manager: Pick<\n BackupsManager,\n 'listFiles' | 'getFileUrl' | 'streamStorageFiles'\n >;\n },\n): Promise<BackupDownloadResult> {\n const { manager, backup, fetchBody, createWriter, onProgress } = opts;\n\n // Internal controller so a pipeline failure also tears down the\n // storage-files discovery stream and any in-flight body fetches.\n const abortController = new AbortController();\n if (opts.signal?.aborted) {\n abortController.abort();\n } else {\n opts.signal?.addEventListener('abort', () => abortController.abort(), {\n once: true,\n });\n }\n const signal = abortController.signal;\n\n let entitiesCompleted = 0;\n let entitiesTotal: number | null = null;\n let filesCompleted = 0;\n let filesTotal: number | null = null;\n let zipBytes = 0;\n let bytesRead = 0;\n let currentEntity = '';\n let currentFile = '';\n const bytesTotal =\n backup.uncompressedSize != null\n ? backup.uncompressedSize + (backup.filesSize ?? 0)\n : null;\n\n const retryAttempts = finitePositiveInt(\n opts.retry?.attempts,\n DEFAULT_FETCH_ATTEMPTS,\n );\n const retryDelayMs = finiteNonNegative(\n opts.retry?.delayMs,\n DEFAULT_RETRY_DELAY_MS,\n );\n\n const tick = () =>\n onProgress?.({\n entitiesCompleted,\n entitiesTotal,\n filesCompleted,\n filesTotal,\n zipBytes,\n bytesRead,\n bytesTotal,\n currentEntity,\n currentFile,\n });\n\n // Throttle by time: a large backup pushes many small chunks and ticking on\n // every one is wasted work. Phase changes tick() directly so they're still\n // immediate.\n const TICK_INTERVAL_MS = 100;\n let lastTickAt = 0;\n const throttledTick = () => {\n const now = Date.now();\n if (now - lastTickAt >= TICK_INTERVAL_MS) {\n lastTickAt = now;\n tick();\n }\n };\n\n // Count the uncompressed bytes of a source body for progress as it streams\n // into the archive.\n const countBytes = (\n body: ReadableStream<Uint8Array>,\n ): ReadableStream<Uint8Array> =>\n body.pipeThrough(\n new TransformStream<Uint8Array, Uint8Array>({\n transform(chunk, controller) {\n bytesRead += chunk.byteLength;\n throttledTick();\n controller.enqueue(chunk);\n },\n }),\n );\n\n // Storage-files discovery runs concurrently with the entity phase and is\n // drained eagerly into a queue. That isn't just overlap: it closes the\n // NDJSON connection quickly instead of holding it open (and at the mercy of\n // idle timeouts) while multi-GB blobs download. `queueHead` walks the array\n // in place, freeing each slot as it's consumed.\n const queue: (AppBackupStorageFile | undefined)[] = [];\n let queueHead = 0;\n let storageDone = false;\n let storageError: Error | null = null;\n let waitResolve: (() => void) | null = null;\n const notify = () => {\n const w = waitResolve;\n waitResolve = null;\n w?.();\n };\n\n // Never rejects: failures land in storageError for the drain loop to throw.\n const discovery = (async () => {\n let discoveryComplete = false;\n try {\n for await (const file of manager.streamStorageFiles(backup.id, {\n signal,\n })) {\n queue.push(file);\n filesTotal = (filesTotal ?? 0) + 1;\n throttledTick();\n notify();\n }\n discoveryComplete = true;\n } catch (e) {\n // The abort path is expected when the pipeline failed and we tore the\n // discovery down.\n if (!isAbortError(e)) {\n storageError = e as Error;\n }\n } finally {\n // A failed listing keeps the total unknown rather than reading as an\n // empty-but-complete storage phase.\n if (discoveryComplete && filesTotal == null) filesTotal = 0;\n storageDone = true;\n tick();\n notify();\n }\n })();\n\n // Entry write order is significant for restore: config.json first, then the\n // entities/*.jsonl shards, then files/<locationId>. In particular ALL entity\n // files must be written before ANY storage file. listFiles returns the\n // entity files in write order; this generator yields thunks for them in\n // order, then drains the storage queue. The thunks are consumed through\n // prefetchEntries, which starts a bounded number of the fetches ahead of the\n // encoder while keeping this order and one-at-a-time writing.\n const thunks = (async function* (): AsyncGenerator<EntryThunk> {\n const files = await manager.listFiles(backup.id, { signal });\n if (files.length === 0) {\n throw new Error('No files found for this backup.');\n }\n // We write entries in the order the server returns them, and restore\n // requires config.json to be the first entry. Fail loudly rather than\n // build a zip that can't be restored.\n if (files[0].name !== 'config.json') {\n throw new Error(\n `Backup files came back in an unexpected order (expected config.json first, got \"${files[0].name}\").`,\n );\n }\n // config.json isn't a namespace — count only the entities/*.jsonl shards.\n entitiesTotal = files.filter((f) => f.name !== 'config.json').length;\n tick();\n\n for (const f of files) {\n yield async () => {\n const body = await openWithRetry(\n async () => {\n const url = await manager.getFileUrl(backup.id, f.name, { signal });\n return fetchBody(url, signal);\n },\n {\n signal,\n attempts: retryAttempts,\n delayMs: retryDelayMs,\n describe: (e) => `Failed to fetch ${f.name}: ${errorMessage(e)}.`,\n },\n );\n return {\n name: f.name,\n input: countBytes(body),\n onWriting: () => {\n currentEntity = f.name;\n currentFile = '';\n tick();\n },\n onAdded: () => {\n if (f.name !== 'config.json') entitiesCompleted++;\n tick();\n },\n };\n };\n }\n\n while (true) {\n if (storageError) throw storageError;\n // A caller abort while no fetch is in flight surfaces only in the\n // discovery stream, which swallows it as expected teardown — check\n // explicitly so a cancellation can't read as a complete storage phase\n // with files still undiscovered.\n signal.throwIfAborted();\n let file: AppBackupStorageFile | undefined;\n if (queueHead < queue.length) {\n file = queue[queueHead];\n queue[queueHead] = undefined;\n queueHead++;\n }\n if (file) {\n const storageFile = file;\n const label = storageFile.path || storageFile.locationId;\n yield async () => {\n const body = await openWithRetry(\n () => fetchBody(storageFile.url, signal),\n {\n signal,\n attempts: retryAttempts,\n delayMs: retryDelayMs,\n describe: (e) =>\n `Couldn't download storage file \"${label}\" (${errorMessage(e)}).`,\n },\n );\n return {\n name: `files/${storageFile.locationId}`,\n input: countBytes(body),\n onWriting: () => {\n currentEntity = '';\n currentFile = label;\n tick();\n },\n onAdded: () => {\n filesCompleted++;\n tick();\n },\n };\n };\n } else if (storageDone) {\n break;\n } else {\n await new Promise<void>((resolve) => {\n waitResolve = resolve;\n });\n }\n }\n\n if (storageError) throw storageError;\n })();\n\n const prefetch = finitePositiveInt(opts.prefetch, DEFAULT_PREFETCH);\n const entries = prefetchEntries(thunks, prefetch);\n\n const sinkWriter = opts.sink.getWriter();\n try {\n // Sink the archive encoder writes into: it tallies the encoded size for\n // progress, then forwards to the caller's sink. Awaiting the downstream\n // write propagates backpressure up into the encoder, so a fast source\n // can't outrun the sink and balloon memory.\n const countingSink = new WritableStream<Uint8Array>({\n async write(chunk) {\n zipBytes += chunk.byteLength;\n throttledTick();\n await sinkWriter.write(chunk);\n },\n async close() {\n await sinkWriter.close();\n tick();\n },\n async abort(reason) {\n await sinkWriter.abort(reason);\n },\n });\n\n const writer = await createWriter(countingSink, signal);\n for await (const entry of entries) {\n entry.onWriting();\n await writer.add(entry.name, entry.input, {\n lastModDate: backup.backupAt,\n });\n entry.onAdded();\n }\n currentEntity = '';\n currentFile = '';\n tick();\n // A caller abort that lands after the last entry lets the generator\n // finish cleanly; don't close and return a complete-looking archive.\n signal.throwIfAborted();\n await writer.close();\n await discovery;\n tick();\n return { entities: entitiesCompleted, files: filesCompleted, zipBytes };\n } catch (e) {\n // Tear down the discovery stream and any in-flight body fetches so we\n // don't keep pulling from S3, and abort the caller's sink so it can\n // discard whatever it wrote.\n abortController.abort();\n await sinkWriter.abort(e).catch(() => {});\n throw e;\n }\n}\n"]}
|