@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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dean
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,415 @@
1
+ # @dynlabs/react-native-immutable-file-cache
2
+
3
+ [![CI](https://github.com/dienp/react-native-immutable-file-cache/actions/workflows/ci.yml/badge.svg)](https://github.com/dienp/react-native-immutable-file-cache/actions/workflows/ci.yml)
4
+ [![npm version](https://img.shields.io/npm/v/@dynlabs/react-native-immutable-file-cache.svg)](https://www.npmjs.com/package/@dynlabs/react-native-immutable-file-cache)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+
7
+ A cross-platform immutable file cache for React Native and Web with pluggable storage adapters.
8
+
9
+ ## Features
10
+
11
+ - **Immutable entries** - Once cached, entries cannot be overwritten
12
+ - **Cross-platform** - Works on iOS, Android, and Web
13
+ - **Pluggable adapters** - Swap storage backends without changing application code
14
+ - **TTL & LRU pruning** - Automatic cache management
15
+ - **Atomic writes** - Crash-safe file operations
16
+ - **TypeScript first** - Full type safety with strict types
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ npm install @dynlabs/react-native-immutable-file-cache
22
+ ```
23
+
24
+ ### React Native (iOS/Android)
25
+
26
+ Also install the filesystem dependency:
27
+
28
+ ```bash
29
+ npm install react-native-fs
30
+ cd ios && pod install
31
+ ```
32
+
33
+ ### Web
34
+
35
+ No additional dependencies required - uses Cache Storage and IndexedDB.
36
+
37
+ ## Quick Start
38
+
39
+ ```typescript
40
+ import { createImmutableFileCache } from "@dynlabs/react-native-immutable-file-cache";
41
+
42
+ // Create cache instance
43
+ const cache = await createImmutableFileCache({
44
+ namespace: "images",
45
+ defaultTtlMs: 7 * 24 * 60 * 60 * 1000, // 7 days
46
+ maxSizeBytes: 100 * 1024 * 1024, // 100 MB
47
+ });
48
+
49
+ // Cache an image from URL
50
+ const result = await cache.putFromUrl(
51
+ "avatar-123",
52
+ "https://example.com/avatar.jpg"
53
+ );
54
+
55
+ if (result.status === "created") {
56
+ console.log("Cached new image:", result.entry.sizeBytes, "bytes");
57
+ } else {
58
+ console.log("Image already cached");
59
+ }
60
+
61
+ // Retrieve cached entry
62
+ const entry = await cache.get("avatar-123");
63
+ if (entry) {
64
+ // Use entry.uri in Image component
65
+ <Image source={{ uri: entry.uri }} />
66
+ }
67
+ ```
68
+
69
+ ## Architecture
70
+
71
+ The package uses a three-layer architecture:
72
+
73
+ ```
74
+ src/
75
+ ├── core/ # Platform-agnostic cache engine
76
+ ├── adapters/ # Platform-specific storage adapters
77
+ └── index.*.ts # Platform entrypoints
78
+ ```
79
+
80
+ ### Core Layer
81
+
82
+ Contains all cache logic (immutability rules, TTL, LRU pruning, indexing) without any platform-specific code. The core depends only on the `IStorageAdapter` interface.
83
+
84
+ ### Adapters Layer
85
+
86
+ Platform-specific implementations of `IStorageAdapter`:
87
+
88
+ - **RNFS Adapter** - Uses `react-native-fs` for native platforms
89
+ - **Web Adapter** - Uses Cache Storage + IndexedDB for browsers
90
+ - **Memory Adapter** - In-memory storage for testing
91
+
92
+ ### Public API
93
+
94
+ Platform entrypoints automatically select the appropriate adapter:
95
+
96
+ - `index.native.ts` - React Native (iOS/Android)
97
+ - `index.web.ts` - Web browsers
98
+
99
+ ## Adapters
100
+
101
+ ### Using the Default Adapter
102
+
103
+ ```typescript
104
+ // React Native - automatically uses RNFS adapter
105
+ import { createImmutableFileCache } from "@dynlabs/react-native-immutable-file-cache";
106
+
107
+ // Web - automatically uses Web adapter
108
+ import { createImmutableFileCache } from "@dynlabs/react-native-immutable-file-cache/web";
109
+ ```
110
+
111
+ ### Injecting a Custom Adapter
112
+
113
+ ```typescript
114
+ import { createImmutableFileCache, createRnfsAdapter } from "@dynlabs/react-native-immutable-file-cache";
115
+
116
+ const customAdapter = createRnfsAdapter({
117
+ baseDir: "/custom/path",
118
+ namespace: "my-app",
119
+ });
120
+
121
+ const cache = await createImmutableFileCache({
122
+ adapter: customAdapter,
123
+ });
124
+ ```
125
+
126
+ ### Building a Custom Adapter
127
+
128
+ Implement the `IStorageAdapter` interface:
129
+
130
+ ```typescript
131
+ import type { IStorageAdapter, TBinarySource, IBinaryWriteResult } from "@dynlabs/react-native-immutable-file-cache";
132
+
133
+ const myAdapter: IStorageAdapter = {
134
+ kind: "my-storage",
135
+ rootId: "my-root",
136
+
137
+ async ensureDir(path) { /* Create directory */ },
138
+ async exists(path) { /* Check if exists */ },
139
+ async remove(path) { /* Remove file */ },
140
+ async removeDir(path) { /* Remove directory recursively */ },
141
+ async listDir(path) { /* List directory contents */ },
142
+ async readText(path, encoding) { /* Read text file */ },
143
+ async writeTextAtomic(path, content, encoding) { /* Write text atomically */ },
144
+ async stat(path) { /* Get file stats */ },
145
+ async writeBinaryAtomic(path, source, options) { /* Write binary atomically */ },
146
+ async getPublicUri(path) { /* Get URI for file */ },
147
+ };
148
+ ```
149
+
150
+ ### Supported Binary Sources by Adapter
151
+
152
+ | Source Type | RNFS Adapter | Web Adapter | Memory Adapter |
153
+ |-------------|--------------|-------------|----------------|
154
+ | `url` | ✅ | ✅ | ✅ |
155
+ | `file` | ✅ | ❌ | ✅ (simulated) |
156
+ | `blob` | ❌ | ✅ | ✅ |
157
+ | `bytes` | ✅ | ✅ | ✅ |
158
+
159
+ ## API Reference
160
+
161
+ ### `createImmutableFileCache(options?)`
162
+
163
+ Creates a new cache instance.
164
+
165
+ #### Options
166
+
167
+ | Option | Type | Default | Description |
168
+ |--------|------|---------|-------------|
169
+ | `namespace` | `string` | `"default"` | Cache namespace for isolation |
170
+ | `adapter` | `IStorageAdapter` | auto-detected | Custom storage adapter |
171
+ | `defaultTtlMs` | `number` | `undefined` | Default TTL for entries |
172
+ | `maxSizeBytes` | `number` | `undefined` | Max cache size (triggers LRU) |
173
+ | `autoPruneExpired` | `boolean` | `true` | Auto-prune on operations |
174
+ | `hashFn` | `(input: string) => string \| Promise<string>` | SHA-256 | Custom hash function |
175
+
176
+ ### Put Operations
177
+
178
+ All put operations are immutable - they return `{ status: "exists" }` if the key already exists.
179
+
180
+ #### `putFromUrl(key, url, options?)`
181
+
182
+ Cache content from a URL.
183
+
184
+ ```typescript
185
+ const result = await cache.putFromUrl("image-key", "https://example.com/image.jpg", {
186
+ ttlMs: 86400000, // 1 day
187
+ ext: ".jpg",
188
+ metadata: { source: "cdn" },
189
+ onProgress: (pct) => console.log(`${pct}% downloaded`),
190
+ headers: { Authorization: "Bearer token" },
191
+ });
192
+ ```
193
+
194
+ #### `putFromFile(key, filePath, options?)` (Native only)
195
+
196
+ Cache content from a local file.
197
+
198
+ ```typescript
199
+ const result = await cache.putFromFile("doc-key", "/path/to/document.pdf", {
200
+ ext: ".pdf",
201
+ });
202
+ ```
203
+
204
+ #### `putFromBlob(key, blob, options?)` (Web only)
205
+
206
+ Cache content from a Blob.
207
+
208
+ ```typescript
209
+ const blob = await response.blob();
210
+ const result = await cache.putFromBlob("data-key", blob);
211
+ ```
212
+
213
+ #### `putFromBytes(key, bytes, options?)`
214
+
215
+ Cache raw bytes.
216
+
217
+ ```typescript
218
+ const result = await cache.putFromBytes("raw-key", new Uint8Array([1, 2, 3]));
219
+ ```
220
+
221
+ ### Get Operations
222
+
223
+ #### `get(key)`
224
+
225
+ Get entry with URI. Updates `lastAccessedAt`.
226
+
227
+ ```typescript
228
+ const result = await cache.get("image-key");
229
+ if (result) {
230
+ console.log("URI:", result.uri);
231
+ console.log("Size:", result.entry.sizeBytes);
232
+ }
233
+ ```
234
+
235
+ #### `has(key)`
236
+
237
+ Check if key exists and is not expired.
238
+
239
+ ```typescript
240
+ if (await cache.has("image-key")) {
241
+ // Entry exists
242
+ }
243
+ ```
244
+
245
+ #### `peek(key)`
246
+
247
+ Get entry metadata without updating `lastAccessedAt`.
248
+
249
+ ```typescript
250
+ const entry = await cache.peek("image-key");
251
+ ```
252
+
253
+ ### List/Query Operations
254
+
255
+ #### `list(options?)`
256
+
257
+ List entries with sorting, filtering, and pagination.
258
+
259
+ ```typescript
260
+ const entries = await cache.list({
261
+ sortBy: "createdAt", // "createdAt" | "lastAccessedAt" | "sizeBytes" | "key"
262
+ order: "desc", // "asc" | "desc"
263
+ limit: 10,
264
+ offset: 0,
265
+ filter: (entry) => entry.sizeBytes > 1000,
266
+ });
267
+ ```
268
+
269
+ #### `stats()`
270
+
271
+ Get cache statistics.
272
+
273
+ ```typescript
274
+ const stats = await cache.stats();
275
+ console.log(`${stats.entryCount} entries, ${stats.totalSizeBytes} bytes`);
276
+ ```
277
+
278
+ ### Remove/Prune Operations
279
+
280
+ #### `remove(key)`
281
+
282
+ Remove specific entry.
283
+
284
+ ```typescript
285
+ const removed = await cache.remove("image-key");
286
+ ```
287
+
288
+ #### `removeExpired()`
289
+
290
+ Remove all expired entries.
291
+
292
+ ```typescript
293
+ const result = await cache.removeExpired();
294
+ console.log(`Removed ${result.removedCount} entries, freed ${result.freedBytes} bytes`);
295
+ ```
296
+
297
+ #### `pruneLru(maxSizeBytes)`
298
+
299
+ Prune to fit size limit using LRU.
300
+
301
+ ```typescript
302
+ await cache.pruneLru(50 * 1024 * 1024); // Prune to 50MB
303
+ ```
304
+
305
+ #### `clear()`
306
+
307
+ Remove all entries.
308
+
309
+ ```typescript
310
+ await cache.clear();
311
+ ```
312
+
313
+ ### Maintenance
314
+
315
+ #### `validateAndRepair()`
316
+
317
+ Validate index against filesystem and rebuild if needed.
318
+
319
+ ```typescript
320
+ const result = await cache.validateAndRepair();
321
+ if (result.repaired) {
322
+ console.log("Fixed issues:", result.issues);
323
+ }
324
+ ```
325
+
326
+ ## Web-Specific: Blob URL Cleanup
327
+
328
+ On web, `getPublicUri()` returns `blob:` URLs. To prevent memory leaks, clean up URLs when done:
329
+
330
+ ```typescript
331
+ import { createWebAdapter, IWebAdapterWithCleanup } from "@dynlabs/react-native-immutable-file-cache";
332
+
333
+ const adapter = createWebAdapter() as IWebAdapterWithCleanup;
334
+
335
+ // When done with a specific URL
336
+ adapter.revokeUri(entry.uri);
337
+
338
+ // Or revoke all URLs
339
+ adapter.revokeAllUris();
340
+ ```
341
+
342
+ ## Error Handling
343
+
344
+ The package provides typed errors:
345
+
346
+ ```typescript
347
+ import {
348
+ CacheError,
349
+ UnsupportedSourceError,
350
+ AdapterIOError,
351
+ CorruptIndexError,
352
+ ImmutableConflictError,
353
+ EntryNotFoundError,
354
+ } from "@dynlabs/react-native-immutable-file-cache";
355
+
356
+ try {
357
+ await cache.putFromBlob("key", blob);
358
+ } catch (error) {
359
+ if (error instanceof UnsupportedSourceError) {
360
+ console.log(`${error.sourceType} not supported on ${error.adapterKind}`);
361
+ }
362
+ }
363
+ ```
364
+
365
+ ## Future Adapters
366
+
367
+ The adapter architecture makes it easy to add support for:
368
+
369
+ - `react-native-blob-util`
370
+ - `expo-file-system`
371
+ - S3-backed remote cache
372
+ - SQLite-based storage
373
+ - Custom encrypted storage
374
+
375
+ To add a new adapter, implement `IStorageAdapter` and pass it to `createImmutableFileCache({ adapter })`.
376
+
377
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed instructions on adding adapters.
378
+
379
+ ## Testing
380
+
381
+ The package includes a `MemoryAdapter` for testing:
382
+
383
+ ```typescript
384
+ import { CacheEngine, createMemoryAdapter } from "@dynlabs/react-native-immutable-file-cache";
385
+
386
+ const adapter = createMemoryAdapter("test");
387
+ const cache = new CacheEngine({ namespace: "test" }, adapter);
388
+ await cache.init();
389
+
390
+ // Run tests...
391
+
392
+ // Clean up
393
+ adapter._reset();
394
+ ```
395
+
396
+ ## Contributing
397
+
398
+ Contributions are welcome! Please read our [Contributing Guide](CONTRIBUTING.md) for details.
399
+
400
+ ```bash
401
+ # Clone and install
402
+ git clone https://github.com/dienp/react-native-immutable-file-cache.git
403
+ cd react-native-immutable-file-cache
404
+ npm install
405
+
406
+ # Development
407
+ npm run typecheck # Type check
408
+ npm run lint # Lint
409
+ npm test # Run tests
410
+ npm run build # Build
411
+ ```
412
+
413
+ ## License
414
+
415
+ MIT
@@ -0,0 +1,266 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.createMemoryAdapter = createMemoryAdapter;
7
+ var _errors = require("../core/errors");
8
+ /**
9
+ * In-Memory Storage Adapter
10
+ *
11
+ * For testing purposes only.
12
+ * Implements IStorageAdapter using in-memory storage.
13
+ */
14
+
15
+ /* eslint-disable @typescript-eslint/require-await */
16
+
17
+ /**
18
+ * Extended interface for memory adapter with test utilities.
19
+ */
20
+
21
+ /**
22
+ * Creates an in-memory storage adapter for testing.
23
+ */
24
+ function createMemoryAdapter(namespace) {
25
+ const ns = namespace ?? "default";
26
+ const rootId = `memory://${ns}`;
27
+
28
+ // In-memory storage
29
+ const files = new Map();
30
+ const directories = new Set();
31
+
32
+ // Track blob URLs for cleanup
33
+ const blobUrls = new Map();
34
+
35
+ /**
36
+ * Normalizes path by removing leading/trailing slashes.
37
+ */
38
+ const normalizePath = path => {
39
+ return path.replace(/^\/+|\/+$/g, "");
40
+ };
41
+
42
+ /**
43
+ * Gets the parent directory of a path.
44
+ */
45
+ const getParentDir = path => {
46
+ const lastSlash = path.lastIndexOf("/");
47
+ return lastSlash > 0 ? path.substring(0, lastSlash) : null;
48
+ };
49
+ const adapter = {
50
+ kind: "memory",
51
+ rootId,
52
+ // ─────────────────────────────────────────────────────────────────
53
+ // Directory Management
54
+ // ─────────────────────────────────────────────────────────────────
55
+
56
+ async ensureDir(path) {
57
+ const normalized = normalizePath(path);
58
+ if (normalized) {
59
+ directories.add(normalized);
60
+ // Also ensure parent directories
61
+ const parent = getParentDir(normalized);
62
+ if (parent) {
63
+ await this.ensureDir(parent);
64
+ }
65
+ }
66
+ },
67
+ async exists(path) {
68
+ const normalized = normalizePath(path);
69
+ return files.has(normalized) || directories.has(normalized);
70
+ },
71
+ async remove(path) {
72
+ const normalized = normalizePath(path);
73
+ files.delete(normalized);
74
+ },
75
+ async removeDir(path) {
76
+ const normalized = normalizePath(path);
77
+ const prefix = normalized + "/";
78
+
79
+ // Remove all files with this prefix
80
+ for (const key of files.keys()) {
81
+ if (key === normalized || key.startsWith(prefix)) {
82
+ files.delete(key);
83
+ }
84
+ }
85
+
86
+ // Remove all directories with this prefix
87
+ for (const dir of directories) {
88
+ if (dir === normalized || dir.startsWith(prefix)) {
89
+ directories.delete(dir);
90
+ }
91
+ }
92
+ },
93
+ async listDir(path) {
94
+ const normalized = normalizePath(path);
95
+ const prefix = normalized ? normalized + "/" : "";
96
+ const entries = new Set();
97
+
98
+ // Find all files in this directory
99
+ for (const key of files.keys()) {
100
+ if (key.startsWith(prefix)) {
101
+ const relativePath = key.slice(prefix.length);
102
+ const firstSlash = relativePath.indexOf("/");
103
+ if (firstSlash === -1) {
104
+ // Direct child file
105
+ entries.add(relativePath);
106
+ }
107
+ }
108
+ }
109
+ return Array.from(entries);
110
+ },
111
+ // ─────────────────────────────────────────────────────────────────
112
+ // File I/O
113
+ // ─────────────────────────────────────────────────────────────────
114
+
115
+ async readText(path, _encoding) {
116
+ const normalized = normalizePath(path);
117
+ const entry = files.get(normalized);
118
+ if (!entry) {
119
+ throw new _errors.AdapterIOError("readText", path, new Error("File not found"));
120
+ }
121
+ const decoder = new TextDecoder("utf-8");
122
+ return decoder.decode(entry.content);
123
+ },
124
+ async writeTextAtomic(path, content, _encoding) {
125
+ const normalized = normalizePath(path);
126
+ const encoder = new TextEncoder();
127
+ const bytes = encoder.encode(content);
128
+ files.set(normalized, {
129
+ content: bytes,
130
+ mtime: Date.now(),
131
+ contentType: "text/plain"
132
+ });
133
+
134
+ // Ensure parent directory exists
135
+ const parent = getParentDir(normalized);
136
+ if (parent) {
137
+ directories.add(parent);
138
+ }
139
+ },
140
+ async stat(path) {
141
+ const normalized = normalizePath(path);
142
+ const entry = files.get(normalized);
143
+ if (!entry) {
144
+ throw new _errors.AdapterIOError("stat", path, new Error("File not found"));
145
+ }
146
+ return {
147
+ sizeBytes: entry.content.length,
148
+ mtimeMs: entry.mtime
149
+ };
150
+ },
151
+ async writeBinaryAtomic(path, source, options) {
152
+ const normalized = normalizePath(path);
153
+ let content;
154
+ let contentType;
155
+ switch (source.type) {
156
+ case "url":
157
+ {
158
+ // Simulate URL fetch
159
+ try {
160
+ const response = await fetch(source.url, {
161
+ headers: {
162
+ ...source.headers,
163
+ ...options?.headers
164
+ }
165
+ });
166
+ if (!response.ok) {
167
+ throw new Error(`Fetch failed: ${response.status}`);
168
+ }
169
+ contentType = response.headers.get("content-type") ?? undefined;
170
+ const buffer = await response.arrayBuffer();
171
+ content = new Uint8Array(buffer);
172
+ options?.onProgress?.(100);
173
+ } catch (error) {
174
+ throw new _errors.AdapterIOError("writeBinaryAtomic", path, error);
175
+ }
176
+ break;
177
+ }
178
+ case "file":
179
+ {
180
+ // Simulate file copy (just use path as content for testing)
181
+ const encoder = new TextEncoder();
182
+ content = encoder.encode(`[file:${source.filePath}]`);
183
+ options?.onProgress?.(100);
184
+ break;
185
+ }
186
+ case "blob":
187
+ {
188
+ const buffer = await source.blob.arrayBuffer();
189
+ content = new Uint8Array(buffer);
190
+ contentType = source.blob.type || undefined;
191
+ options?.onProgress?.(100);
192
+ break;
193
+ }
194
+ case "bytes":
195
+ {
196
+ content = source.bytes;
197
+ options?.onProgress?.(100);
198
+ break;
199
+ }
200
+ default:
201
+ {
202
+ const _exhaustive = source;
203
+ throw new _errors.AdapterIOError("writeBinaryAtomic", path, new Error(`Unknown source type: ${_exhaustive.type}`));
204
+ }
205
+ }
206
+ files.set(normalized, {
207
+ content,
208
+ mtime: Date.now(),
209
+ contentType
210
+ });
211
+
212
+ // Ensure parent directory exists
213
+ const parent = getParentDir(normalized);
214
+ if (parent) {
215
+ directories.add(parent);
216
+ }
217
+ return {
218
+ sizeBytes: content.length,
219
+ contentType
220
+ };
221
+ },
222
+ async getPublicUri(path) {
223
+ const normalized = normalizePath(path);
224
+ const entry = files.get(normalized);
225
+ if (!entry) {
226
+ throw new _errors.AdapterIOError("getPublicUri", path, new Error("File not found"));
227
+ }
228
+
229
+ // Check if we already have a blob URL for this path
230
+ const existing = blobUrls.get(normalized);
231
+ if (existing) {
232
+ return existing;
233
+ }
234
+
235
+ // Create blob URL
236
+ const blob = new Blob([entry.content.buffer], {
237
+ type: entry.contentType ?? "application/octet-stream"
238
+ });
239
+ const url = URL.createObjectURL(blob);
240
+ blobUrls.set(normalized, url);
241
+ return url;
242
+ },
243
+ // ─────────────────────────────────────────────────────────────────
244
+ // Test Utilities
245
+ // ─────────────────────────────────────────────────────────────────
246
+
247
+ _reset() {
248
+ files.clear();
249
+ directories.clear();
250
+ // Revoke all blob URLs
251
+ for (const url of blobUrls.values()) {
252
+ URL.revokeObjectURL(url);
253
+ }
254
+ blobUrls.clear();
255
+ },
256
+ _getContent(path) {
257
+ const normalized = normalizePath(path);
258
+ return files.get(normalized)?.content;
259
+ },
260
+ _getAllPaths() {
261
+ return Array.from(files.keys());
262
+ }
263
+ };
264
+ return adapter;
265
+ }
266
+ //# sourceMappingURL=memoryAdapter.js.map