@livestore/utils 0.4.0-dev.17 → 0.4.0-dev.19

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 (86) hide show
  1. package/dist/.tsbuildinfo.json +1 -1
  2. package/dist/browser/Opfs/Opfs.d.ts +51 -0
  3. package/dist/browser/Opfs/Opfs.d.ts.map +1 -0
  4. package/dist/browser/Opfs/Opfs.js +345 -0
  5. package/dist/browser/Opfs/Opfs.js.map +1 -0
  6. package/dist/browser/Opfs/debug-utils.d.ts +20 -0
  7. package/dist/browser/Opfs/debug-utils.d.ts.map +1 -0
  8. package/dist/browser/Opfs/debug-utils.js +94 -0
  9. package/dist/browser/Opfs/debug-utils.js.map +1 -0
  10. package/dist/browser/Opfs/mod.d.ts +4 -0
  11. package/dist/browser/Opfs/mod.d.ts.map +1 -0
  12. package/dist/browser/Opfs/mod.js +4 -0
  13. package/dist/browser/Opfs/mod.js.map +1 -0
  14. package/dist/browser/Opfs/utils.d.ts +68 -0
  15. package/dist/browser/Opfs/utils.d.ts.map +1 -0
  16. package/dist/browser/Opfs/utils.js +206 -0
  17. package/dist/browser/Opfs/utils.js.map +1 -0
  18. package/dist/browser/QuotaExceededError.d.ts +59 -0
  19. package/dist/browser/QuotaExceededError.d.ts.map +1 -0
  20. package/dist/browser/QuotaExceededError.js +2 -0
  21. package/dist/browser/QuotaExceededError.js.map +1 -0
  22. package/dist/browser/WebChannelBrowser.d.ts +22 -0
  23. package/dist/browser/WebChannelBrowser.d.ts.map +1 -0
  24. package/dist/browser/WebChannelBrowser.js +76 -0
  25. package/dist/browser/WebChannelBrowser.js.map +1 -0
  26. package/dist/browser/WebError.d.ts +425 -0
  27. package/dist/browser/WebError.d.ts.map +1 -0
  28. package/dist/browser/WebError.js +414 -0
  29. package/dist/browser/WebError.js.map +1 -0
  30. package/dist/browser/WebError.test.d.ts +2 -0
  31. package/dist/browser/WebError.test.d.ts.map +1 -0
  32. package/dist/browser/WebError.test.js +46 -0
  33. package/dist/browser/WebError.test.js.map +1 -0
  34. package/dist/browser/WebLock.d.ts.map +1 -0
  35. package/dist/browser/WebLock.js.map +1 -0
  36. package/dist/{browser.d.ts → browser/detect.d.ts} +1 -1
  37. package/dist/browser/detect.d.ts.map +1 -0
  38. package/dist/{browser.js → browser/detect.js} +1 -1
  39. package/dist/browser/detect.js.map +1 -0
  40. package/dist/browser/mod.d.ts +8 -0
  41. package/dist/browser/mod.d.ts.map +1 -0
  42. package/dist/browser/mod.js +8 -0
  43. package/dist/browser/mod.js.map +1 -0
  44. package/dist/effect/WebChannel/WebChannel.d.ts +2 -21
  45. package/dist/effect/WebChannel/WebChannel.d.ts.map +1 -1
  46. package/dist/effect/WebChannel/WebChannel.js +3 -75
  47. package/dist/effect/WebChannel/WebChannel.js.map +1 -1
  48. package/dist/effect/WebChannel/WebChannel.test.js +1 -1
  49. package/dist/effect/WebChannel/WebChannel.test.js.map +1 -1
  50. package/dist/effect/{index.d.ts → mod.d.ts} +2 -4
  51. package/dist/effect/mod.d.ts.map +1 -0
  52. package/dist/effect/{index.js → mod.js} +2 -4
  53. package/dist/effect/mod.js.map +1 -0
  54. package/dist/mod.d.ts +1 -1
  55. package/dist/mod.d.ts.map +1 -1
  56. package/dist/mod.js +1 -1
  57. package/dist/mod.js.map +1 -1
  58. package/dist/node/mod.d.ts +1 -1
  59. package/dist/node/mod.d.ts.map +1 -1
  60. package/dist/node/mod.js +1 -1
  61. package/dist/node/mod.js.map +1 -1
  62. package/package.json +27 -19
  63. package/src/browser/Opfs/Opfs.ts +428 -0
  64. package/src/browser/Opfs/debug-utils.ts +151 -0
  65. package/src/browser/Opfs/mod.ts +3 -0
  66. package/src/browser/Opfs/utils.ts +270 -0
  67. package/src/browser/QuotaExceededError.ts +59 -0
  68. package/src/browser/WebChannelBrowser.ts +131 -0
  69. package/src/browser/WebError.test.ts +66 -0
  70. package/src/browser/WebError.ts +599 -0
  71. package/src/browser/mod.ts +8 -0
  72. package/src/effect/WebChannel/WebChannel.test.ts +1 -1
  73. package/src/effect/WebChannel/WebChannel.ts +11 -127
  74. package/src/effect/{index.ts → mod.ts} +1 -2
  75. package/src/mod.ts +1 -1
  76. package/src/node/mod.ts +1 -1
  77. package/dist/browser.d.ts.map +0 -1
  78. package/dist/browser.js.map +0 -1
  79. package/dist/effect/WebLock.d.ts.map +0 -1
  80. package/dist/effect/WebLock.js.map +0 -1
  81. package/dist/effect/index.d.ts.map +0 -1
  82. package/dist/effect/index.js.map +0 -1
  83. /package/dist/{effect → browser}/WebLock.d.ts +0 -0
  84. /package/dist/{effect → browser}/WebLock.js +0 -0
  85. /package/src/{effect → browser}/WebLock.ts +0 -0
  86. /package/src/{browser.ts → browser/detect.ts} +0 -0
@@ -0,0 +1,428 @@
1
+ /// <reference lib="webworker" />
2
+
3
+ import { Effect, Option, Schema, Stream } from 'effect'
4
+ import * as WebError from '../WebError.ts'
5
+
6
+ /**
7
+ * Effect service that exposes ergonomic wrappers around Origin Private File System (OPFS) operations.
8
+ *
9
+ * @remarks
10
+ * - Helpers mirror the File System Access API where possible and parse web exceptions into Effect errors.
11
+ * - Sync access handle helpers can only be used in dedicated workers; invoking them in other contexts fails at runtime.
12
+ *
13
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Origin_private_file_system | MDN Reference}
14
+ */
15
+ export class Opfs extends Effect.Service<Opfs>()('@livestore/utils/Opfs', {
16
+ sync: () => {
17
+ /**
18
+ * Acquire the OPFS root directory handle.
19
+ *
20
+ * @returns Root directory handle for the current origin.
21
+ *
22
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/StorageManager/getDirectory | MDN Reference}
23
+ */
24
+ const getRootDirectoryHandle = Effect.tryPromise({
25
+ try: () => navigator.storage.getDirectory(),
26
+ catch: (u) => WebError.parseWebError(u, [WebError.SecurityError]),
27
+ })
28
+
29
+ /**
30
+ * Resolve (and optionally create) a file handle relative to a directory.
31
+ *
32
+ * @param parent - Directory to search.
33
+ * @param name - Target file name.
34
+ * @param options - Forwarded `getFileHandle` options such as `{ create: true }`.
35
+ *
36
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/FileSystemDirectoryHandle/getFileHandle | MDN Reference}
37
+ */
38
+ const getFileHandle = (parent: FileSystemDirectoryHandle, name: string, options?: FileSystemGetFileOptions) =>
39
+ Effect.tryPromise({
40
+ try: () => parent.getFileHandle(name, options),
41
+ catch: (u) =>
42
+ WebError.parseWebError(u, [
43
+ WebError.NotAllowedError,
44
+ WebError.TypeError,
45
+ WebError.TypeMismatchError,
46
+ WebError.NotFoundError,
47
+ ]),
48
+ })
49
+
50
+ /**
51
+ * Resolve (and optionally create) a directory handle relative to another directory.
52
+ *
53
+ * @param parent - Directory to search.
54
+ * @param name - Target directory name.
55
+ * @param options - Forwarded `getDirectoryHandle` options such as `{ create: true }`.
56
+ *
57
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/FileSystemDirectoryHandle/getDirectoryHandle | MDN Reference}
58
+ */
59
+ const getDirectoryHandle = (
60
+ parent: FileSystemDirectoryHandle,
61
+ name: string,
62
+ options?: FileSystemGetDirectoryOptions,
63
+ ) =>
64
+ Effect.tryPromise({
65
+ try: () => parent.getDirectoryHandle(name, options),
66
+ catch: (u) =>
67
+ WebError.parseWebError(u, [
68
+ WebError.NotAllowedError,
69
+ WebError.TypeError,
70
+ WebError.TypeMismatchError,
71
+ WebError.NotFoundError,
72
+ ]),
73
+ })
74
+
75
+ /**
76
+ * Remove a file-system entry (file or directory) from its parent directory.
77
+ *
78
+ * @param parent - Directory containing the entry.
79
+ * @param name - Entry name.
80
+ * @param options - Removal behavior (for example `{ recursive: true }`).
81
+ *
82
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/FileSystemDirectoryHandle/removeEntry | MDN Reference}
83
+ */
84
+ const removeEntry = (parent: FileSystemDirectoryHandle, name: string, options?: FileSystemRemoveOptions) =>
85
+ Effect.tryPromise({
86
+ try: () => parent.removeEntry(name, options),
87
+ catch: (u) =>
88
+ WebError.parseWebError(u, [
89
+ WebError.TypeError,
90
+ WebError.NotAllowedError,
91
+ WebError.InvalidModificationError,
92
+ WebError.NotFoundError,
93
+ WebError.NoModificationAllowedError,
94
+ ]),
95
+ })
96
+
97
+ /**
98
+ * Return a stream of child file-system handles for a directory.
99
+ *
100
+ * @param directory - Directory whose children are to be streamed
101
+ * @returns `Stream` of `FileSystemHandle`
102
+ *
103
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/FileSystemDirectoryHandle/values | MDN Reference}
104
+ */
105
+ const values = (directory: FileSystemDirectoryHandle) =>
106
+ Stream.fromAsyncIterable(directory.values(), (u) =>
107
+ WebError.parseWebError(u, [WebError.NotAllowedError, WebError.NotFoundError]),
108
+ )
109
+
110
+ /**
111
+ * Resolve the relative path from a parent directory to a descendant handle.
112
+ *
113
+ * @param parent - Reference directory.
114
+ * @param child - File or directory handle within the parent hierarchy.
115
+ * @returns `Option.some(pathSegments)` when reachable, otherwise `Option.none()`.
116
+ *
117
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/FileSystemDirectoryHandle/resolve | MDN Reference}
118
+ */
119
+ const resolve = (parent: FileSystemDirectoryHandle, child: FileSystemHandle) =>
120
+ Effect.tryPromise({
121
+ try: () => parent.resolve(child),
122
+ catch: (u) => WebError.parseWebError(u),
123
+ }).pipe(Effect.map((path) => (path === null ? Option.none() : Option.some(path))))
124
+
125
+ /**
126
+ * Read the underlying `File` for a file handle.
127
+ *
128
+ * @param handle - Handle referencing the target file.
129
+ *
130
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/FileSystemFileHandle/getFile | MDN Reference}
131
+ */
132
+ const getFile = (handle: FileSystemFileHandle) =>
133
+ Effect.tryPromise({
134
+ try: () => handle.getFile(),
135
+ catch: (u) => WebError.parseWebError(u, [WebError.NotAllowedError, WebError.NotFoundError]),
136
+ })
137
+
138
+ /**
139
+ * Overwrite the contents of a file with the provided data.
140
+ *
141
+ * @param handle - File to write to.
142
+ * @param data - Chunk(s) accepted by `FileSystemWritableFileStream.write`.
143
+ * @param options - Stream creation options (for example `{ keepExistingData: false }`).
144
+ *
145
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/FileSystemFileHandle/createWritable | MDN Reference}
146
+ */
147
+ const writeFile = (
148
+ handle: FileSystemFileHandle,
149
+ data: FileSystemWriteChunkType,
150
+ options?: FileSystemCreateWritableOptions,
151
+ ) =>
152
+ Effect.acquireUseRelease(
153
+ Effect.tryPromise({
154
+ try: () => handle.createWritable(options),
155
+ catch: (u) =>
156
+ WebError.parseWebError(u, [
157
+ WebError.NotAllowedError,
158
+ WebError.NotFoundError,
159
+ WebError.NoModificationAllowedError,
160
+ WebError.AbortError,
161
+ ]),
162
+ }),
163
+ (stream) =>
164
+ Effect.tryPromise({
165
+ try: () => stream.write(data),
166
+ catch: (u) =>
167
+ WebError.parseWebError(u, [WebError.NotAllowedError, WebError.QuotaExceededError, WebError.TypeError]),
168
+ }),
169
+ (stream) =>
170
+ Effect.tryPromise({
171
+ try: () => stream.close(),
172
+ catch: (u) => WebError.parseWebError(u, [WebError.TypeError]),
173
+ }).pipe(Effect.orElse(() => Effect.void)),
174
+ )
175
+
176
+ /**
177
+ * Append data to the end of an existing file.
178
+ *
179
+ * @param handle - File to extend.
180
+ * @param data - Data to append.
181
+ *
182
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/FileSystemWritableFileStream/write | MDN Reference}
183
+ */
184
+ const appendToFile = (handle: FileSystemFileHandle, data: FileSystemWriteChunkType) =>
185
+ Effect.acquireUseRelease(
186
+ Effect.tryPromise({
187
+ try: () => handle.createWritable({ keepExistingData: true }),
188
+ catch: (u) =>
189
+ WebError.parseWebError(u, [
190
+ WebError.NotAllowedError,
191
+ WebError.NotFoundError,
192
+ WebError.NoModificationAllowedError,
193
+ WebError.AbortError,
194
+ ]),
195
+ }),
196
+ (stream) =>
197
+ Effect.gen(function* () {
198
+ const file = yield* getFile(handle)
199
+ yield* Effect.tryPromise({
200
+ try: () => stream.seek(file.size),
201
+ catch: (u) => WebError.parseWebError(u, [WebError.NotAllowedError, WebError.TypeError]),
202
+ })
203
+ yield* Effect.tryPromise({
204
+ try: () => stream.write(data),
205
+ catch: (u) =>
206
+ WebError.parseWebError(u, [WebError.NotAllowedError, WebError.QuotaExceededError, WebError.TypeError]),
207
+ })
208
+ }),
209
+ (stream) =>
210
+ Effect.tryPromise({
211
+ try: () => stream.close(),
212
+ catch: (u) => WebError.parseWebError(u, [WebError.TypeError]),
213
+ }).pipe(Effect.orElse(() => Effect.void)),
214
+ )
215
+
216
+ /**
217
+ * Truncate a file to the specified size in bytes.
218
+ *
219
+ * @param handle - File to shrink or pad.
220
+ * @param size - Target byte length.
221
+ *
222
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/FileSystemWritableFileStream/truncate | MDN Reference}
223
+ */
224
+ const truncateFile = (handle: FileSystemFileHandle, size: number) =>
225
+ Effect.acquireUseRelease(
226
+ Effect.tryPromise({
227
+ try: () => handle.createWritable({ keepExistingData: true }),
228
+ catch: (u) =>
229
+ WebError.parseWebError(u, [
230
+ WebError.NotAllowedError,
231
+ WebError.NotFoundError,
232
+ WebError.NoModificationAllowedError,
233
+ WebError.AbortError,
234
+ ]),
235
+ }),
236
+ (stream) =>
237
+ Effect.tryPromise({
238
+ try: () => stream.truncate(size),
239
+ catch: (u) =>
240
+ WebError.parseWebError(u, [WebError.NotAllowedError, WebError.TypeError, WebError.QuotaExceededError]),
241
+ }),
242
+ (stream) =>
243
+ Effect.tryPromise({
244
+ try: () => stream.close(),
245
+ catch: (u) => WebError.parseWebError(u, [WebError.TypeError]),
246
+ }).pipe(Effect.orElse(() => Effect.void)),
247
+ )
248
+
249
+ /**
250
+ * Create a synchronous access handle for a file.
251
+ *
252
+ * @param handle - File handle to open.
253
+ * @returns A managed handle that is automatically closed when released.
254
+ *
255
+ * @remarks
256
+ * - Only available in Dedicated Web Workers.
257
+ * - This method is asynchronous even though the `FileSystemSyncAccessHandle` APIs are synchronous.
258
+ *
259
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/FileSystemFileHandle/createSyncAccessHandle | MDN Reference}
260
+ */
261
+ const createSyncAccessHandle = (handle: FileSystemFileHandle) =>
262
+ Effect.acquireRelease(
263
+ Effect.tryPromise({
264
+ try: () => handle.createSyncAccessHandle(),
265
+ catch: (u) =>
266
+ WebError.parseWebError(u, [
267
+ WebError.NotAllowedError,
268
+ WebError.InvalidStateError,
269
+ WebError.NotFoundError,
270
+ WebError.NoModificationAllowedError,
271
+ ]),
272
+ }),
273
+ (syncHandle) => Effect.sync(() => syncHandle.close()),
274
+ )
275
+
276
+ /**
277
+ * Perform a synchronous read into the provided buffer from a sync access handle.
278
+ *
279
+ * @param handle - Sync access handle to read from.
280
+ * @param buffer - Destination buffer (can be a specific view like Uint8Array).
281
+ * @param options - Read position options.
282
+ * @returns Number of bytes read.
283
+ *
284
+ * @remarks
285
+ * Only available in Dedicated Web Workers.
286
+ *
287
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/FileSystemSyncAccessHandle/read | MDN Reference}
288
+ */
289
+ const syncRead = (
290
+ handle: FileSystemSyncAccessHandle,
291
+ buffer: ArrayBuffer | ArrayBufferView,
292
+ options?: FileSystemReadWriteOptions,
293
+ ) =>
294
+ Effect.try({
295
+ try: () => handle.read(buffer, options),
296
+ catch: (u) => WebError.parseWebError(u, [WebError.RangeError, WebError.InvalidStateError, WebError.TypeError]),
297
+ })
298
+
299
+ /**
300
+ * Perform a synchronous write from the provided buffer into the file.
301
+ *
302
+ * @param handle - Sync access handle to write to.
303
+ * @param buffer - Source data.
304
+ * @param options - Write position options.
305
+ * @returns Number of bytes written.
306
+ *
307
+ * @remarks
308
+ * Only available in Dedicated Web Workers.
309
+ *
310
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/FileSystemSyncAccessHandle/write | MDN Reference}
311
+ */
312
+ const syncWrite = (
313
+ handle: FileSystemSyncAccessHandle,
314
+ buffer: AllowSharedBufferSource,
315
+ options?: FileSystemReadWriteOptions,
316
+ ) =>
317
+ Effect.try({
318
+ try: () => handle.write(buffer, options),
319
+ catch: (u) => WebError.parseWebError(u),
320
+ })
321
+
322
+ /**
323
+ * Truncate the file associated with a sync access handle to the specified size.
324
+ *
325
+ * @param handle - Sync access handle to mutate.
326
+ * @param size - Desired byte length.
327
+ *
328
+ * @remarks
329
+ * Only available in Dedicated Web Workers.
330
+ *
331
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/FileSystemSyncAccessHandle/truncate | MDN Reference}
332
+ */
333
+ const syncTruncate = (handle: FileSystemSyncAccessHandle, size: number) =>
334
+ Effect.try({
335
+ try: () => handle.truncate(size),
336
+ catch: (u) => WebError.parseWebError(u),
337
+ })
338
+
339
+ /**
340
+ * Retrieve the current size of a file via its sync access handle.
341
+ *
342
+ * @param handle - Sync access handle.
343
+ * @returns File size in bytes.
344
+ *
345
+ * @remarks
346
+ * Only available in Dedicated Web Workers.
347
+ *
348
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/FileSystemSyncAccessHandle/getSize | MDN Reference}
349
+ */
350
+ const syncGetSize = (handle: FileSystemSyncAccessHandle) =>
351
+ Effect.try({
352
+ try: () => handle.getSize(),
353
+ catch: (u) => WebError.parseWebError(u),
354
+ })
355
+
356
+ /**
357
+ * Flush pending synchronous writes to durable storage.
358
+ *
359
+ * @param handle - Sync access handle to flush.
360
+ *
361
+ * @remarks
362
+ * Only available in Dedicated Web Workers.
363
+ *
364
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/FileSystemSyncAccessHandle/flush | MDN Reference}
365
+ */
366
+ const syncFlush = (handle: FileSystemSyncAccessHandle) =>
367
+ Effect.try({
368
+ try: () => handle.flush(),
369
+ catch: (u) => WebError.parseWebError(u),
370
+ })
371
+
372
+ return {
373
+ getRootDirectoryHandle,
374
+ getFileHandle,
375
+ getDirectoryHandle,
376
+ removeEntry,
377
+ values,
378
+ resolve,
379
+ getFile,
380
+ writeFile,
381
+ appendToFile,
382
+ truncateFile,
383
+ createSyncAccessHandle,
384
+ syncRead,
385
+ syncWrite,
386
+ syncTruncate,
387
+ syncGetSize,
388
+ syncFlush,
389
+ } as const
390
+ },
391
+ accessors: true,
392
+ }) {}
393
+
394
+ const notFoundError = new WebError.NotFoundError({
395
+ cause: new DOMException('The object can not be found here.', 'NotFoundError'),
396
+ })
397
+
398
+ const unknownError = (message: string) => new WebError.UnknownError({ description: message })
399
+
400
+ /**
401
+ * A no-op Opfs service that can be used for testing.
402
+ */
403
+ export const noopOpfs = new Opfs({
404
+ getRootDirectoryHandle: Effect.fail(unknownError('OPFS is not supported in this environment')),
405
+ getFileHandle: () => Effect.fail(notFoundError),
406
+ getDirectoryHandle: () => Effect.fail(notFoundError),
407
+ removeEntry: () => Effect.fail(notFoundError),
408
+ values: () => Effect.fail(notFoundError),
409
+ resolve: () => Effect.succeed(Option.none()),
410
+ getFile: () => Effect.fail(notFoundError),
411
+ writeFile: () => Effect.fail(notFoundError),
412
+ appendToFile: () => Effect.fail(notFoundError),
413
+ truncateFile: () => Effect.fail(notFoundError),
414
+ createSyncAccessHandle: () => Effect.fail(unknownError('OPFS is not supported in this environment')),
415
+ syncRead: () => Effect.fail(unknownError('OPFS is not supported in this environment')),
416
+ syncWrite: () => Effect.fail(unknownError('OPFS is not supported in this environment')),
417
+ syncTruncate: () => Effect.fail(unknownError('OPFS is not supported in this environment')),
418
+ syncGetSize: () => Effect.fail(unknownError('OPFS is not supported in this environment')),
419
+ syncFlush: () => Effect.fail(unknownError('OPFS is not supported in this environment')),
420
+ })
421
+
422
+ /**
423
+ * Error raised when OPFS operations fail.
424
+ */
425
+ export class OpfsError extends Schema.TaggedError<OpfsError>()('@livestore/utils/Opfs/Error', {
426
+ message: Schema.String,
427
+ cause: Schema.optional(Schema.Defect),
428
+ }) {}
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Debug utilities for OPFS (Origin Private File System) inspection and manipulation.
3
+ * These functions are designed for use in browser devtools console to help debug
4
+ * and inspect the OPFS structure during development.
5
+ */
6
+
7
+ import { Effect, Stream } from 'effect'
8
+ import prettyBytes from 'pretty-bytes'
9
+ import { Opfs } from './Opfs.ts'
10
+ import { getDirectoryHandleByPath, getMetadata, remove } from './utils.ts'
11
+
12
+ /**
13
+ * Metadata exposed directly on OPFS handles so we avoid `getFile()` reads.
14
+ */
15
+ interface OpfsEntryMetadata {
16
+ readonly name: string
17
+ readonly path: string
18
+ readonly kind: FileSystemHandleKind
19
+ readonly size?: number
20
+ readonly lastModified?: number
21
+ }
22
+
23
+ interface OpfsTreeNode {
24
+ readonly metadata: OpfsEntryMetadata
25
+ readonly children?: ReadonlyArray<OpfsTreeNode>
26
+ }
27
+
28
+ const ROOT_NAME = '/'
29
+
30
+ /**
31
+ * Materialize the entire OPFS tree starting from the origin root.
32
+ */
33
+ const buildTree = Effect.fn('@livestore/utils:Opfs.buildTree')(function* () {
34
+ const rootHandle = yield* Opfs.getRootDirectoryHandle
35
+
36
+ const collectDirectory: (
37
+ handle: FileSystemDirectoryHandle,
38
+ pathSegments: ReadonlyArray<string>,
39
+ ) => Effect.Effect<OpfsTreeNode, unknown, Opfs> = (handle, pathSegments) =>
40
+ Effect.gen(function* () {
41
+ const handlesStream = yield* Opfs.values(handle)
42
+ const handles = yield* handlesStream.pipe(
43
+ Stream.runCollect,
44
+ Effect.map((chunk) => Array.from(chunk).sort((a, b) => a.name.localeCompare(b.name))),
45
+ )
46
+
47
+ const children = yield* Effect.forEach(
48
+ handles,
49
+ (childHandle) =>
50
+ Effect.gen(function* () {
51
+ const nextSegments = [...pathSegments, childHandle.name]
52
+ const path = formatPath(nextSegments)
53
+
54
+ if (childHandle.kind === 'directory') {
55
+ return yield* collectDirectory(childHandle as FileSystemDirectoryHandle, nextSegments)
56
+ }
57
+
58
+ const metadata = yield* getMetadata(childHandle as FileSystemFileHandle)
59
+
60
+ return {
61
+ metadata: {
62
+ name: childHandle.name,
63
+ path,
64
+ kind: 'file',
65
+ size: metadata.size,
66
+ lastModified: metadata.lastModified,
67
+ },
68
+ } satisfies OpfsTreeNode
69
+ }),
70
+ { concurrency: 'unbounded' },
71
+ )
72
+
73
+ return {
74
+ metadata: {
75
+ name: pathSegments.length === 0 ? ROOT_NAME : pathSegments[pathSegments.length - 1]!,
76
+ path: formatPath(pathSegments),
77
+ kind: 'directory',
78
+ },
79
+ children,
80
+ }
81
+ })
82
+
83
+ return yield* collectDirectory(rootHandle, [])
84
+ })
85
+
86
+ const formatPath = (segments: ReadonlyArray<string>) => (segments.length === 0 ? ROOT_NAME : `/${segments.join('/')}`)
87
+
88
+ const formatLabel = ({ name, kind, size, lastModified }: OpfsEntryMetadata) => {
89
+ let label = name
90
+
91
+ if (kind === 'file' && lastModified !== undefined) {
92
+ const date = new Date(lastModified)
93
+ label += ` │ ${date.toISOString().split('T')[0]} ${date.toTimeString().split(' ')[0]}`
94
+
95
+ if (size !== undefined) {
96
+ label += ` │ ${prettyBytes(size)}`
97
+ }
98
+ }
99
+
100
+ return label
101
+ }
102
+
103
+ const logAsciiTree = (node: OpfsTreeNode): Effect.Effect<void, never, never> =>
104
+ logAsciiNode(node, { prefix: '', isLast: true, isRoot: true })
105
+
106
+ const logAsciiNode: (
107
+ node: OpfsTreeNode,
108
+ options: { readonly prefix: string; readonly isLast: boolean; readonly isRoot?: boolean },
109
+ ) => Effect.Effect<void, never, never> = (node, options) =>
110
+ Effect.gen(function* () {
111
+ const label = formatLabel(node.metadata)
112
+ const branch = options.isRoot ? '' : `${options.prefix}${options.isLast ? '└── ' : '├── '}`
113
+ const nextPrefix = options.isRoot ? '' : `${options.prefix}${options.isLast ? ' ' : '│ '}`
114
+
115
+ console.log(`${branch}${label}`)
116
+
117
+ if (node.children === undefined || node.children.length === 0) return
118
+
119
+ for (let index = 0; index < node.children.length; index++) {
120
+ const child = node.children[index]!
121
+ const isLastChild = index === node.children.length - 1
122
+ yield* logAsciiNode(child, { prefix: nextPrefix, isLast: isLastChild })
123
+ }
124
+ })
125
+
126
+ const printTree = Effect.gen(function* () {
127
+ const tree = yield* buildTree()
128
+ yield* logAsciiTree(tree)
129
+ })
130
+
131
+ const resetTree = remove('/')
132
+
133
+ const getDirHandle = (path: string, options?: FileSystemGetDirectoryOptions) => getDirectoryHandleByPath(path, options)
134
+
135
+ const runOpfsEffect = <A, E>(effect: Effect.Effect<A, E, Opfs>) =>
136
+ effect.pipe(Effect.provide(Opfs.Default), Effect.runPromise)
137
+
138
+ export const debugUtils = {
139
+ /**
140
+ * Print the entire OPFS tree structure to the console in an ASCII format.
141
+ */
142
+ printTree: (): Promise<void> => runOpfsEffect(printTree),
143
+ /**
144
+ * Reset the entire OPFS tree by removing all files and directories.
145
+ */
146
+ resetTree: (): Promise<void> => runOpfsEffect(resetTree),
147
+ /**
148
+ * Get a directory handle for a given path, useful for inspecting or manipulating specific directories.
149
+ */
150
+ getDirHandle: (path: string, options?: FileSystemGetDirectoryOptions) => runOpfsEffect(getDirHandle(path, options)),
151
+ } as const
@@ -0,0 +1,3 @@
1
+ export * from './debug-utils.ts'
2
+ export * from './Opfs.ts'
3
+ export * from './utils.ts'