@componentor/fs 1.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/errors.ts ADDED
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Custom error class for filesystem errors
3
+ */
4
+ export class FSError extends Error {
5
+ code: string
6
+ syscall?: string
7
+ path?: string
8
+ original?: Error
9
+
10
+ constructor(message: string, code: string, options?: { syscall?: string; path?: string; original?: Error }) {
11
+ super(message)
12
+ this.name = 'FSError'
13
+ this.code = code
14
+ this.syscall = options?.syscall
15
+ this.path = options?.path
16
+ this.original = options?.original
17
+ }
18
+ }
19
+
20
+ /**
21
+ * Create ENOENT (No such file or directory) error
22
+ */
23
+ export function createENOENT(path: string): FSError {
24
+ return new FSError(`ENOENT: No such file or directory, '${path}'`, 'ENOENT', { path })
25
+ }
26
+
27
+ /**
28
+ * Create EEXIST (File exists) error
29
+ */
30
+ export function createEEXIST(path: string, operation?: string): FSError {
31
+ const message = operation
32
+ ? `EEXIST: file already exists, ${operation} '${path}'`
33
+ : `EEXIST: File exists, '${path}'`
34
+ return new FSError(message, 'EEXIST', { path })
35
+ }
36
+
37
+ /**
38
+ * Create EACCES (Permission denied) error
39
+ */
40
+ export function createEACCES(path: string, syscall?: string): FSError {
41
+ return new FSError(`EACCES: permission denied, access '${path}'`, 'EACCES', { syscall, path })
42
+ }
43
+
44
+ /**
45
+ * Create EISDIR (Is a directory) error
46
+ */
47
+ export function createEISDIR(path: string, operation = 'operation'): FSError {
48
+ return new FSError(`EISDIR: illegal operation on a directory, ${operation} '${path}'`, 'EISDIR', { path })
49
+ }
50
+
51
+ /**
52
+ * Create ELOOP (Too many symbolic links) error
53
+ */
54
+ export function createELOOP(path: string): FSError {
55
+ return new FSError(`ELOOP: Too many symbolic links, '${path}'`, 'ELOOP', { path })
56
+ }
57
+
58
+ /**
59
+ * Create EINVAL (Invalid argument) error
60
+ */
61
+ export function createEINVAL(path: string): FSError {
62
+ return new FSError(`EINVAL: Invalid argument, '${path}'`, 'EINVAL', { path })
63
+ }
64
+
65
+ /**
66
+ * Create ECORRUPTED (Data corruption detected) error
67
+ */
68
+ export function createECORRUPTED(path: string): FSError {
69
+ return new FSError(`ECORRUPTED: Pack file integrity check failed, '${path}'`, 'ECORRUPTED', { path })
70
+ }
71
+
72
+ /**
73
+ * Wrap an error with a standard code if it doesn't have one
74
+ */
75
+ export function wrapError(err: unknown): FSError {
76
+ if (err instanceof FSError) return err
77
+
78
+ const error = err as Error
79
+ if (typeof (error as FSError).code === 'string') {
80
+ const fsErr = new FSError(error.message, (error as FSError).code)
81
+ fsErr.original = error
82
+ return fsErr
83
+ }
84
+
85
+ const wrapped = new FSError(error.message || 'Unknown error', 'UNKNOWN')
86
+ wrapped.original = error
87
+ return wrapped
88
+ }
@@ -0,0 +1,100 @@
1
+ import type { FileHandle, ReadResult, WriteResult, Stats, ReadFileOptions, WriteFileOptions } from './types.js'
2
+
3
+ export interface FileHandleContext {
4
+ readFile(path: string, options?: ReadFileOptions): Promise<string | Uint8Array>
5
+ writeFile(path: string, data: string | Uint8Array, options?: WriteFileOptions): Promise<void>
6
+ appendFile(path: string, data: string | Uint8Array, options?: WriteFileOptions): Promise<void>
7
+ stat(path: string): Promise<Stats>
8
+ truncate(path: string, len: number): Promise<void>
9
+ }
10
+
11
+ /**
12
+ * Create a FileHandle-like object for the open() method
13
+ */
14
+ export function createFileHandle(
15
+ resolvedPath: string,
16
+ initialPosition: number,
17
+ context: FileHandleContext
18
+ ): FileHandle {
19
+ let position = initialPosition
20
+
21
+ return {
22
+ fd: Math.floor(Math.random() * 1000000),
23
+
24
+ async read(
25
+ buffer: Uint8Array,
26
+ offset = 0,
27
+ length = buffer.length,
28
+ pos: number | null = null
29
+ ): Promise<ReadResult> {
30
+ const readPos = pos !== null ? pos : position
31
+ const data = await context.readFile(resolvedPath) as Uint8Array
32
+ const bytesToRead = Math.min(length, data.length - readPos)
33
+ buffer.set(data.subarray(readPos, readPos + bytesToRead), offset)
34
+ if (pos === null) position += bytesToRead
35
+ return { bytesRead: bytesToRead, buffer }
36
+ },
37
+
38
+ async write(
39
+ buffer: Uint8Array,
40
+ offset = 0,
41
+ length = buffer.length,
42
+ pos: number | null = null
43
+ ): Promise<WriteResult> {
44
+ const writePos = pos !== null ? pos : position
45
+ let existingData = new Uint8Array(0)
46
+
47
+ try {
48
+ existingData = await context.readFile(resolvedPath) as Uint8Array
49
+ } catch (e) {
50
+ if ((e as { code?: string }).code !== 'ENOENT') throw e
51
+ }
52
+
53
+ const dataToWrite = buffer.subarray(offset, offset + length)
54
+ const newSize = Math.max(existingData.length, writePos + length)
55
+ const newData = new Uint8Array(newSize)
56
+ newData.set(existingData, 0)
57
+ newData.set(dataToWrite, writePos)
58
+
59
+ await context.writeFile(resolvedPath, newData)
60
+ if (pos === null) position += length
61
+ return { bytesWritten: length, buffer }
62
+ },
63
+
64
+ async close(): Promise<void> {
65
+ // No-op for OPFS
66
+ },
67
+
68
+ async stat(): Promise<Stats> {
69
+ return context.stat(resolvedPath)
70
+ },
71
+
72
+ async truncate(len = 0): Promise<void> {
73
+ return context.truncate(resolvedPath, len)
74
+ },
75
+
76
+ async sync(): Promise<void> {
77
+ // No-op for OPFS (writes are already persisted)
78
+ },
79
+
80
+ async datasync(): Promise<void> {
81
+ // No-op for OPFS
82
+ },
83
+
84
+ async readFile(options?: ReadFileOptions): Promise<string | Uint8Array> {
85
+ return context.readFile(resolvedPath, options)
86
+ },
87
+
88
+ async writeFile(data: string | Uint8Array, options?: WriteFileOptions): Promise<void> {
89
+ return context.writeFile(resolvedPath, data, options)
90
+ },
91
+
92
+ async appendFile(data: string | Uint8Array, options?: WriteFileOptions): Promise<void> {
93
+ return context.appendFile(resolvedPath, data, options)
94
+ },
95
+
96
+ [Symbol.asyncDispose]: async function(): Promise<void> {
97
+ // No-op for OPFS
98
+ }
99
+ }
100
+ }
@@ -0,0 +1,57 @@
1
+ // File System Access API type declarations
2
+
3
+ interface FileSystemSyncAccessHandle {
4
+ read(buffer: ArrayBufferView, options?: { at?: number }): number
5
+ write(buffer: ArrayBufferView, options?: { at?: number }): number
6
+ truncate(newSize: number): void
7
+ getSize(): number
8
+ flush(): void
9
+ close(): void
10
+ }
11
+
12
+ interface FileSystemFileHandle {
13
+ readonly kind: 'file'
14
+ readonly name: string
15
+ getFile(): Promise<File>
16
+ createWritable(options?: { keepExistingData?: boolean }): Promise<FileSystemWritableFileStream>
17
+ createSyncAccessHandle(): Promise<FileSystemSyncAccessHandle>
18
+ }
19
+
20
+ interface FileSystemDirectoryHandle {
21
+ readonly kind: 'directory'
22
+ readonly name: string
23
+ getFileHandle(name: string, options?: { create?: boolean }): Promise<FileSystemFileHandle>
24
+ getDirectoryHandle(name: string, options?: { create?: boolean }): Promise<FileSystemDirectoryHandle>
25
+ removeEntry(name: string, options?: { recursive?: boolean }): Promise<void>
26
+ resolve(possibleDescendant: FileSystemHandle): Promise<string[] | null>
27
+ entries(): AsyncIterableIterator<[string, FileSystemHandle]>
28
+ keys(): AsyncIterableIterator<string>
29
+ values(): AsyncIterableIterator<FileSystemHandle>
30
+ [Symbol.asyncIterator](): AsyncIterableIterator<[string, FileSystemHandle]>
31
+ }
32
+
33
+ interface FileSystemWritableFileStream extends WritableStream {
34
+ write(data: ArrayBuffer | ArrayBufferView | Blob | string | WriteParams): Promise<void>
35
+ seek(position: number): Promise<void>
36
+ truncate(size: number): Promise<void>
37
+ }
38
+
39
+ interface WriteParams {
40
+ type: 'write' | 'seek' | 'truncate'
41
+ data?: ArrayBuffer | ArrayBufferView | Blob | string
42
+ position?: number
43
+ size?: number
44
+ }
45
+
46
+ type FileSystemHandle = FileSystemFileHandle | FileSystemDirectoryHandle
47
+
48
+ interface StorageManager {
49
+ getDirectory(): Promise<FileSystemDirectoryHandle>
50
+ estimate(): Promise<StorageEstimate>
51
+ persist(): Promise<boolean>
52
+ persisted(): Promise<boolean>
53
+ }
54
+
55
+ interface Navigator {
56
+ storage: StorageManager
57
+ }
@@ -0,0 +1,250 @@
1
+ import { normalize, segments, dirname } from './path-utils.js'
2
+ import { createENOENT } from './errors.js'
3
+
4
+ export interface HandleResult {
5
+ dir: FileSystemDirectoryHandle
6
+ name: string
7
+ fileHandle: FileSystemFileHandle | null
8
+ dirHandle: FileSystemDirectoryHandle | null
9
+ }
10
+
11
+ export interface GetHandleOptions {
12
+ create?: boolean
13
+ kind?: 'file' | 'directory'
14
+ }
15
+
16
+ const FILE_HANDLE_POOL_SIZE = 50
17
+ const DIR_CACHE_MAX_SIZE = 200
18
+
19
+ /**
20
+ * Manages OPFS handles with caching for improved performance
21
+ */
22
+ export class HandleManager {
23
+ private rootPromise: Promise<FileSystemDirectoryHandle>
24
+ private dirCache: Map<string, FileSystemDirectoryHandle> = new Map()
25
+ private fileHandlePool: Map<string, FileSystemFileHandle> = new Map()
26
+
27
+ constructor() {
28
+ this.rootPromise = navigator.storage.getDirectory()
29
+ }
30
+
31
+ /**
32
+ * Get the root directory handle
33
+ */
34
+ async getRoot(): Promise<FileSystemDirectoryHandle> {
35
+ return this.rootPromise
36
+ }
37
+
38
+ /**
39
+ * Cache a directory handle with LRU eviction
40
+ */
41
+ private cacheDirHandle(path: string, handle: FileSystemDirectoryHandle): void {
42
+ if (this.dirCache.size >= DIR_CACHE_MAX_SIZE) {
43
+ // Delete oldest entry (first key in Map maintains insertion order)
44
+ const firstKey = this.dirCache.keys().next().value
45
+ if (firstKey) this.dirCache.delete(firstKey)
46
+ }
47
+ this.dirCache.set(path, handle)
48
+ }
49
+
50
+ /**
51
+ * Clear directory cache for a path and its children
52
+ */
53
+ clearCache(path = ''): void {
54
+ const normalizedPath = normalize(path)
55
+
56
+ // For root path, just clear everything
57
+ if (normalizedPath === '/' || normalizedPath === '') {
58
+ this.dirCache.clear()
59
+ this.fileHandlePool.clear()
60
+ return
61
+ }
62
+
63
+ // Clear directory cache
64
+ if (this.dirCache.size > 0) {
65
+ for (const key of this.dirCache.keys()) {
66
+ if (key === normalizedPath || key.startsWith(normalizedPath + '/')) {
67
+ this.dirCache.delete(key)
68
+ }
69
+ }
70
+ }
71
+
72
+ // Clear file handle pool for affected paths
73
+ if (this.fileHandlePool.size > 0) {
74
+ for (const key of this.fileHandlePool.keys()) {
75
+ if (key === normalizedPath || key.startsWith(normalizedPath + '/')) {
76
+ this.fileHandlePool.delete(key)
77
+ }
78
+ }
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Get a file handle from the pool or create a new one
84
+ */
85
+ async getPooledFileHandle(path: string, create = false): Promise<FileSystemFileHandle | null> {
86
+ const normalizedPath = normalize(path)
87
+
88
+ // Check pool first
89
+ const pooled = this.fileHandlePool.get(normalizedPath)
90
+ if (pooled) {
91
+ return pooled
92
+ }
93
+
94
+ // Get handle the normal way
95
+ const { fileHandle } = await this.getHandle(normalizedPath, { create })
96
+ if (!fileHandle) return null
97
+
98
+ // Add to pool with LRU eviction
99
+ if (this.fileHandlePool.size >= FILE_HANDLE_POOL_SIZE) {
100
+ // Delete oldest entry (first key in Map maintains insertion order)
101
+ const firstKey = this.fileHandlePool.keys().next().value
102
+ if (firstKey) this.fileHandlePool.delete(firstKey)
103
+ }
104
+ this.fileHandlePool.set(normalizedPath, fileHandle)
105
+
106
+ return fileHandle
107
+ }
108
+
109
+ /**
110
+ * Invalidate a specific file handle from the pool
111
+ */
112
+ invalidateFileHandle(path: string): void {
113
+ const normalizedPath = normalize(path)
114
+ this.fileHandlePool.delete(normalizedPath)
115
+ }
116
+
117
+ /**
118
+ * Get file or directory handle for a path
119
+ */
120
+ async getHandle(path: string, opts: GetHandleOptions = {}): Promise<HandleResult> {
121
+ // Use segments() for optimized path parsing (leverages normalize cache)
122
+ const parts = segments(path)
123
+
124
+ // Handle root or empty path
125
+ if (parts.length === 0) {
126
+ const root = await this.rootPromise
127
+ return { dir: root, name: '', fileHandle: null, dirHandle: root }
128
+ }
129
+
130
+ let dir = await this.rootPromise
131
+ let currentPath = ''
132
+
133
+ // Navigate to parent directory using cache
134
+ for (let i = 0; i < parts.length - 1; i++) {
135
+ currentPath += '/' + parts[i]
136
+
137
+ // Check cache first for better performance
138
+ if (this.dirCache.has(currentPath)) {
139
+ dir = this.dirCache.get(currentPath)!
140
+ continue
141
+ }
142
+
143
+ try {
144
+ dir = await dir.getDirectoryHandle(parts[i], { create: opts.create })
145
+ this.cacheDirHandle(currentPath, dir)
146
+ } catch {
147
+ throw createENOENT(path)
148
+ }
149
+ }
150
+
151
+ const name = parts[parts.length - 1]
152
+
153
+ try {
154
+ if (opts.kind === 'directory') {
155
+ const dirHandle = await dir.getDirectoryHandle(name, { create: opts.create })
156
+ return { dir, name, fileHandle: null, dirHandle }
157
+ } else {
158
+ const fileHandle = await dir.getFileHandle(name, { create: opts.create })
159
+ return { dir, name, fileHandle, dirHandle: null }
160
+ }
161
+ } catch {
162
+ if (!opts.create) {
163
+ return { dir, name, fileHandle: null, dirHandle: null }
164
+ }
165
+ throw createENOENT(path)
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Get directory handle with caching
171
+ */
172
+ async getDirectoryHandle(path: string): Promise<FileSystemDirectoryHandle> {
173
+ const normalizedPath = normalize(path)
174
+
175
+ if (normalizedPath === '/' || normalizedPath === '') {
176
+ return this.rootPromise
177
+ }
178
+
179
+ // Check cache first
180
+ if (this.dirCache.has(normalizedPath)) {
181
+ return this.dirCache.get(normalizedPath)!
182
+ }
183
+
184
+ const parts = segments(normalizedPath)
185
+ let dir = await this.rootPromise
186
+ let currentPath = ''
187
+
188
+ for (const part of parts) {
189
+ currentPath += '/' + part
190
+
191
+ if (this.dirCache.has(currentPath)) {
192
+ dir = this.dirCache.get(currentPath)!
193
+ continue
194
+ }
195
+
196
+ dir = await dir.getDirectoryHandle(part)
197
+ this.cacheDirHandle(currentPath, dir)
198
+ }
199
+
200
+ return dir
201
+ }
202
+
203
+ /**
204
+ * Ensure parent directory exists
205
+ */
206
+ async ensureParentDir(path: string): Promise<void> {
207
+ const parentPath = dirname(path)
208
+ if (parentPath === '/' || parentPath === '') return
209
+
210
+ const parts = segments(parentPath)
211
+ let dir = await this.rootPromise
212
+ let currentPath = ''
213
+
214
+ for (const part of parts) {
215
+ currentPath += '/' + part
216
+
217
+ // Check cache first for better performance
218
+ if (this.dirCache.has(currentPath)) {
219
+ dir = this.dirCache.get(currentPath)!
220
+ continue
221
+ }
222
+
223
+ dir = await dir.getDirectoryHandle(part, { create: true })
224
+ this.cacheDirHandle(currentPath, dir)
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Create directory (with automatic parent creation)
230
+ */
231
+ async mkdir(path: string): Promise<void> {
232
+ const normalizedPath = normalize(path)
233
+ this.clearCache(normalizedPath)
234
+
235
+ const parts = segments(normalizedPath)
236
+ let dir = await this.rootPromise
237
+
238
+ for (let i = 0; i < parts.length; i++) {
239
+ const part = parts[i]
240
+ const subPath = '/' + parts.slice(0, i + 1).join('/')
241
+
242
+ if (this.dirCache.has(subPath)) {
243
+ dir = this.dirCache.get(subPath)!
244
+ } else {
245
+ dir = await dir.getDirectoryHandle(part, { create: true })
246
+ this.cacheDirHandle(subPath, dir)
247
+ }
248
+ }
249
+ }
250
+ }