@zenfs/dom 1.1.2 → 1.1.4

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.
@@ -1,27 +1,27 @@
1
- import type { AsyncMixin, SharedConfig, Store } from '@zenfs/core';
1
+ import type { SharedConfig, Store } from '@zenfs/core';
2
2
  import { AsyncTransaction, StoreFS } from '@zenfs/core';
3
+ import type * as cache from 'utilium/cache.js';
3
4
  /**
4
- * @hidden
5
+ * @internal @hidden
5
6
  */
6
7
  export declare class IndexedDBTransaction extends AsyncTransaction<IndexedDBStore> {
7
8
  tx: IDBTransaction;
8
9
  store: IndexedDBStore;
9
10
  private _idb;
10
11
  constructor(tx: IDBTransaction, store: IndexedDBStore);
11
- keys(): Promise<Iterable<bigint>>;
12
- get(key: bigint): Promise<Uint8Array>;
13
- set(key: bigint, data: Uint8Array): Promise<void>;
14
- remove(key: bigint): Promise<void>;
12
+ keys(): Promise<Iterable<number>>;
13
+ get(id: number): Promise<Uint8Array | undefined>;
14
+ set(id: number, data: Uint8Array): Promise<void>;
15
+ remove(id: number): Promise<void>;
15
16
  commit(): Promise<void>;
16
17
  abort(): Promise<void>;
17
18
  }
18
19
  export declare class IndexedDBStore implements Store {
19
20
  protected db: IDBDatabase;
21
+ cache: Map<number, cache.Resource<number>>;
20
22
  constructor(db: IDBDatabase);
21
23
  sync(): Promise<void>;
22
24
  get name(): string;
23
- clear(): Promise<void>;
24
- clearSync(): void;
25
25
  transaction(): IndexedDBTransaction;
26
26
  }
27
27
  /**
@@ -53,7 +53,7 @@ declare const _IndexedDB: {
53
53
  };
54
54
  };
55
55
  readonly isAvailable: (idbFactory?: IDBFactory) => Promise<boolean>;
56
- readonly create: (options: IndexedDBOptions & Partial<SharedConfig>) => Promise<AsyncMixin & StoreFS<IndexedDBStore>>;
56
+ readonly create: (options: IndexedDBOptions & Partial<SharedConfig>) => Promise<StoreFS<IndexedDBStore>>;
57
57
  };
58
58
  type _IndexedDB = typeof _IndexedDB;
59
59
  export interface IndexedDB extends _IndexedDB {
package/dist/IndexedDB.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Async, AsyncTransaction, ErrnoError, InMemory, StoreFS } from '@zenfs/core';
1
+ import { AsyncTransaction, StoreFS, log } from '@zenfs/core';
2
2
  import { convertException } from './utils.js';
3
3
  function wrap(request) {
4
4
  return new Promise((resolve, reject) => {
@@ -10,7 +10,7 @@ function wrap(request) {
10
10
  });
11
11
  }
12
12
  /**
13
- * @hidden
13
+ * @internal @hidden
14
14
  */
15
15
  export class IndexedDBTransaction extends AsyncTransaction {
16
16
  constructor(tx, store) {
@@ -20,33 +20,30 @@ export class IndexedDBTransaction extends AsyncTransaction {
20
20
  this._idb = tx.objectStore(store.name);
21
21
  }
22
22
  async keys() {
23
- return (await wrap(this._idb.getAllKeys())).filter(k => typeof k == 'string').map(k => BigInt(k));
23
+ return (await wrap(this._idb.getAllKeys())).filter(k => typeof k == 'string').map(k => Number(k));
24
24
  }
25
- get(key) {
26
- return wrap(this._idb.get(key.toString()));
25
+ async get(id) {
26
+ const data = await wrap(this._idb.get(id.toString()));
27
+ if (data)
28
+ this._cached(id, { size: data.byteLength }).add(data, 0);
29
+ return data;
27
30
  }
28
- async set(key, data) {
29
- await wrap(this._idb.put(data, key.toString()));
31
+ async set(id, data) {
32
+ this._cached(id, { size: data.byteLength }).add(data, 0);
33
+ await wrap(this._idb.put(data, id.toString()));
30
34
  }
31
- remove(key) {
32
- return wrap(this._idb.delete(key.toString()));
35
+ remove(id) {
36
+ this.store.cache.delete(id);
37
+ return wrap(this._idb.delete(id.toString()));
33
38
  }
34
39
  async commit() {
35
- if (this.done) {
36
- return;
37
- }
38
40
  const { promise, resolve, reject } = Promise.withResolvers();
39
- this.done = true;
40
41
  this.tx.oncomplete = () => resolve();
41
42
  this.tx.onerror = () => reject(convertException(this.tx.error));
42
43
  this.tx.commit();
43
44
  return promise;
44
45
  }
45
46
  async abort() {
46
- if (this.done) {
47
- return;
48
- }
49
- this.done = true;
50
47
  const { promise, resolve, reject } = Promise.withResolvers();
51
48
  this.tx.onabort = () => resolve();
52
49
  this.tx.onerror = () => reject(convertException(this.tx.error));
@@ -60,29 +57,24 @@ async function createDB(name, indexedDB = globalThis.indexedDB) {
60
57
  const db = req.result;
61
58
  // This should never happen; we're at version 1. Why does another database exist?
62
59
  if (db.objectStoreNames.contains(name)) {
60
+ log.warn('Found unexpected object store: ' + name);
63
61
  db.deleteObjectStore(name);
64
62
  }
65
63
  db.createObjectStore(name);
66
64
  };
67
- const result = await wrap(req);
68
- return result;
65
+ return await wrap(req);
69
66
  }
70
67
  export class IndexedDBStore {
71
68
  constructor(db) {
72
69
  this.db = db;
70
+ this.cache = new Map();
73
71
  }
74
72
  sync() {
75
- throw ErrnoError.With('ENOSYS', undefined, 'IndexedDBStore.sync');
73
+ return Promise.resolve();
76
74
  }
77
75
  get name() {
78
76
  return this.db.name;
79
77
  }
80
- clear() {
81
- return wrap(this.db.transaction(this.name, 'readwrite').objectStore(this.name).clear());
82
- }
83
- clearSync() {
84
- throw ErrnoError.With('ENOSYS', undefined, 'IndexedDBStore.clearSync');
85
- }
86
78
  transaction() {
87
79
  const tx = this.db.transaction(this.name, 'readwrite');
88
80
  return new IndexedDBTransaction(tx, this);
@@ -94,14 +86,8 @@ export class IndexedDBStore {
94
86
  const _IndexedDB = {
95
87
  name: 'IndexedDB',
96
88
  options: {
97
- storeName: {
98
- type: 'string',
99
- required: false,
100
- },
101
- idbFactory: {
102
- type: 'object',
103
- required: false,
104
- },
89
+ storeName: { type: 'string', required: false },
90
+ idbFactory: { type: 'object', required: false },
105
91
  },
106
92
  async isAvailable(idbFactory = globalThis.indexedDB) {
107
93
  try {
@@ -122,9 +108,14 @@ const _IndexedDB = {
122
108
  async create(options) {
123
109
  const db = await createDB(options.storeName || 'zenfs', options.idbFactory);
124
110
  const store = new IndexedDBStore(db);
125
- const fs = new (Async(StoreFS))(store);
126
- if (!options?.disableAsyncCache) {
127
- fs._sync = InMemory.create({ name: 'idb-cache' });
111
+ const fs = new StoreFS(store);
112
+ if (options?.disableAsyncCache) {
113
+ log.notice('Async preloading disabled for IndexedDB');
114
+ return fs;
115
+ }
116
+ const tx = store.transaction();
117
+ for (const id of await tx.keys()) {
118
+ await tx.get(id); // Adds to cache
128
119
  }
129
120
  return fs;
130
121
  },
package/dist/access.d.ts CHANGED
@@ -1,29 +1,60 @@
1
- import type { CreationOptions, FileSystemMetadata } from '@zenfs/core';
2
- import { FileSystem, PreloadFile, Stats } from '@zenfs/core';
1
+ import type { CreationOptions, File, InodeLike, Stats } from '@zenfs/core';
2
+ import { FileSystem, Index } from '@zenfs/core';
3
3
  export interface WebAccessOptions {
4
4
  handle: FileSystemDirectoryHandle;
5
+ metadata?: string;
5
6
  }
7
+ type HKindToType<T extends FileSystemHandleKind> = T extends 'directory' ? FileSystemDirectoryHandle : T extends 'file' ? FileSystemFileHandle : FileSystemHandle;
6
8
  declare const WebAccessFS_base: import("@zenfs/core").Mixin<typeof FileSystem, import("@zenfs/core").AsyncMixin>;
7
9
  export declare class WebAccessFS extends WebAccessFS_base {
8
- private _handles;
10
+ /**
11
+ * Disables index optimizations,
12
+ * like using the index for `readdir`
13
+ */
14
+ private readonly disableIndexOptimizations;
15
+ protected index: Index;
16
+ protected _handles: Map<string, FileSystemHandle>;
17
+ /**
18
+ * Loads all of the handles.
19
+ * @internal @hidden
20
+ */
21
+ _loadHandles(path: string, handle: FileSystemDirectoryHandle): Promise<void>;
22
+ /**
23
+ * Loads metadata
24
+ * @internal @hidden
25
+ */
26
+ _loadMetadata(metadataPath?: string): Promise<void>;
9
27
  /**
10
28
  * @hidden
11
29
  */
12
30
  _sync: FileSystem;
13
- constructor(handle: FileSystemDirectoryHandle);
14
- metadata(): FileSystemMetadata;
15
- sync(path: string, data: Uint8Array): Promise<void>;
31
+ constructor(handle: FileSystemDirectoryHandle,
32
+ /**
33
+ * Disables index optimizations,
34
+ * like using the index for `readdir`
35
+ */
36
+ disableIndexOptimizations?: boolean);
16
37
  rename(oldPath: string, newPath: string): Promise<void>;
38
+ unlink(path: string): Promise<void>;
39
+ read(path: string, buffer: Uint8Array, offset: number, end: number): Promise<void>;
40
+ write(path: string, buffer: Uint8Array, offset: number): Promise<void>;
41
+ /**
42
+ * Do not use!
43
+ * @deprecated @internal @hidden
44
+ */
17
45
  writeFile(path: string, data: Uint8Array): Promise<void>;
18
- createFile(path: string, flag: string): Promise<PreloadFile<this>>;
19
46
  stat(path: string): Promise<Stats>;
20
- openFile(path: string, flag: string): Promise<PreloadFile<this>>;
21
- unlink(path: string): Promise<void>;
47
+ createFile(path: string, flag: string, mode: number, options: CreationOptions): Promise<File>;
48
+ openFile(path: string, flag: string): Promise<File>;
49
+ /**
50
+ * @todo Implement
51
+ */
22
52
  link(srcpath: string): Promise<void>;
53
+ sync(path: string, data?: Uint8Array, stats?: Readonly<Partial<InodeLike>>): Promise<void>;
23
54
  rmdir(path: string): Promise<void>;
24
- mkdir(path: string, mode?: number, options?: CreationOptions): Promise<void>;
55
+ mkdir(path: string, mode: number, options: CreationOptions): Promise<void>;
25
56
  readdir(path: string): Promise<string[]>;
26
- protected getHandle(path: string): Promise<FileSystemHandle | undefined>;
57
+ protected get<const T extends FileSystemHandleKind | null>(kind: T | undefined, path: string, syscall?: string): T extends FileSystemHandleKind ? HKindToType<T> : FileSystemHandle;
27
58
  }
28
59
  declare const _WebAccess: {
29
60
  readonly name: "WebAccess";
@@ -32,9 +63,12 @@ declare const _WebAccess: {
32
63
  readonly type: "object";
33
64
  readonly required: true;
34
65
  };
66
+ readonly metadata: {
67
+ readonly type: "string";
68
+ readonly required: false;
69
+ };
35
70
  };
36
- readonly isAvailable: () => boolean;
37
- readonly create: (options: WebAccessOptions) => WebAccessFS;
71
+ readonly create: (options: WebAccessOptions) => Promise<WebAccessFS>;
38
72
  };
39
73
  type _WebAccess = typeof _WebAccess;
40
74
  export interface WebAccess extends _WebAccess {
package/dist/access.js CHANGED
@@ -1,6 +1,7 @@
1
- import { Async, Errno, ErrnoError, FileSystem, InMemory, PreloadFile, Stats } from '@zenfs/core';
2
- import { S_IFDIR, S_IFREG } from '@zenfs/core/emulation/constants.js';
3
- import { basename, dirname, join } from '@zenfs/core/emulation/path.js';
1
+ import { Async, constants, Errno, ErrnoError, FileSystem, Index, InMemory, Inode, LazyFile } from '@zenfs/core';
2
+ import { S_IFDIR, S_IFREG } from '@zenfs/core/vfs/constants.js';
3
+ import { basename, dirname, join } from '@zenfs/core/vfs/path.js';
4
+ import { _throw } from 'utilium';
4
5
  import { convertException } from './utils.js';
5
6
  function isResizable(buffer) {
6
7
  if (buffer instanceof ArrayBuffer)
@@ -9,184 +10,243 @@ function isResizable(buffer) {
9
10
  return buffer.growable;
10
11
  return false;
11
12
  }
13
+ /**
14
+ * Since `FileSystemHandle.kind` doesn't have correct type support
15
+ */
16
+ function isKind(handle, kind) {
17
+ return handle.kind == kind;
18
+ }
12
19
  export class WebAccessFS extends Async(FileSystem) {
13
- constructor(handle) {
14
- super();
20
+ /**
21
+ * Loads all of the handles.
22
+ * @internal @hidden
23
+ */
24
+ async _loadHandles(path, handle) {
25
+ for await (const [key, child] of handle.entries()) {
26
+ const p = join(path, key);
27
+ this._handles.set(p, child);
28
+ if (isKind(child, 'directory'))
29
+ await this._loadHandles(p, child);
30
+ }
31
+ }
32
+ /**
33
+ * Loads metadata
34
+ * @internal @hidden
35
+ */
36
+ async _loadMetadata(metadataPath) {
37
+ if (metadataPath) {
38
+ const handle = this.get('file', metadataPath);
39
+ const file = await handle.getFile();
40
+ const raw = await file.text();
41
+ const data = JSON.parse(raw);
42
+ this.index.fromJSON(data);
43
+ return;
44
+ }
45
+ for (const [path, handle] of this._handles) {
46
+ if (isKind(handle, 'file')) {
47
+ const { lastModified, size } = await handle.getFile();
48
+ this.index.set(path, new Inode({ mode: 0o644 | constants.S_IFREG, size, mtimeMs: lastModified }));
49
+ continue;
50
+ }
51
+ if (!isKind(handle, 'directory'))
52
+ throw new ErrnoError(Errno.EIO, 'Invalid handle', path);
53
+ this.index.set(path, new Inode({ mode: 0o777 | constants.S_IFDIR, size: 0 }));
54
+ }
55
+ }
56
+ constructor(handle,
57
+ /**
58
+ * Disables index optimizations,
59
+ * like using the index for `readdir`
60
+ */
61
+ disableIndexOptimizations = false) {
62
+ super(0x77656261, 'webaccessfs');
63
+ this.disableIndexOptimizations = disableIndexOptimizations;
64
+ this.index = new Index();
15
65
  this._handles = new Map();
16
66
  /**
17
67
  * @hidden
18
68
  */
19
69
  this._sync = InMemory.create({ name: 'accessfs-cache' });
70
+ this.attributes.set('no_buffer_resize');
71
+ this.attributes.set('setid');
20
72
  this._handles.set('/', handle);
21
73
  }
22
- metadata() {
23
- return {
24
- ...super.metadata(),
25
- name: 'WebAccess',
26
- noResizableBuffers: true,
27
- // Not really, but we don't support opening directories so this prevent the VFS from trying
28
- features: ['setid'],
29
- };
30
- }
31
- async sync(path, data) {
32
- await this.writeFile(path, data);
33
- }
34
74
  async rename(oldPath, newPath) {
35
- const handle = await this.getHandle(oldPath);
36
- if (handle instanceof FileSystemDirectoryHandle) {
75
+ if (oldPath == newPath)
76
+ return;
77
+ if (newPath.startsWith(oldPath + '/'))
78
+ throw ErrnoError.With('EBUSY', oldPath, 'rename');
79
+ const handle = this.get(null, oldPath, 'rename');
80
+ if (isKind(handle, 'directory')) {
37
81
  const files = await this.readdir(oldPath);
38
- await this.mkdir(newPath);
39
- if (!files.length) {
40
- await this.unlink(oldPath);
41
- return;
42
- }
43
- for (const file of files) {
82
+ const stats = await this.stat(oldPath);
83
+ await this.mkdir(newPath, stats.mode, stats);
84
+ for (const file of files)
44
85
  await this.rename(join(oldPath, file), join(newPath, file));
45
- await this.unlink(oldPath);
46
- }
86
+ await this.rmdir(oldPath);
47
87
  return;
48
88
  }
49
- if (!(handle instanceof FileSystemFileHandle)) {
89
+ if (!isKind(handle, 'file')) {
50
90
  throw new ErrnoError(Errno.ENOTSUP, 'Not a file or directory handle', oldPath, 'rename');
51
91
  }
52
- const oldFile = await handle.getFile().catch((ex) => {
53
- throw convertException(ex, oldPath, 'rename');
54
- }), destFolder = await this.getHandle(dirname(newPath));
55
- if (!(destFolder instanceof FileSystemDirectoryHandle)) {
56
- return;
57
- }
58
- const newFile = await destFolder.getFileHandle(basename(newPath), { create: true }).catch((ex) => {
59
- throw convertException(ex, newPath, 'rename');
60
- });
92
+ const oldFile = await handle.getFile().catch(ex => _throw(convertException(ex, oldPath, 'rename')));
93
+ const destFolder = this.get('directory', dirname(newPath), 'rename');
94
+ const newFile = await destFolder
95
+ .getFileHandle(basename(newPath), { create: true })
96
+ .catch((ex) => _throw(ex.name == 'TypeMismatchError' ? ErrnoError.With('EISDIR', newPath, 'rename') : convertException(ex, newPath, 'rename')));
61
97
  const writable = await newFile.createWritable();
62
98
  await writable.write(await oldFile.arrayBuffer());
63
99
  await writable.close();
64
100
  await this.unlink(oldPath);
101
+ this._handles.set(newPath, newFile);
65
102
  }
66
- async writeFile(path, data) {
67
- if (isResizable(data.buffer)) {
68
- throw new ErrnoError(Errno.EINVAL, 'Resizable buffers can not be written', path, 'write');
69
- }
70
- const handle = await this.getHandle(dirname(path));
71
- if (!(handle instanceof FileSystemDirectoryHandle)) {
103
+ async unlink(path) {
104
+ if (path == '/')
105
+ throw ErrnoError.With('EBUSY', '/', 'unlink');
106
+ const handle = this.get('directory', dirname(path), 'unlink');
107
+ await handle.removeEntry(basename(path), { recursive: true }).catch(ex => _throw(convertException(ex, path, 'unlink')));
108
+ this.index.delete(path);
109
+ }
110
+ async read(path, buffer, offset, end) {
111
+ if (end <= offset)
72
112
  return;
73
- }
74
- const file = await handle.getFileHandle(basename(path), { create: true });
75
- const writable = await file.createWritable();
76
- await writable.write(data);
113
+ const handle = this.get('file', path, 'write');
114
+ const file = await handle.getFile();
115
+ const data = await file.arrayBuffer();
116
+ if (data.byteLength < end - offset)
117
+ throw ErrnoError.With('ENODATA', path, 'read');
118
+ buffer.set(new Uint8Array(data, offset, end - offset));
119
+ }
120
+ async write(path, buffer, offset) {
121
+ if (isResizable(buffer.buffer)) {
122
+ const newBuffer = new Uint8Array(new ArrayBuffer(buffer.byteLength), buffer.byteOffset, buffer.byteLength);
123
+ newBuffer.set(buffer);
124
+ buffer = newBuffer;
125
+ }
126
+ const inode = this.index.get(path);
127
+ if (!inode)
128
+ throw ErrnoError.With('ENOENT', path, 'write');
129
+ const handle = this.get('file', path, 'write');
130
+ const writable = await handle.createWritable();
131
+ try {
132
+ await writable.seek(offset);
133
+ }
134
+ catch {
135
+ await writable.write({ type: 'seek', position: offset });
136
+ }
137
+ await writable.write(buffer);
77
138
  await writable.close();
139
+ const { size, lastModified } = await handle.getFile();
140
+ inode.update({ size, mtimeMs: lastModified });
141
+ this.index.set(path, inode);
78
142
  }
79
- async createFile(path, flag) {
80
- await this.writeFile(path, new Uint8Array());
81
- return this.openFile(path, flag);
143
+ /**
144
+ * Do not use!
145
+ * @deprecated @internal @hidden
146
+ */
147
+ async writeFile(path, data) {
148
+ return this.write(path, data, 0);
82
149
  }
150
+ // eslint-disable-next-line @typescript-eslint/require-await
83
151
  async stat(path) {
84
- const handle = await this.getHandle(path);
85
- if (!handle) {
152
+ const inode = this.index.get(path);
153
+ if (!inode)
86
154
  throw ErrnoError.With('ENOENT', path, 'stat');
87
- }
88
- if (handle instanceof FileSystemDirectoryHandle) {
89
- return new Stats({ mode: 0o777 | S_IFDIR, size: 4096 });
90
- }
91
- if (handle instanceof FileSystemFileHandle) {
92
- const { lastModified, size } = await handle.getFile();
93
- return new Stats({ mode: 0o777 | S_IFREG, size, mtimeMs: lastModified });
94
- }
95
- throw new ErrnoError(Errno.EBADE, 'Handle is not a directory or file', path, 'stat');
155
+ return inode.toStats();
96
156
  }
97
- async openFile(path, flag) {
98
- const handle = await this.getHandle(path);
99
- if (!(handle instanceof FileSystemFileHandle)) {
100
- throw ErrnoError.With('EISDIR', path, 'openFile');
101
- }
102
- const file = await handle.getFile().catch((ex) => {
103
- throw convertException(ex, path, 'openFile');
104
- });
105
- const data = new Uint8Array(await file.arrayBuffer());
106
- const stats = new Stats({ mode: 0o777 | S_IFREG, size: file.size, mtimeMs: file.lastModified });
107
- return new PreloadFile(this, path, flag, stats, data);
157
+ async createFile(path, flag, mode, options) {
158
+ const handle = this.get('directory', dirname(path), 'createFile');
159
+ if (this.index.has(path))
160
+ throw ErrnoError.With('EEXIST', path, 'createFile');
161
+ const file = await handle.getFileHandle(basename(path), { create: true });
162
+ // Race condition bypass
163
+ const inode = this.index.get(path) ?? new Inode({ ...options, mode: mode | S_IFREG });
164
+ this.index.set(path, inode);
165
+ this._handles.set(path, file);
166
+ return new LazyFile(this, path, flag, inode);
108
167
  }
109
- async unlink(path) {
110
- const handle = await this.getHandle(dirname(path));
111
- if (!(handle instanceof FileSystemDirectoryHandle)) {
112
- throw ErrnoError.With('ENOTDIR', dirname(path), 'unlink');
113
- }
114
- await handle.removeEntry(basename(path), { recursive: true }).catch((ex) => {
115
- throw convertException(ex, path, 'unlink');
116
- });
168
+ async openFile(path, flag) {
169
+ const inode = this.index.get(path);
170
+ if (!inode)
171
+ throw ErrnoError.With('ENOENT', path, 'stat');
172
+ return new LazyFile(this, path, flag, inode.toStats());
117
173
  }
118
- // eslint-disable-next-line @typescript-eslint/require-await
174
+ /**
175
+ * @todo Implement
176
+ */
119
177
  async link(srcpath) {
120
178
  return;
121
179
  }
180
+ async sync(path, data, stats) {
181
+ const inode = this.index.get(path) ?? new Inode();
182
+ inode.update(stats);
183
+ this.index.set(path, inode);
184
+ if (!data)
185
+ return;
186
+ const handle = this.get('file', path, 'write');
187
+ const writable = await handle.createWritable();
188
+ try {
189
+ await writable.seek(0);
190
+ }
191
+ catch {
192
+ await writable.write({ type: 'seek', position: 0 });
193
+ }
194
+ await writable.write(data);
195
+ await writable.close();
196
+ }
122
197
  async rmdir(path) {
123
- return this.unlink(path);
198
+ if ((await this.readdir(path)).length)
199
+ throw ErrnoError.With('ENOTEMPTY', path, 'rmdir');
200
+ if (path == '/')
201
+ throw ErrnoError.With('EBUSY', '/', 'rmdir');
202
+ const handle = this.get('directory', dirname(path), 'rmdir');
203
+ await handle.removeEntry(basename(path), { recursive: true }).catch(ex => _throw(convertException(ex, path, 'rmdir')));
204
+ this.index.delete(path);
124
205
  }
125
206
  async mkdir(path, mode, options) {
126
- const existingHandle = await this.getHandle(path).catch((ex) => {
127
- if (ex.code != 'ENOENT') {
128
- throw ex;
129
- }
130
- });
131
- if (existingHandle) {
207
+ if (this.index.has(path))
132
208
  throw ErrnoError.With('EEXIST', path, 'mkdir');
133
- }
134
- const handle = await this.getHandle(dirname(path));
135
- if (!(handle instanceof FileSystemDirectoryHandle)) {
136
- throw ErrnoError.With('ENOTDIR', path, 'mkdir');
137
- }
138
- await handle.getDirectoryHandle(basename(path), { create: true });
209
+ const handle = this.get('directory', dirname(path), 'mkdir');
210
+ const dir = await handle.getDirectoryHandle(basename(path), { create: true });
211
+ this._handles.set(path, dir);
212
+ this.index.set(path, new Inode({ ...options, mode: mode | S_IFDIR }));
139
213
  }
140
214
  async readdir(path) {
141
- const handle = await this.getHandle(path);
142
- if (!(handle instanceof FileSystemDirectoryHandle)) {
143
- throw ErrnoError.With('ENOTDIR', path, 'readdir');
144
- }
215
+ if (!this.disableIndexOptimizations) {
216
+ if (!this.index.has(path))
217
+ throw ErrnoError.With('ENOENT', path, 'readdir');
218
+ const entries = this.index.directoryEntries(path);
219
+ if (!entries)
220
+ throw ErrnoError.With('ENOTDIR', path, 'readdir');
221
+ return Object.keys(entries);
222
+ }
223
+ const handle = this.get('directory', path, 'readdir');
145
224
  const entries = [];
146
225
  for await (const k of handle.keys()) {
147
226
  entries.push(k);
148
227
  }
149
228
  return entries;
150
229
  }
151
- async getHandle(path) {
152
- if (this._handles.has(path)) {
153
- return this._handles.get(path);
154
- }
155
- let walked = '/';
156
- for (const part of path.split('/').slice(1)) {
157
- const handle = this._handles.get(walked);
158
- if (!(handle instanceof FileSystemDirectoryHandle)) {
159
- throw ErrnoError.With('ENOTDIR', walked, 'getHandle');
160
- }
161
- walked = join(walked, part);
162
- const child = await handle.getDirectoryHandle(part).catch((ex) => {
163
- switch (ex.name) {
164
- case 'TypeMismatchError':
165
- return handle.getFileHandle(part).catch((ex) => {
166
- //throw convertException(ex, walked, 'getHandle');
167
- });
168
- case 'TypeError':
169
- throw new ErrnoError(Errno.ENOENT, ex.message, walked, 'getHandle');
170
- default:
171
- throw convertException(ex, walked, 'getHandle');
172
- }
173
- });
174
- if (child)
175
- this._handles.set(walked, child);
176
- }
177
- return this._handles.get(path);
230
+ get(kind = null, path, syscall) {
231
+ const handle = this._handles.get(path);
232
+ if (!handle)
233
+ throw ErrnoError.With('ENODATA', path, syscall);
234
+ if (kind && !isKind(handle, kind))
235
+ throw ErrnoError.With(kind == 'directory' ? 'ENOTDIR' : 'EISDIR', path, syscall);
236
+ return handle;
178
237
  }
179
238
  }
180
239
  const _WebAccess = {
181
240
  name: 'WebAccess',
182
241
  options: {
183
242
  handle: { type: 'object', required: true },
243
+ metadata: { type: 'string', required: false },
184
244
  },
185
- isAvailable() {
186
- return typeof FileSystemHandle == 'function';
187
- },
188
- create(options) {
189
- return new WebAccessFS(options.handle);
245
+ async create(options) {
246
+ const fs = new WebAccessFS(options.handle);
247
+ await fs._loadHandles('/', options.handle);
248
+ await fs._loadMetadata(options.metadata);
249
+ return fs;
190
250
  },
191
251
  };
192
252
  export const WebAccess = _WebAccess;
@@ -8,8 +8,8 @@ if ('AudioWorkletProcessor' in globalThis) {
8
8
  }
9
9
  process(inputs, outputs) {
10
10
  if (this.buffer && this.buffer.byteLength >= 128) {
11
- outputs[0][0].set(this.buffer.slice(0, 128));
12
- this.buffer = this.buffer.slice(128);
11
+ outputs[0][0].set(this.buffer.subarray(0, 128));
12
+ this.buffer = this.buffer.subarray(128);
13
13
  }
14
14
  return true;
15
15
  }
@@ -44,12 +44,11 @@ export async function dsp(options = {}) {
44
44
  init(ino, options) {
45
45
  return { data: dsp, major: 14, minor: 3 };
46
46
  },
47
- read() {
48
- return 0;
47
+ readD() {
48
+ return;
49
49
  },
50
- write(file, data) {
51
- file.device.data.port.postMessage(data.buffer);
52
- return data.byteLength;
50
+ writeD(device, buffer, offset) {
51
+ device.data.port.postMessage(buffer.buffer);
53
52
  },
54
53
  };
55
54
  }
@@ -2,6 +2,10 @@ import type { DeviceDriver } from '@zenfs/core';
2
2
  export interface FramebufferOptions {
3
3
  canvas?: HTMLCanvasElement | null;
4
4
  }
5
+ export interface FramebufferData {
6
+ context: CanvasRenderingContext2D;
7
+ image: ImageData;
8
+ }
5
9
  /**
6
10
  * A frame buffer
7
11
  *
@@ -10,4 +14,4 @@ export interface FramebufferOptions {
10
14
  * addDevice(framebuffer, { canvas: document.querySelector('#your-canvas') })
11
15
  * ```
12
16
  */
13
- export declare const framebuffer: DeviceDriver<CanvasRenderingContext2D>;
17
+ export declare const framebuffer: DeviceDriver<FramebufferData>;
@@ -15,22 +15,22 @@ export const framebuffer = {
15
15
  canvas = document.createElement('canvas');
16
16
  document.body.appendChild(canvas);
17
17
  }
18
- const ctx = canvas.getContext('2d');
19
- if (!ctx) {
18
+ const context = canvas.getContext('2d');
19
+ if (!context) {
20
20
  throw new ErrnoError(Errno.EIO, 'Could not get context from canvas whilst initializing frame buffer.');
21
21
  }
22
- return { data: ctx, major: 29, minor: framebufferN++, name: 'fb' };
22
+ const image = new ImageData(canvas.width, canvas.height);
23
+ return {
24
+ data: { context, image },
25
+ major: 29,
26
+ minor: framebufferN++,
27
+ name: 'fb',
28
+ };
23
29
  },
24
- read() {
25
- return 0;
26
- },
27
- write(file, data) {
28
- const { width, height } = file.device.data.canvas;
29
- if (data.byteLength < 4 * width * height) {
30
- return 0;
31
- }
32
- const imageData = new ImageData(new Uint8ClampedArray(data), width, height);
33
- file.device.data.putImageData(imageData, 0, 0);
34
- return data.byteLength;
30
+ readD() { },
31
+ writeD({ data: { image, context } }, buffer, offset) {
32
+ image.data.set(buffer, offset);
33
+ context.putImageData(image, 0, 0);
34
+ return buffer.byteLength;
35
35
  },
36
36
  };
package/dist/storage.d.ts CHANGED
@@ -1,20 +1,20 @@
1
- import type { SimpleSyncStore, Store } from '@zenfs/core';
2
- import { SimpleTransaction, StoreFS } from '@zenfs/core';
1
+ import type { SyncMapStore, Store } from '@zenfs/core';
2
+ import { StoreFS, SyncMapTransaction } from '@zenfs/core';
3
3
  /**
4
4
  * A synchronous key-value store backed by Storage.
5
5
  */
6
- export declare class WebStorageStore implements Store, SimpleSyncStore {
6
+ export declare class WebStorageStore implements Store, SyncMapStore {
7
7
  protected storage: Storage;
8
8
  get name(): string;
9
9
  constructor(storage: Storage);
10
10
  clear(): void;
11
11
  clearSync(): void;
12
12
  sync(): Promise<void>;
13
- transaction(): SimpleTransaction;
14
- keys(): Iterable<bigint>;
15
- get(key: bigint): Uint8Array | undefined;
16
- set(key: bigint, data: Uint8Array): void;
17
- delete(key: bigint): void;
13
+ transaction(): SyncMapTransaction;
14
+ keys(): Iterable<number>;
15
+ get(key: number): Uint8Array | undefined;
16
+ set(key: number, data: Uint8Array): void;
17
+ delete(key: number): void;
18
18
  }
19
19
  /**
20
20
  * Options to pass to the StorageFileSystem
package/dist/storage.js CHANGED
@@ -1,4 +1,4 @@
1
- import { ErrnoError, Errno, SimpleTransaction, StoreFS, decodeRaw, encodeRaw } from '@zenfs/core';
1
+ import { ErrnoError, Errno, StoreFS, decodeRaw, encodeRaw, SyncMapTransaction } from '@zenfs/core';
2
2
  /**
3
3
  * A synchronous key-value store backed by Storage.
4
4
  */
@@ -18,10 +18,10 @@ export class WebStorageStore {
18
18
  }
19
19
  async sync() { }
20
20
  transaction() {
21
- return new SimpleTransaction(this);
21
+ return new SyncMapTransaction(this);
22
22
  }
23
23
  keys() {
24
- return Object.keys(this.storage).map(k => BigInt(k));
24
+ return Object.keys(this.storage).map(k => Number(k));
25
25
  }
26
26
  get(key) {
27
27
  const data = this.storage.getItem(key.toString());
package/dist/utils.js CHANGED
@@ -57,9 +57,8 @@ function errnoForDOMException(ex) {
57
57
  * @internal
58
58
  */
59
59
  export function convertException(ex, path, syscall) {
60
- if (ex instanceof ErrnoError) {
60
+ if (ex instanceof ErrnoError)
61
61
  return ex;
62
- }
63
62
  const code = ex instanceof DOMException ? Errno[errnoForDOMException(ex)] : Errno.EIO;
64
63
  const error = new ErrnoError(code, ex.message, path, syscall);
65
64
  error.stack = ex.stack;
package/dist/xml.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { CreationOptions, File, FileSystemMetadata, StatsLike } from '@zenfs/core';
1
+ import type { CreationOptions, File, InodeLike, StatsLike } from '@zenfs/core';
2
2
  import { FileSystem, Stats } from '@zenfs/core';
3
3
  export interface XMLOptions {
4
4
  /**
@@ -9,15 +9,14 @@ export interface XMLOptions {
9
9
  declare const XMLFS_base: import("@zenfs/core").Mixin<typeof FileSystem, import("@zenfs/core").AsyncFSMethods>;
10
10
  export declare class XMLFS extends XMLFS_base {
11
11
  /**
12
- * @inheritdoc XMLOptions.root
12
+ * @inheritDoc XMLOptions.root
13
13
  */
14
14
  readonly root: Element;
15
15
  constructor(
16
16
  /**
17
- * @inheritdoc XMLOptions.root
17
+ * @inheritDoc XMLOptions.root
18
18
  */
19
19
  root?: Element);
20
- metadata(): FileSystemMetadata;
21
20
  renameSync(oldPath: string, newPath: string): void;
22
21
  statSync(path: string): Stats;
23
22
  openFileSync(path: string, flag: string): File;
@@ -27,10 +26,12 @@ export declare class XMLFS extends XMLFS_base {
27
26
  mkdirSync(path: string, mode: number, { uid, gid }: CreationOptions): void;
28
27
  readdirSync(path: string): string[];
29
28
  linkSync(target: string, link: string): void;
30
- syncSync(path: string, data: Uint8Array, stats: Readonly<Stats>): void;
29
+ syncSync(path: string, data?: Uint8Array, stats?: Readonly<Partial<InodeLike>>): void;
30
+ readSync(path: string, buffer: Uint8Array, offset: number, end: number): void;
31
+ writeSync(path: string, buffer: Uint8Array, offset: number): void;
31
32
  toString(): string;
32
33
  protected get(syscall: string, path: string): Element;
33
- protected create(syscall: string, path: string, stats: Partial<StatsLike<number>> & Pick<StatsLike, 'mode'>): Element;
34
+ protected create(syscall: string, path: string, stats: Partial<InodeLike> & Pick<StatsLike, 'mode'>): Element;
34
35
  protected add(syscall: string, node: Element, path: string, contents?: boolean): void;
35
36
  protected remove(syscall: string, node: Element, path: string, contents?: boolean): void;
36
37
  }
package/dist/xml.js CHANGED
@@ -1,20 +1,19 @@
1
- import { decodeRaw, encodeRaw, Errno, ErrnoError, FileSystem, PreloadFile, Stats, Sync } from '@zenfs/core';
2
- import { S_IFDIR, S_IFREG, S_ISGID, S_ISUID } from '@zenfs/core/emulation/constants.js';
3
- import { basename, dirname } from '@zenfs/core/emulation/path.js';
4
- const statsLikeKeys = ['size', 'mode', 'atimeMs', 'mtimeMs', 'ctimeMs', 'birthtimeMs', 'uid', 'gid', 'ino', 'nlink'];
1
+ import { _inode_fields, constants, decodeRaw, encodeRaw, Errno, ErrnoError, FileSystem, LazyFile, Stats, Sync } from '@zenfs/core';
2
+ import { basename, dirname } from '@zenfs/core/vfs/path.js';
5
3
  function get_stats(node) {
6
4
  const stats = {};
7
- for (const key of statsLikeKeys) {
5
+ for (const key of _inode_fields) {
8
6
  const value = node.getAttribute(key);
9
- stats[key] = value != null ? parseInt(value, 16) : undefined;
7
+ if (value !== null && value !== undefined)
8
+ stats[key] = parseInt(value, 16);
10
9
  }
11
10
  return new Stats(stats);
12
11
  }
13
12
  function set_stats(node, stats) {
14
- for (const key of statsLikeKeys) {
15
- if (stats[key] != undefined) {
16
- node.setAttribute(key, stats[key].toString(16));
17
- }
13
+ for (const key of Object.keys(stats)) {
14
+ if (!(key in _inode_fields) || stats[key] === undefined)
15
+ continue;
16
+ node.setAttribute(key, stats[key].toString(16));
18
17
  }
19
18
  }
20
19
  function get_paths(node, contents = false) {
@@ -31,11 +30,12 @@ function get_paths(node, contents = false) {
31
30
  export class XMLFS extends Sync(FileSystem) {
32
31
  constructor(
33
32
  /**
34
- * @inheritdoc XMLOptions.root
33
+ * @inheritDoc XMLOptions.root
35
34
  */
36
35
  root = new DOMParser().parseFromString('<fs></fs>', 'application/xml').documentElement) {
37
- super();
36
+ super(0x20786d6c, 'xmltmpfs');
38
37
  this.root = root;
38
+ this.attributes.set('setid');
39
39
  try {
40
40
  this.mkdirSync('/', 0o777, { uid: 0, gid: 0 });
41
41
  }
@@ -45,9 +45,6 @@ export class XMLFS extends Sync(FileSystem) {
45
45
  throw error;
46
46
  }
47
47
  }
48
- metadata() {
49
- return { ...super.metadata(), features: ['setid'] };
50
- }
51
48
  renameSync(oldPath, newPath) {
52
49
  const node = this.get('rename', oldPath);
53
50
  this.remove('rename', node, oldPath);
@@ -58,17 +55,17 @@ export class XMLFS extends Sync(FileSystem) {
58
55
  }
59
56
  openFileSync(path, flag) {
60
57
  const node = this.get('openFile', path);
61
- return new PreloadFile(this, path, flag, get_stats(node), encodeRaw(node.textContent));
58
+ return new LazyFile(this, path, flag, get_stats(node));
62
59
  }
63
60
  createFileSync(path, flag, mode, { uid, gid }) {
64
61
  const parent = this.statSync(dirname(path));
65
62
  const stats = new Stats({
66
- mode: mode | S_IFREG,
67
- uid: parent.mode & S_ISUID ? parent.uid : uid,
68
- gid: parent.mode & S_ISGID ? parent.gid : gid,
63
+ mode: mode | constants.S_IFREG,
64
+ uid: parent.mode & constants.S_ISUID ? parent.uid : uid,
65
+ gid: parent.mode & constants.S_ISGID ? parent.gid : gid,
69
66
  });
70
67
  this.create('createFile', path, stats);
71
- return new PreloadFile(this, path, flag, stats);
68
+ return new LazyFile(this, path, flag, stats);
72
69
  }
73
70
  unlinkSync(path) {
74
71
  const node = this.get('unlink', path);
@@ -87,9 +84,9 @@ export class XMLFS extends Sync(FileSystem) {
87
84
  mkdirSync(path, mode, { uid, gid }) {
88
85
  const parent = this.statSync(dirname(path));
89
86
  const node = this.create('mkdir', path, {
90
- mode: mode | S_IFDIR,
91
- uid: parent.mode & S_ISUID ? parent.uid : uid,
92
- gid: parent.mode & S_ISGID ? parent.gid : gid,
87
+ mode: mode | constants.S_IFDIR,
88
+ uid: parent.mode & constants.S_ISUID ? parent.uid : uid,
89
+ gid: parent.mode & constants.S_ISGID ? parent.gid : gid,
93
90
  });
94
91
  node.textContent = '[]';
95
92
  }
@@ -108,11 +105,23 @@ export class XMLFS extends Sync(FileSystem) {
108
105
  const node = this.get('link', target);
109
106
  this.add('link', node, link);
110
107
  }
111
- syncSync(path, data, stats) {
108
+ syncSync(path, data, stats = {}) {
112
109
  const node = this.get('sync', path);
113
- node.textContent = decodeRaw(data);
110
+ if (data)
111
+ node.textContent = decodeRaw(data);
114
112
  set_stats(node, stats);
115
113
  }
114
+ readSync(path, buffer, offset, end) {
115
+ const node = this.get('read', path);
116
+ const raw = encodeRaw(node.textContent.slice(offset, end));
117
+ buffer.set(raw);
118
+ }
119
+ writeSync(path, buffer, offset) {
120
+ const node = this.get('write', path);
121
+ const data = decodeRaw(buffer);
122
+ const after = node.textContent.slice(offset + data.length);
123
+ node.textContent = node.textContent.slice(0, offset) + data + after;
124
+ }
116
125
  toString() {
117
126
  return new XMLSerializer().serializeToString(this.root);
118
127
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenfs/dom",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "description": "DOM backends for ZenFS",
5
5
  "funding": {
6
6
  "type": "individual",
@@ -45,14 +45,14 @@
45
45
  "lint": "eslint src",
46
46
  "build": "tsc -p tsconfig.json",
47
47
  "build:docs": "typedoc --out docs --name 'ZenFS DOM' src/index.ts",
48
- "test": "echo No tests yet",
48
+ "test": "npx zenfs-test -abcf",
49
49
  "prepublishOnly": "npm run build"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@eslint/js": "^9.12.0",
53
+ "c8": "^10.1.3",
53
54
  "eslint": "^9.12.0",
54
55
  "fake-indexeddb": "^6.0.0",
55
- "file-system-access": "^1.0.4",
56
56
  "globals": "^15.10.0",
57
57
  "prettier": "^3.2.5",
58
58
  "tsx": "^4.19.2",
@@ -61,7 +61,8 @@
61
61
  "typescript-eslint": "^8.8.1"
62
62
  },
63
63
  "peerDependencies": {
64
- "@zenfs/core": "^1.7.0"
64
+ "@zenfs/core": "^1.9.0",
65
+ "utilium": "^1.2.10"
65
66
  },
66
67
  "keywords": [
67
68
  "filesystem",
package/readme.md CHANGED
@@ -6,10 +6,10 @@ Please read the ZenFS core documentation!
6
6
 
7
7
  ## Backends
8
8
 
9
- - `WebStorage` stores files in a `Storage` object, like `localStorage` and `sessionStorage`.
10
- - `IndexedDB` stores files into an `IndexedDB` object database.
11
- - `WebAccess` uses the [File System Access API](https://developer.mozilla.org/Web/API/File_System_API).
12
- - `XML` uses an `XMLDocument` to store files, which can be appended to the DOM.
9
+ - `WebStorage` stores files in a `Storage` object, like `localStorage` and `sessionStorage`.
10
+ - `IndexedDB` stores files into an `IndexedDB` object database.
11
+ - `WebAccess` uses the [File System Access API](https://developer.mozilla.org/Web/API/File_System_API).
12
+ - `XML` uses an `XMLDocument` to store files, which can be appended to the DOM.
13
13
 
14
14
  For more information, see the [API documentation](https://zen-fs.github.io/dom).
15
15
 
package/tsconfig.json CHANGED
@@ -3,10 +3,11 @@
3
3
  "module": "NodeNext",
4
4
  "target": "ES2020",
5
5
  "outDir": "dist",
6
- "lib": ["ESNext", "DOM"],
6
+ "lib": ["ESNext", "DOM", "ESNext.AsyncIterable", "DOM.AsyncIterable"],
7
7
  "strict": true,
8
8
  "moduleResolution": "NodeNext",
9
- "declaration": true
9
+ "declaration": true,
10
+ "verbatimModuleSyntax": true
10
11
  },
11
12
  "include": ["src/**/*"],
12
13
  "exclude": ["node_modules"]