@instantdb/platform 1.0.60 → 1.0.61

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/.turbo/turbo-build.log +9 -9
  2. package/__tests__/src/backupDownload.test.ts +193 -0
  3. package/__tests__/src/backups.test.ts +168 -0
  4. package/dist/commonjs/api.d.ts +7 -0
  5. package/dist/commonjs/api.d.ts.map +1 -1
  6. package/dist/commonjs/api.js +10 -0
  7. package/dist/commonjs/api.js.map +1 -1
  8. package/dist/commonjs/backupDownload.d.ts +74 -0
  9. package/dist/commonjs/backupDownload.d.ts.map +1 -0
  10. package/dist/commonjs/backupDownload.js +255 -0
  11. package/dist/commonjs/backupDownload.js.map +1 -0
  12. package/dist/commonjs/backups.d.ts +146 -0
  13. package/dist/commonjs/backups.d.ts.map +1 -0
  14. package/dist/commonjs/backups.js +248 -0
  15. package/dist/commonjs/backups.js.map +1 -0
  16. package/dist/commonjs/index.d.ts +2 -0
  17. package/dist/commonjs/index.d.ts.map +1 -1
  18. package/dist/commonjs/index.js +7 -1
  19. package/dist/commonjs/index.js.map +1 -1
  20. package/dist/esm/api.d.ts +7 -0
  21. package/dist/esm/api.d.ts.map +1 -1
  22. package/dist/esm/api.js +10 -0
  23. package/dist/esm/api.js.map +1 -1
  24. package/dist/esm/backupDownload.d.ts +74 -0
  25. package/dist/esm/backupDownload.d.ts.map +1 -0
  26. package/dist/esm/backupDownload.js +252 -0
  27. package/dist/esm/backupDownload.js.map +1 -0
  28. package/dist/esm/backups.d.ts +146 -0
  29. package/dist/esm/backups.d.ts.map +1 -0
  30. package/dist/esm/backups.js +237 -0
  31. package/dist/esm/backups.js.map +1 -0
  32. package/dist/esm/index.d.ts +2 -0
  33. package/dist/esm/index.d.ts.map +1 -1
  34. package/dist/esm/index.js +1 -0
  35. package/dist/esm/index.js.map +1 -1
  36. package/dist/standalone/index.js +1929 -1610
  37. package/dist/standalone/index.umd.cjs +24 -22
  38. package/package.json +4 -4
  39. package/src/api.ts +15 -0
  40. package/src/backupDownload.ts +364 -0
  41. package/src/backups.ts +342 -0
  42. package/src/index.ts +18 -0
@@ -0,0 +1,255 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.downloadBackupArchive = downloadBackupArchive;
4
+ const isAbortError = (e) => e?.name === 'AbortError';
5
+ const errorMessage = (e) => e instanceof Error ? e.message : String(e);
6
+ /**
7
+ * Downloads a backup into a single archive written to `opts.sink`: entries
8
+ * in the canonical restore order (`config.json`, then the
9
+ * `entities/<etype>.jsonl` shards, then `files/<locationId>` storage blobs —
10
+ * all entity files before any storage file), with the encoder writing
11
+ * through a counting sink that awaits the caller's sink, so a fast source
12
+ * can't outrun it and balloon memory.
13
+ *
14
+ * The runtime-specific pieces are injected: how to fetch a presigned URL
15
+ * (`fetchBody`), where the bytes go (`sink`), and the archive encoder
16
+ * (`createWriter`). Most callers reach this via
17
+ * {@link BackupsManager.downloadArchive}.
18
+ */
19
+ async function downloadBackupArchive(opts) {
20
+ const { manager, backup, fetchBody, createWriter, onProgress } = opts;
21
+ // Internal controller so a pipeline failure also tears down the
22
+ // storage-files discovery stream and any in-flight body fetches.
23
+ const abortController = new AbortController();
24
+ if (opts.signal?.aborted) {
25
+ abortController.abort();
26
+ }
27
+ else {
28
+ opts.signal?.addEventListener('abort', () => abortController.abort(), {
29
+ once: true,
30
+ });
31
+ }
32
+ const signal = abortController.signal;
33
+ let entitiesCompleted = 0;
34
+ let entitiesTotal = null;
35
+ let filesCompleted = 0;
36
+ let filesTotal = null;
37
+ let zipBytes = 0;
38
+ let bytesRead = 0;
39
+ let currentEntity = '';
40
+ let currentFile = '';
41
+ const bytesTotal = backup.uncompressedSize != null
42
+ ? backup.uncompressedSize + (backup.filesSize ?? 0)
43
+ : null;
44
+ const tick = () => onProgress?.({
45
+ entitiesCompleted,
46
+ entitiesTotal,
47
+ filesCompleted,
48
+ filesTotal,
49
+ zipBytes,
50
+ bytesRead,
51
+ bytesTotal,
52
+ currentEntity,
53
+ currentFile,
54
+ });
55
+ // Throttle by time: a large backup pushes many small chunks and ticking on
56
+ // every one is wasted work. Phase changes tick() directly so they're still
57
+ // immediate.
58
+ const TICK_INTERVAL_MS = 100;
59
+ let lastTickAt = 0;
60
+ const throttledTick = () => {
61
+ const now = Date.now();
62
+ if (now - lastTickAt >= TICK_INTERVAL_MS) {
63
+ lastTickAt = now;
64
+ tick();
65
+ }
66
+ };
67
+ // Count the uncompressed bytes of a source body for progress as it streams
68
+ // into the archive.
69
+ const countBytes = (body) => body.pipeThrough(new TransformStream({
70
+ transform(chunk, controller) {
71
+ bytesRead += chunk.byteLength;
72
+ throttledTick();
73
+ controller.enqueue(chunk);
74
+ },
75
+ }));
76
+ // Storage-files discovery runs concurrently with the entity phase and is
77
+ // drained eagerly into a queue. That isn't just overlap: it closes the
78
+ // NDJSON connection quickly instead of holding it open (and at the mercy of
79
+ // idle timeouts) while multi-GB blobs download. `queueHead` walks the array
80
+ // in place, freeing each slot as it's consumed.
81
+ const queue = [];
82
+ let queueHead = 0;
83
+ let storageDone = false;
84
+ let storageError = null;
85
+ let waitResolve = null;
86
+ const notify = () => {
87
+ const w = waitResolve;
88
+ waitResolve = null;
89
+ w?.();
90
+ };
91
+ // Never rejects: failures land in storageError for the drain loop to throw.
92
+ const discovery = (async () => {
93
+ let discoveryComplete = false;
94
+ try {
95
+ for await (const file of manager.streamStorageFiles(backup.id, {
96
+ signal,
97
+ })) {
98
+ queue.push(file);
99
+ filesTotal = (filesTotal ?? 0) + 1;
100
+ throttledTick();
101
+ notify();
102
+ }
103
+ discoveryComplete = true;
104
+ }
105
+ catch (e) {
106
+ // The abort path is expected when the pipeline failed and we tore the
107
+ // discovery down.
108
+ if (!isAbortError(e)) {
109
+ storageError = e;
110
+ }
111
+ }
112
+ finally {
113
+ // A failed listing keeps the total unknown rather than reading as an
114
+ // empty-but-complete storage phase.
115
+ if (discoveryComplete && filesTotal == null)
116
+ filesTotal = 0;
117
+ storageDone = true;
118
+ tick();
119
+ notify();
120
+ }
121
+ })();
122
+ const entries = (async function* () {
123
+ const files = await manager.listFiles(backup.id, { signal });
124
+ if (files.length === 0) {
125
+ throw new Error('No files found for this backup.');
126
+ }
127
+ // We write entries in the order the server returns them, and restore
128
+ // requires config.json to be the first entry. Fail loudly rather than
129
+ // build a zip that can't be restored.
130
+ if (files[0].name !== 'config.json') {
131
+ throw new Error(`Backup files came back in an unexpected order (expected config.json first, got "${files[0].name}").`);
132
+ }
133
+ // config.json isn't a namespace — count only the entities/*.jsonl shards.
134
+ entitiesTotal = files.filter((f) => f.name !== 'config.json').length;
135
+ tick();
136
+ for (const f of files) {
137
+ currentEntity = f.name;
138
+ tick();
139
+ const url = await manager.getFileUrl(backup.id, f.name, { signal });
140
+ let body;
141
+ try {
142
+ body = await fetchBody(url, signal);
143
+ }
144
+ catch (e) {
145
+ if (isAbortError(e))
146
+ throw e;
147
+ throw new Error(`Failed to fetch ${f.name}: ${errorMessage(e)}.`);
148
+ }
149
+ yield {
150
+ name: f.name,
151
+ input: countBytes(body),
152
+ onAdded: () => {
153
+ if (f.name !== 'config.json')
154
+ entitiesCompleted++;
155
+ tick();
156
+ },
157
+ };
158
+ }
159
+ currentEntity = '';
160
+ tick();
161
+ while (true) {
162
+ if (storageError)
163
+ throw storageError;
164
+ // A caller abort while no fetch is in flight surfaces only in the
165
+ // discovery stream, which swallows it as expected teardown — check
166
+ // explicitly so a cancellation can't read as a complete storage phase
167
+ // with files still undiscovered.
168
+ signal.throwIfAborted();
169
+ let file;
170
+ if (queueHead < queue.length) {
171
+ file = queue[queueHead];
172
+ queue[queueHead] = undefined;
173
+ queueHead++;
174
+ }
175
+ if (file) {
176
+ const label = file.path || file.locationId;
177
+ currentFile = label;
178
+ tick();
179
+ let body;
180
+ try {
181
+ body = await fetchBody(file.url, signal);
182
+ }
183
+ catch (e) {
184
+ if (isAbortError(e))
185
+ throw e;
186
+ throw new Error(`Couldn't download storage file "${label}" (${errorMessage(e)}).`);
187
+ }
188
+ yield {
189
+ name: `files/${file.locationId}`,
190
+ input: countBytes(body),
191
+ onAdded: () => {
192
+ filesCompleted++;
193
+ tick();
194
+ },
195
+ };
196
+ }
197
+ else if (storageDone) {
198
+ break;
199
+ }
200
+ else {
201
+ await new Promise((resolve) => {
202
+ waitResolve = resolve;
203
+ });
204
+ }
205
+ }
206
+ currentFile = '';
207
+ tick();
208
+ if (storageError)
209
+ throw storageError;
210
+ })();
211
+ const sinkWriter = opts.sink.getWriter();
212
+ try {
213
+ // Sink the archive encoder writes into: it tallies the encoded size for
214
+ // progress, then forwards to the caller's sink. Awaiting the downstream
215
+ // write propagates backpressure up into the encoder, so a fast source
216
+ // can't outrun the sink and balloon memory.
217
+ const countingSink = new WritableStream({
218
+ async write(chunk) {
219
+ zipBytes += chunk.byteLength;
220
+ throttledTick();
221
+ await sinkWriter.write(chunk);
222
+ },
223
+ async close() {
224
+ await sinkWriter.close();
225
+ tick();
226
+ },
227
+ async abort(reason) {
228
+ await sinkWriter.abort(reason);
229
+ },
230
+ });
231
+ const writer = await createWriter(countingSink, signal);
232
+ for await (const entry of entries) {
233
+ await writer.add(entry.name, entry.input, {
234
+ lastModDate: backup.backupAt,
235
+ });
236
+ entry.onAdded();
237
+ }
238
+ // A caller abort that lands after the last entry lets the generator
239
+ // finish cleanly; don't close and return a complete-looking archive.
240
+ signal.throwIfAborted();
241
+ await writer.close();
242
+ await discovery;
243
+ tick();
244
+ return { entities: entitiesCompleted, files: filesCompleted, zipBytes };
245
+ }
246
+ catch (e) {
247
+ // Tear down the discovery stream and any in-flight body fetches so we
248
+ // don't keep pulling from S3, and abort the caller's sink so it can
249
+ // discard whatever it wrote.
250
+ abortController.abort();
251
+ await sinkWriter.abort(e).catch(() => { });
252
+ throw e;
253
+ }
254
+ }
255
+ //# sourceMappingURL=backupDownload.js.map
@@ -0,0 +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"]}
@@ -0,0 +1,146 @@
1
+ import type { WithAuth } from '@instantdb/webhooks';
2
+ import { type BackupDownloadResult, type DownloadBackupArchiveOpts } from './backupDownload.ts';
3
+ /** A point-in-time snapshot of an app. */
4
+ export type AppBackup = {
5
+ /** Unique identifier for the backup. */
6
+ id: string;
7
+ /** Instant sequence number the snapshot was taken at. */
8
+ isn: string;
9
+ /** When the snapshot was taken. */
10
+ backupAt: Date;
11
+ /** Total size in bytes of the app's storage files at backup time, if known. */
12
+ filesSize: number | null;
13
+ /** Size in bytes of the app's database at backup time, if known. */
14
+ dbSize: number | null;
15
+ /**
16
+ * Total uncompressed size in bytes of the backup's entity files (what they
17
+ * take up unpacked on disk), if known.
18
+ */
19
+ uncompressedSize: number | null;
20
+ /** Human-readable label, e.g. "Automated Daily Snapshot". */
21
+ description: string | null;
22
+ /** When the backup stops being available for download. */
23
+ expiresAt: Date | null;
24
+ };
25
+ /**
26
+ * A file that makes up the backup payload: `config.json` or an
27
+ * `entities/<etype>.jsonl` shard.
28
+ */
29
+ export type AppBackupFile = {
30
+ name: string;
31
+ /** Size in bytes as stored (the entity shards are stored compressed). */
32
+ size: number;
33
+ };
34
+ /** A storage file captured in a backup, with a presigned download URL. */
35
+ export type AppBackupStorageFile = {
36
+ /**
37
+ * Stable id of the file's blob. A backup archive stores the blob at
38
+ * `files/<locationId>`.
39
+ */
40
+ locationId: string;
41
+ /** The path the user uploaded the file to. */
42
+ path: string | null;
43
+ /** Presigned URL for the file's contents. Expires after 12 hours. */
44
+ url: string;
45
+ };
46
+ type AppBackupResponse = {
47
+ id: string;
48
+ isn: string;
49
+ backup_at: string;
50
+ files_size: number | null;
51
+ db_size: number | null;
52
+ uncompressed_size: number | null;
53
+ description: string | null;
54
+ expires_at: string | null;
55
+ };
56
+ /** Converts a backup row as the server sends it into an {@link AppBackup}. */
57
+ export declare function toAppBackup(row: AppBackupResponse): AppBackup;
58
+ /** Suggested filename for a backup archive. */
59
+ export declare function backupZipName(backup: AppBackup): string;
60
+ /**
61
+ * Formats a byte count the way macOS/Finder reports file sizes: decimal
62
+ * (1000-based) units with SI labels, so the number lines up with what lands
63
+ * on disk.
64
+ */
65
+ export declare function formatFileSize(n: number): string;
66
+ /**
67
+ * Estimated size range for a backup's zip archive, or null when the backup
68
+ * row carries no sizes. Upper bound: everything stored uncompressed (STORE
69
+ * mode and/or files that don't compress). Lower bound: everything compressed
70
+ * at a ~4x DEFLATE ratio, best case for text/JSON, but storage files vary
71
+ * wildly (raw text compresses well, already-compressed images/videos don't).
72
+ * The same divisor applies to both since the file types aren't visible from
73
+ * here; the actual zip lands somewhere inside the range.
74
+ */
75
+ export declare function estimateZipSize(backup: AppBackup): {
76
+ min: number;
77
+ max: number;
78
+ } | null;
79
+ /**
80
+ * Read-only API for an app's backups.
81
+ *
82
+ * A backup archive has a canonical entry order that restore relies on:
83
+ * `config.json` first, then every `entities/<etype>.jsonl` shard, then the
84
+ * `files/<locationId>` storage blobs. In particular ALL entity files must
85
+ * come before ANY storage file, so a restore can process the archive in a
86
+ * single streaming pass: `config.json` sets up the schema, the `$files`
87
+ * entities register file metadata, and only then can each blob be matched
88
+ * to its entity. {@link downloadArchive} implements that order end to end;
89
+ * {@link listFiles} (which returns the entity files already in write order)
90
+ * and {@link streamStorageFiles} are the pieces for building a custom
91
+ * pipeline.
92
+ */
93
+ export declare class BackupsManager {
94
+ #private;
95
+ constructor(opts: {
96
+ appId: string;
97
+ apiURI: string;
98
+ withAuth: WithAuth;
99
+ });
100
+ /**
101
+ * Returns the app's downloadable (non-expired) backups, newest first.
102
+ */
103
+ list(opts?: {
104
+ signal?: AbortSignal;
105
+ }): Promise<AppBackup[]>;
106
+ /**
107
+ * Returns the backup's entity files (`config.json` and the
108
+ * `entities/<etype>.jsonl` shards) in archive write order. Storage blobs
109
+ * are not included; discover those with {@link streamStorageFiles}.
110
+ */
111
+ listFiles(backupId: string, opts?: {
112
+ signal?: AbortSignal;
113
+ }): Promise<AppBackupFile[]>;
114
+ /**
115
+ * Returns a presigned download URL for one of the backup's entity files.
116
+ * The URL expires after 1 hour, so fetch it right before downloading.
117
+ *
118
+ * The entity files are stored zstd-compressed (the response carries
119
+ * `Content-Encoding: zstd`); decompress to get the raw JSON/JSONL.
120
+ */
121
+ getFileUrl(backupId: string, name: string, opts?: {
122
+ signal?: AbortSignal;
123
+ }): Promise<string>;
124
+ /**
125
+ * Streams every storage file captured in the backup, each with a presigned
126
+ * download URL. Completes without yielding anything when the app has no
127
+ * storage files.
128
+ *
129
+ * The server ends a healthy stream with a terminal sentinel; if the stream
130
+ * closes without it (a server-side failure truncated the listing), this
131
+ * throws instead of silently under-reporting files.
132
+ */
133
+ streamStorageFiles(backupId: string, opts?: {
134
+ signal?: AbortSignal;
135
+ }): AsyncGenerator<AppBackupStorageFile, void, void>;
136
+ /**
137
+ * Downloads the backup into a single zip archive written to `opts.sink`,
138
+ * entries in the canonical restore order described above. The caller
139
+ * supplies the runtime-specific pieces — how to fetch a presigned URL,
140
+ * where the bytes go, and the archive encoder (e.g. zip.js's `ZipWriter`);
141
+ * see {@link DownloadBackupArchiveOpts}.
142
+ */
143
+ downloadArchive(opts: DownloadBackupArchiveOpts): Promise<BackupDownloadResult>;
144
+ }
145
+ export {};
146
+ //# sourceMappingURL=backups.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backups.d.ts","sourceRoot":"","sources":["../../src/backups.ts"],"names":[],"mappings":"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"}