@dynlabs/react-native-immutable-file-cache 1.0.0-alpha.1

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 (113) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +415 -0
  3. package/lib/commonjs/adapters/memoryAdapter.js +266 -0
  4. package/lib/commonjs/adapters/memoryAdapter.js.map +1 -0
  5. package/lib/commonjs/adapters/rnfsAdapter.js +259 -0
  6. package/lib/commonjs/adapters/rnfsAdapter.js.map +1 -0
  7. package/lib/commonjs/adapters/webAdapter.js +432 -0
  8. package/lib/commonjs/adapters/webAdapter.js.map +1 -0
  9. package/lib/commonjs/core/adapter.js +2 -0
  10. package/lib/commonjs/core/adapter.js.map +1 -0
  11. package/lib/commonjs/core/cacheEngine.js +578 -0
  12. package/lib/commonjs/core/cacheEngine.js.map +1 -0
  13. package/lib/commonjs/core/errors.js +83 -0
  14. package/lib/commonjs/core/errors.js.map +1 -0
  15. package/lib/commonjs/core/hash.js +83 -0
  16. package/lib/commonjs/core/hash.js.map +1 -0
  17. package/lib/commonjs/core/indexStore.js +175 -0
  18. package/lib/commonjs/core/indexStore.js.map +1 -0
  19. package/lib/commonjs/core/mutex.js +143 -0
  20. package/lib/commonjs/core/mutex.js.map +1 -0
  21. package/lib/commonjs/core/prune.js +127 -0
  22. package/lib/commonjs/core/prune.js.map +1 -0
  23. package/lib/commonjs/core/types.js +6 -0
  24. package/lib/commonjs/core/types.js.map +1 -0
  25. package/lib/commonjs/factory.js +56 -0
  26. package/lib/commonjs/factory.js.map +1 -0
  27. package/lib/commonjs/index.js +110 -0
  28. package/lib/commonjs/index.js.map +1 -0
  29. package/lib/commonjs/index.native.js +74 -0
  30. package/lib/commonjs/index.native.js.map +1 -0
  31. package/lib/commonjs/index.web.js +75 -0
  32. package/lib/commonjs/index.web.js.map +1 -0
  33. package/lib/commonjs/types/react-native-fs.d.js +2 -0
  34. package/lib/commonjs/types/react-native-fs.d.js.map +1 -0
  35. package/lib/module/adapters/memoryAdapter.js +261 -0
  36. package/lib/module/adapters/memoryAdapter.js.map +1 -0
  37. package/lib/module/adapters/rnfsAdapter.js +251 -0
  38. package/lib/module/adapters/rnfsAdapter.js.map +1 -0
  39. package/lib/module/adapters/webAdapter.js +426 -0
  40. package/lib/module/adapters/webAdapter.js.map +1 -0
  41. package/lib/module/core/adapter.js +2 -0
  42. package/lib/module/core/adapter.js.map +1 -0
  43. package/lib/module/core/cacheEngine.js +571 -0
  44. package/lib/module/core/cacheEngine.js.map +1 -0
  45. package/lib/module/core/errors.js +71 -0
  46. package/lib/module/core/errors.js.map +1 -0
  47. package/lib/module/core/hash.js +76 -0
  48. package/lib/module/core/hash.js.map +1 -0
  49. package/lib/module/core/indexStore.js +168 -0
  50. package/lib/module/core/indexStore.js.map +1 -0
  51. package/lib/module/core/mutex.js +135 -0
  52. package/lib/module/core/mutex.js.map +1 -0
  53. package/lib/module/core/prune.js +116 -0
  54. package/lib/module/core/prune.js.map +1 -0
  55. package/lib/module/core/types.js +2 -0
  56. package/lib/module/core/types.js.map +1 -0
  57. package/lib/module/factory.js +49 -0
  58. package/lib/module/factory.js.map +1 -0
  59. package/lib/module/index.js +41 -0
  60. package/lib/module/index.js.map +1 -0
  61. package/lib/module/index.native.js +54 -0
  62. package/lib/module/index.native.js.map +1 -0
  63. package/lib/module/index.web.js +55 -0
  64. package/lib/module/index.web.js.map +1 -0
  65. package/lib/module/types/react-native-fs.d.js +2 -0
  66. package/lib/module/types/react-native-fs.d.js.map +1 -0
  67. package/lib/typescript/src/adapters/memoryAdapter.d.ts +23 -0
  68. package/lib/typescript/src/adapters/memoryAdapter.d.ts.map +1 -0
  69. package/lib/typescript/src/adapters/rnfsAdapter.d.ts +18 -0
  70. package/lib/typescript/src/adapters/rnfsAdapter.d.ts.map +1 -0
  71. package/lib/typescript/src/adapters/webAdapter.d.ts +30 -0
  72. package/lib/typescript/src/adapters/webAdapter.d.ts.map +1 -0
  73. package/lib/typescript/src/core/adapter.d.ts +105 -0
  74. package/lib/typescript/src/core/adapter.d.ts.map +1 -0
  75. package/lib/typescript/src/core/cacheEngine.d.ts +99 -0
  76. package/lib/typescript/src/core/cacheEngine.d.ts.map +1 -0
  77. package/lib/typescript/src/core/errors.d.ts +54 -0
  78. package/lib/typescript/src/core/errors.d.ts.map +1 -0
  79. package/lib/typescript/src/core/hash.d.ts +20 -0
  80. package/lib/typescript/src/core/hash.d.ts.map +1 -0
  81. package/lib/typescript/src/core/indexStore.d.ts +34 -0
  82. package/lib/typescript/src/core/indexStore.d.ts.map +1 -0
  83. package/lib/typescript/src/core/mutex.d.ts +49 -0
  84. package/lib/typescript/src/core/mutex.d.ts.map +1 -0
  85. package/lib/typescript/src/core/prune.d.ts +39 -0
  86. package/lib/typescript/src/core/prune.d.ts.map +1 -0
  87. package/lib/typescript/src/core/types.d.ts +109 -0
  88. package/lib/typescript/src/core/types.d.ts.map +1 -0
  89. package/lib/typescript/src/factory.d.ts +46 -0
  90. package/lib/typescript/src/factory.d.ts.map +1 -0
  91. package/lib/typescript/src/index.d.ts +20 -0
  92. package/lib/typescript/src/index.d.ts.map +1 -0
  93. package/lib/typescript/src/index.native.d.ts +37 -0
  94. package/lib/typescript/src/index.native.d.ts.map +1 -0
  95. package/lib/typescript/src/index.web.d.ts +38 -0
  96. package/lib/typescript/src/index.web.d.ts.map +1 -0
  97. package/package.json +125 -0
  98. package/src/adapters/memoryAdapter.ts +307 -0
  99. package/src/adapters/rnfsAdapter.ts +283 -0
  100. package/src/adapters/webAdapter.ts +480 -0
  101. package/src/core/adapter.ts +128 -0
  102. package/src/core/cacheEngine.ts +634 -0
  103. package/src/core/errors.ts +82 -0
  104. package/src/core/hash.ts +78 -0
  105. package/src/core/indexStore.ts +184 -0
  106. package/src/core/mutex.ts +134 -0
  107. package/src/core/prune.ts +145 -0
  108. package/src/core/types.ts +165 -0
  109. package/src/factory.ts +60 -0
  110. package/src/index.native.ts +58 -0
  111. package/src/index.ts +82 -0
  112. package/src/index.web.ts +59 -0
  113. package/src/types/react-native-fs.d.ts +75 -0
@@ -0,0 +1,480 @@
1
+ /**
2
+ * Web Storage Adapter
3
+ *
4
+ * Implements IStorageAdapter using:
5
+ * - Cache Storage API for binary file storage
6
+ * - IndexedDB for metadata and index storage
7
+ */
8
+
9
+ import type {
10
+ IStorageAdapter,
11
+ TAdapterPath,
12
+ TBinarySource,
13
+ IBinaryWriteResult,
14
+ IBinaryWriteOptions,
15
+ IFileStat,
16
+ } from "../core/adapter";
17
+ import { UnsupportedSourceError, AdapterIOError } from "../core/errors";
18
+
19
+ export interface IWebAdapterOptions {
20
+ /** Cache name for Cache Storage. @default "immutable-file-cache" */
21
+ readonly cacheName?: string;
22
+ /** IDB database name. @default "immutable-file-cache-meta" */
23
+ readonly idbName?: string;
24
+ /** Namespace. */
25
+ readonly namespace?: string;
26
+ }
27
+
28
+ interface IFileMetadata {
29
+ readonly sizeBytes: number;
30
+ readonly mtimeMs: number;
31
+ readonly contentType?: string;
32
+ }
33
+
34
+ const CACHE_URL_PREFIX = "https://cache.local";
35
+
36
+ /**
37
+ * Creates a storage adapter for web platforms using Cache Storage and IndexedDB.
38
+ */
39
+ export function createWebAdapter(options?: IWebAdapterOptions): IStorageAdapter {
40
+ const cacheName = options?.cacheName ?? "immutable-file-cache";
41
+ const idbName = options?.idbName ?? "immutable-file-cache-meta";
42
+ const namespace = options?.namespace ?? "default";
43
+ const rootId = `${cacheName}/${namespace}`;
44
+
45
+ // Track created blob URLs for cleanup
46
+ const blobUrls = new Set<string>();
47
+
48
+ /**
49
+ * Converts adapter path to cache URL key.
50
+ */
51
+ const toCacheKey = (path: TAdapterPath): string => {
52
+ return `${CACHE_URL_PREFIX}/${namespace}/${path}`;
53
+ };
54
+
55
+ /**
56
+ * Converts cache URL key back to adapter path.
57
+ */
58
+ const fromCacheKey = (url: string): TAdapterPath => {
59
+ const prefix = `${CACHE_URL_PREFIX}/${namespace}/`;
60
+ return url.startsWith(prefix) ? url.slice(prefix.length) : url;
61
+ };
62
+
63
+ /**
64
+ * Gets the IDB object store key for metadata.
65
+ */
66
+ const toMetaKey = (path: TAdapterPath): string => {
67
+ return `${namespace}/${path}`;
68
+ };
69
+
70
+ /**
71
+ * Opens or creates the IndexedDB database.
72
+ */
73
+ const openDB = (): Promise<IDBDatabase> => {
74
+ return new Promise((resolve, reject) => {
75
+ const request = indexedDB.open(idbName, 1);
76
+
77
+ request.onerror = () => reject(request.error);
78
+ request.onsuccess = () => resolve(request.result);
79
+
80
+ request.onupgradeneeded = (event) => {
81
+ const db = (event.target as IDBOpenDBRequest).result;
82
+ if (!db.objectStoreNames.contains("metadata")) {
83
+ db.createObjectStore("metadata");
84
+ }
85
+ if (!db.objectStoreNames.contains("directories")) {
86
+ db.createObjectStore("directories");
87
+ }
88
+ };
89
+ });
90
+ };
91
+
92
+ /**
93
+ * Gets metadata from IDB.
94
+ */
95
+ const getMetadata = async (path: TAdapterPath): Promise<IFileMetadata | null> => {
96
+ const db = await openDB();
97
+ return new Promise((resolve, reject) => {
98
+ const tx = db.transaction("metadata", "readonly");
99
+ const store = tx.objectStore("metadata");
100
+ const request = store.get(toMetaKey(path));
101
+
102
+ request.onerror = () => reject(request.error);
103
+ request.onsuccess = () => resolve((request.result as IFileMetadata | undefined) ?? null);
104
+
105
+ tx.oncomplete = () => db.close();
106
+ });
107
+ };
108
+
109
+ /**
110
+ * Sets metadata in IDB.
111
+ */
112
+ const setMetadata = async (path: TAdapterPath, meta: IFileMetadata): Promise<void> => {
113
+ const db = await openDB();
114
+ return new Promise((resolve, reject) => {
115
+ const tx = db.transaction("metadata", "readwrite");
116
+ const store = tx.objectStore("metadata");
117
+ const request = store.put(meta, toMetaKey(path));
118
+
119
+ request.onerror = () => reject(request.error);
120
+ tx.oncomplete = () => {
121
+ db.close();
122
+ resolve();
123
+ };
124
+ });
125
+ };
126
+
127
+ /**
128
+ * Deletes metadata from IDB.
129
+ */
130
+ const deleteMetadata = async (path: TAdapterPath): Promise<void> => {
131
+ const db = await openDB();
132
+ return new Promise((resolve, reject) => {
133
+ const tx = db.transaction("metadata", "readwrite");
134
+ const store = tx.objectStore("metadata");
135
+ const request = store.delete(toMetaKey(path));
136
+
137
+ request.onerror = () => reject(request.error);
138
+ tx.oncomplete = () => {
139
+ db.close();
140
+ resolve();
141
+ };
142
+ });
143
+ };
144
+
145
+ /**
146
+ * Marks a directory as existing in IDB.
147
+ */
148
+ const markDirectory = async (path: TAdapterPath): Promise<void> => {
149
+ const db = await openDB();
150
+ return new Promise((resolve, reject) => {
151
+ const tx = db.transaction("directories", "readwrite");
152
+ const store = tx.objectStore("directories");
153
+ const request = store.put(true, toMetaKey(path));
154
+
155
+ request.onerror = () => reject(request.error);
156
+ tx.oncomplete = () => {
157
+ db.close();
158
+ resolve();
159
+ };
160
+ });
161
+ };
162
+
163
+ /**
164
+ * Checks if directory exists in IDB.
165
+ */
166
+ const hasDirectory = async (path: TAdapterPath): Promise<boolean> => {
167
+ const db = await openDB();
168
+ return new Promise((resolve, reject) => {
169
+ const tx = db.transaction("directories", "readonly");
170
+ const store = tx.objectStore("directories");
171
+ const request = store.get(toMetaKey(path));
172
+
173
+ request.onerror = () => reject(request.error);
174
+ request.onsuccess = () => resolve(request.result === true);
175
+
176
+ tx.oncomplete = () => db.close();
177
+ });
178
+ };
179
+
180
+ const adapter: IStorageAdapter = {
181
+ kind: "web",
182
+ rootId,
183
+
184
+ // ─────────────────────────────────────────────────────────────────
185
+ // Directory Management
186
+ // ─────────────────────────────────────────────────────────────────
187
+
188
+ async ensureDir(path: TAdapterPath): Promise<void> {
189
+ try {
190
+ await markDirectory(path);
191
+ } catch (error) {
192
+ throw new AdapterIOError("ensureDir", path, error as Error);
193
+ }
194
+ },
195
+
196
+ async exists(path: TAdapterPath): Promise<boolean> {
197
+ try {
198
+ // Check if it's a file in cache storage
199
+ const cache = await caches.open(cacheName);
200
+ const response = await cache.match(toCacheKey(path));
201
+ if (response) {
202
+ return true;
203
+ }
204
+
205
+ // Check if it's a directory
206
+ return await hasDirectory(path);
207
+ } catch {
208
+ return false;
209
+ }
210
+ },
211
+
212
+ async remove(path: TAdapterPath): Promise<void> {
213
+ try {
214
+ const cache = await caches.open(cacheName);
215
+ await cache.delete(toCacheKey(path));
216
+ await deleteMetadata(path);
217
+ } catch (error) {
218
+ throw new AdapterIOError("remove", path, error as Error);
219
+ }
220
+ },
221
+
222
+ async removeDir(path: TAdapterPath): Promise<void> {
223
+ try {
224
+ const cache = await caches.open(cacheName);
225
+ const keys = await cache.keys();
226
+ const prefix = toCacheKey(path + "/");
227
+
228
+ // Delete all entries with this prefix
229
+ for (const request of keys) {
230
+ if (request.url.startsWith(prefix) || request.url === toCacheKey(path)) {
231
+ await cache.delete(request);
232
+ const entryPath = fromCacheKey(request.url);
233
+ await deleteMetadata(entryPath);
234
+ }
235
+ }
236
+
237
+ // Remove directory marker
238
+ const db = await openDB();
239
+ await new Promise<void>((resolve, reject) => {
240
+ const tx = db.transaction("directories", "readwrite");
241
+ const store = tx.objectStore("directories");
242
+ store.delete(toMetaKey(path));
243
+ tx.oncomplete = () => {
244
+ db.close();
245
+ resolve();
246
+ };
247
+ tx.onerror = () => reject(tx.error);
248
+ });
249
+ } catch (error) {
250
+ throw new AdapterIOError("removeDir", path, error as Error);
251
+ }
252
+ },
253
+
254
+ async listDir(path: TAdapterPath): Promise<ReadonlyArray<TAdapterPath>> {
255
+ try {
256
+ const cache = await caches.open(cacheName);
257
+ const keys = await cache.keys();
258
+ const prefix = toCacheKey(path + "/");
259
+ const entries: TAdapterPath[] = [];
260
+
261
+ for (const request of keys) {
262
+ if (request.url.startsWith(prefix)) {
263
+ // Extract the relative path from this directory
264
+ const relativePath = request.url.slice(prefix.length);
265
+ // Only include direct children (no further slashes)
266
+ if (!relativePath.includes("/")) {
267
+ entries.push(relativePath);
268
+ }
269
+ }
270
+ }
271
+
272
+ return entries;
273
+ } catch (error) {
274
+ throw new AdapterIOError("listDir", path, error as Error);
275
+ }
276
+ },
277
+
278
+ // ─────────────────────────────────────────────────────────────────
279
+ // File I/O
280
+ // ─────────────────────────────────────────────────────────────────
281
+
282
+ async readText(path: TAdapterPath, _encoding: "utf8"): Promise<string> {
283
+ try {
284
+ const cache = await caches.open(cacheName);
285
+ const response = await cache.match(toCacheKey(path));
286
+ if (!response) {
287
+ throw new Error("File not found");
288
+ }
289
+ return await response.text();
290
+ } catch (error) {
291
+ throw new AdapterIOError("readText", path, error as Error);
292
+ }
293
+ },
294
+
295
+ async writeTextAtomic(path: TAdapterPath, content: string, _encoding: "utf8"): Promise<void> {
296
+ try {
297
+ const cache = await caches.open(cacheName);
298
+ const blob = new Blob([content], { type: "text/plain; charset=utf-8" });
299
+ const response = new Response(blob);
300
+
301
+ // Cache Storage puts are atomic by nature
302
+ await cache.put(toCacheKey(path), response);
303
+
304
+ // Store metadata
305
+ await setMetadata(path, {
306
+ sizeBytes: blob.size,
307
+ mtimeMs: Date.now(),
308
+ contentType: "text/plain",
309
+ });
310
+ } catch (error) {
311
+ throw new AdapterIOError("writeTextAtomic", path, error as Error);
312
+ }
313
+ },
314
+
315
+ async stat(path: TAdapterPath): Promise<IFileStat> {
316
+ try {
317
+ const meta = await getMetadata(path);
318
+ if (!meta) {
319
+ throw new Error("File not found");
320
+ }
321
+ return {
322
+ sizeBytes: meta.sizeBytes,
323
+ mtimeMs: meta.mtimeMs,
324
+ };
325
+ } catch (error) {
326
+ throw new AdapterIOError("stat", path, error as Error);
327
+ }
328
+ },
329
+
330
+ async writeBinaryAtomic(
331
+ path: TAdapterPath,
332
+ source: TBinarySource,
333
+ options?: IBinaryWriteOptions
334
+ ): Promise<IBinaryWriteResult> {
335
+ try {
336
+ const cache = await caches.open(cacheName);
337
+ let blob: Blob;
338
+ let contentType: string | undefined;
339
+
340
+ switch (source.type) {
341
+ case "url": {
342
+ // Fetch with progress tracking
343
+ const response = await fetch(source.url, {
344
+ headers: { ...source.headers, ...options?.headers },
345
+ });
346
+
347
+ if (!response.ok) {
348
+ throw new Error(`Fetch failed with status ${response.status}`);
349
+ }
350
+
351
+ contentType = response.headers.get("content-type") ?? undefined;
352
+
353
+ // Try to track progress if content-length is available
354
+ const contentLength = response.headers.get("content-length");
355
+ if (contentLength && options?.onProgress && response.body) {
356
+ const total = parseInt(contentLength, 10);
357
+ let loaded = 0;
358
+ const reader = response.body.getReader();
359
+ const chunks: Uint8Array[] = [];
360
+
361
+ // eslint-disable-next-line no-constant-condition
362
+ while (true) {
363
+ const { done, value } = await reader.read();
364
+ if (done) {
365
+ break;
366
+ }
367
+ chunks.push(value);
368
+ loaded += value.length;
369
+ options.onProgress((loaded / total) * 100);
370
+ }
371
+
372
+ blob = new Blob(
373
+ chunks.map((c) => c.buffer as ArrayBuffer),
374
+ { type: contentType }
375
+ );
376
+ } else {
377
+ blob = await response.blob();
378
+ options?.onProgress?.(100);
379
+ }
380
+ break;
381
+ }
382
+
383
+ case "blob": {
384
+ blob = source.blob;
385
+ contentType = source.blob.type || undefined;
386
+ options?.onProgress?.(100);
387
+ break;
388
+ }
389
+
390
+ case "bytes": {
391
+ blob = new Blob([source.bytes.buffer as ArrayBuffer]);
392
+ options?.onProgress?.(100);
393
+ break;
394
+ }
395
+
396
+ case "file": {
397
+ // File paths not supported on web
398
+ throw new UnsupportedSourceError("file", "web");
399
+ }
400
+
401
+ default: {
402
+ const _exhaustive: never = source;
403
+ throw new UnsupportedSourceError((_exhaustive as TBinarySource).type, "web");
404
+ }
405
+ }
406
+
407
+ // Store in cache
408
+ const response = new Response(blob, {
409
+ headers: contentType ? { "Content-Type": contentType } : {},
410
+ });
411
+ await cache.put(toCacheKey(path), response);
412
+
413
+ // Store metadata
414
+ const meta: IFileMetadata = {
415
+ sizeBytes: blob.size,
416
+ mtimeMs: Date.now(),
417
+ contentType,
418
+ };
419
+ await setMetadata(path, meta);
420
+
421
+ return {
422
+ sizeBytes: blob.size,
423
+ contentType,
424
+ };
425
+ } catch (error) {
426
+ if (error instanceof UnsupportedSourceError) {
427
+ throw error;
428
+ }
429
+ throw new AdapterIOError("writeBinaryAtomic", path, error as Error);
430
+ }
431
+ },
432
+
433
+ async getPublicUri(path: TAdapterPath): Promise<string> {
434
+ try {
435
+ const cache = await caches.open(cacheName);
436
+ const response = await cache.match(toCacheKey(path));
437
+ if (!response) {
438
+ throw new Error("File not found");
439
+ }
440
+
441
+ const blob = await response.blob();
442
+ const blobUrl = URL.createObjectURL(blob);
443
+
444
+ // Track for cleanup
445
+ blobUrls.add(blobUrl);
446
+
447
+ return blobUrl;
448
+ } catch (error) {
449
+ throw new AdapterIOError("getPublicUri", path, error as Error);
450
+ }
451
+ },
452
+ };
453
+
454
+ // Add cleanup method (non-standard, for internal use)
455
+ (adapter as IWebAdapterWithCleanup).revokeUri = (uri: string): void => {
456
+ if (blobUrls.has(uri)) {
457
+ URL.revokeObjectURL(uri);
458
+ blobUrls.delete(uri);
459
+ }
460
+ };
461
+
462
+ (adapter as IWebAdapterWithCleanup).revokeAllUris = (): void => {
463
+ for (const uri of blobUrls) {
464
+ URL.revokeObjectURL(uri);
465
+ }
466
+ blobUrls.clear();
467
+ };
468
+
469
+ return adapter;
470
+ }
471
+
472
+ /**
473
+ * Extended interface for web adapter with cleanup methods.
474
+ */
475
+ export interface IWebAdapterWithCleanup extends IStorageAdapter {
476
+ /** Revokes a specific blob URL to free memory. */
477
+ revokeUri(uri: string): void;
478
+ /** Revokes all created blob URLs. */
479
+ revokeAllUris(): void;
480
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Adapter-relative path. Core never constructs absolute OS paths.
3
+ * All paths are relative to the adapter's root.
4
+ */
5
+ export type TAdapterPath = string;
6
+
7
+ /**
8
+ * Discriminated union for binary content sources.
9
+ * Adapters support the subset they can handle; throw UnsupportedSourceError otherwise.
10
+ */
11
+ export type TBinarySource =
12
+ | {
13
+ readonly type: "url";
14
+ readonly url: string;
15
+ readonly headers?: Readonly<Record<string, string>>;
16
+ }
17
+ | { readonly type: "file"; readonly filePath: string } // Native only
18
+ | { readonly type: "blob"; readonly blob: Blob } // Web only
19
+ | { readonly type: "bytes"; readonly bytes: Uint8Array }; // Universal fallback
20
+
21
+ /**
22
+ * Result of a binary write operation.
23
+ */
24
+ export interface IBinaryWriteResult {
25
+ readonly sizeBytes: number;
26
+ readonly contentType?: string;
27
+ }
28
+
29
+ /**
30
+ * File/entry stat information.
31
+ */
32
+ export interface IFileStat {
33
+ readonly sizeBytes: number;
34
+ readonly mtimeMs: number;
35
+ }
36
+
37
+ /**
38
+ * Progress callback for binary operations.
39
+ */
40
+ export type TProgressCallback = (percent: number) => void;
41
+
42
+ /**
43
+ * Options for binary write operations.
44
+ */
45
+ export interface IBinaryWriteOptions {
46
+ readonly onProgress?: TProgressCallback;
47
+ readonly headers?: Readonly<Record<string, string>>;
48
+ }
49
+
50
+ /**
51
+ * Storage adapter interface.
52
+ * Represents a key-addressed "filesystem-like" store with atomic write support.
53
+ * All paths are adapter-relative. Core never constructs absolute OS paths.
54
+ */
55
+ export interface IStorageAdapter {
56
+ /** Adapter kind identifier. */
57
+ readonly kind: string;
58
+
59
+ /** A stable root identifier (e.g., absolute cache root on native, cache name on web). */
60
+ readonly rootId: string;
61
+
62
+ // ─────────────────────────────────────────────────────────────────
63
+ // Directory Management
64
+ // ─────────────────────────────────────────────────────────────────
65
+
66
+ /**
67
+ * Ensures a directory exists at the given path, creating it recursively if needed.
68
+ */
69
+ ensureDir(path: TAdapterPath): Promise<void>;
70
+
71
+ /**
72
+ * Checks if a file or directory exists at the given path.
73
+ */
74
+ exists(path: TAdapterPath): Promise<boolean>;
75
+
76
+ /**
77
+ * Removes a file at the given path. No-op if file doesn't exist.
78
+ */
79
+ remove(path: TAdapterPath): Promise<void>;
80
+
81
+ /**
82
+ * Removes a directory and all its contents recursively.
83
+ */
84
+ removeDir(path: TAdapterPath): Promise<void>;
85
+
86
+ /**
87
+ * Lists all entries (files/directories) in a directory.
88
+ * Returns relative paths from the listed directory.
89
+ */
90
+ listDir(path: TAdapterPath): Promise<ReadonlyArray<TAdapterPath>>;
91
+
92
+ // ─────────────────────────────────────────────────────────────────
93
+ // File I/O
94
+ // ─────────────────────────────────────────────────────────────────
95
+
96
+ /**
97
+ * Reads text content from a file.
98
+ */
99
+ readText(path: TAdapterPath, encoding: "utf8"): Promise<string>;
100
+
101
+ /**
102
+ * Writes text content atomically (write to temp, then rename).
103
+ */
104
+ writeTextAtomic(path: TAdapterPath, content: string, encoding: "utf8"): Promise<void>;
105
+
106
+ /**
107
+ * Gets file statistics.
108
+ */
109
+ stat(path: TAdapterPath): Promise<IFileStat>;
110
+
111
+ /**
112
+ * Writes binary content atomically.
113
+ * Source may be URL, blob, file path, or raw bytes depending on adapter.
114
+ * Adapter must throw UnsupportedSourceError for unsupported source types.
115
+ */
116
+ writeBinaryAtomic(
117
+ path: TAdapterPath,
118
+ source: TBinarySource,
119
+ options?: IBinaryWriteOptions
120
+ ): Promise<IBinaryWriteResult>;
121
+
122
+ /**
123
+ * Returns a URI that the app can use to read/view the stored file.
124
+ * - Native: file:// URI
125
+ * - Web: blob: URL (caller responsible for revoking)
126
+ */
127
+ getPublicUri(path: TAdapterPath): Promise<string>;
128
+ }