@zenfs/dom 1.1.3 → 1.1.5

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,39 @@
1
- import type { CreationOptions, FileSystemMetadata } from '@zenfs/core';
2
- import { FileSystem, PreloadFile, Stats } from '@zenfs/core';
1
+ import type { CreationOptions, FileSystem } from '@zenfs/core';
2
+ import { IndexFS } from '@zenfs/core';
3
3
  export interface WebAccessOptions {
4
4
  handle: FileSystemDirectoryHandle;
5
+ metadata?: string;
5
6
  }
6
- declare const WebAccessFS_base: import("@zenfs/core").Mixin<typeof FileSystem, import("@zenfs/core").AsyncMixin>;
7
+ type HKindToType<T extends FileSystemHandleKind> = T extends 'directory' ? FileSystemDirectoryHandle : T extends 'file' ? FileSystemFileHandle : FileSystemHandle;
8
+ declare const WebAccessFS_base: import("@zenfs/core").Mixin<typeof IndexFS, import("@zenfs/core").AsyncMixin>;
7
9
  export declare class WebAccessFS extends WebAccessFS_base {
8
- private _handles;
10
+ protected _handles: Map<string, FileSystemHandle>;
11
+ /**
12
+ * Loads all of the handles.
13
+ * @internal @hidden
14
+ */
15
+ _loadHandles(path: string, handle: FileSystemDirectoryHandle): Promise<void>;
16
+ /**
17
+ * Loads metadata
18
+ * @internal @hidden
19
+ */
20
+ _loadMetadata(metadataPath?: string): Promise<void>;
9
21
  /**
10
22
  * @hidden
11
23
  */
12
24
  _sync: FileSystem;
13
25
  constructor(handle: FileSystemDirectoryHandle);
14
- metadata(): FileSystemMetadata;
15
- sync(path: string, data: Uint8Array): Promise<void>;
16
- rename(oldPath: string, newPath: string): Promise<void>;
26
+ remove(path: string): Promise<void>;
27
+ protected removeSync(path: string): void;
28
+ read(path: string, buffer: Uint8Array, offset: number, end: number): Promise<void>;
29
+ write(path: string, buffer: Uint8Array, offset: number): Promise<void>;
30
+ /**
31
+ * Do not use!
32
+ * @deprecated @internal @hidden
33
+ */
17
34
  writeFile(path: string, data: Uint8Array): Promise<void>;
18
- createFile(path: string, flag: string): Promise<PreloadFile<this>>;
19
- stat(path: string): Promise<Stats>;
20
- openFile(path: string, flag: string): Promise<PreloadFile<this>>;
21
- unlink(path: string): Promise<void>;
22
- link(srcpath: string): Promise<void>;
23
- rmdir(path: string): Promise<void>;
24
- mkdir(path: string, mode?: number, options?: CreationOptions): Promise<void>;
25
- readdir(path: string): Promise<string[]>;
26
- protected getHandle(path: string): Promise<FileSystemHandle | undefined>;
35
+ mkdir(path: string, mode: number, options: CreationOptions): Promise<void>;
36
+ protected get<const T extends FileSystemHandleKind | null>(kind: T | undefined, path: string, syscall?: string): T extends FileSystemHandleKind ? HKindToType<T> : FileSystemHandle;
27
37
  }
28
38
  declare const _WebAccess: {
29
39
  readonly name: "WebAccess";
@@ -32,9 +42,12 @@ declare const _WebAccess: {
32
42
  readonly type: "object";
33
43
  readonly required: true;
34
44
  };
45
+ readonly metadata: {
46
+ readonly type: "string";
47
+ readonly required: false;
48
+ };
35
49
  };
36
- readonly isAvailable: () => boolean;
37
- readonly create: (options: WebAccessOptions) => WebAccessFS;
50
+ readonly create: (options: WebAccessOptions) => Promise<WebAccessFS>;
38
51
  };
39
52
  type _WebAccess = typeof _WebAccess;
40
53
  export interface WebAccess extends _WebAccess {
package/dist/access.js CHANGED
@@ -1,5 +1,7 @@
1
- import { Async, constants, Errno, ErrnoError, FileSystem, InMemory, PreloadFile, Stats } from '@zenfs/core';
1
+ import { Async, constants, Errno, ErrnoError, IndexFS, InMemory, Inode, log } from '@zenfs/core';
2
+ import { S_IFDIR, S_IFMT } from '@zenfs/core/vfs/constants.js';
2
3
  import { basename, dirname, join } from '@zenfs/core/vfs/path.js';
4
+ import { _throw } from 'utilium';
3
5
  import { convertException } from './utils.js';
4
6
  function isResizable(buffer) {
5
7
  if (buffer instanceof ArrayBuffer)
@@ -8,184 +10,148 @@ function isResizable(buffer) {
8
10
  return buffer.growable;
9
11
  return false;
10
12
  }
11
- export class WebAccessFS extends Async(FileSystem) {
13
+ /**
14
+ * Since `FileSystemHandle.kind` doesn't have correct type support
15
+ */
16
+ function isKind(handle, kind) {
17
+ return handle.kind == kind;
18
+ }
19
+ export class WebAccessFS extends Async(IndexFS) {
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
+ }
12
56
  constructor(handle) {
13
- super();
57
+ super(0x77656261, 'webaccessfs');
14
58
  this._handles = new Map();
15
59
  /**
16
60
  * @hidden
17
61
  */
18
62
  this._sync = InMemory.create({ name: 'accessfs-cache' });
63
+ this.attributes.set('no_buffer_resize');
64
+ this.attributes.set('setid');
19
65
  this._handles.set('/', handle);
20
66
  }
21
- metadata() {
22
- return {
23
- ...super.metadata(),
24
- name: 'WebAccess',
25
- noResizableBuffers: true,
26
- // Not really, but we don't support opening directories so this prevent the VFS from trying
27
- features: ['setid'],
28
- };
67
+ async remove(path) {
68
+ const handle = this.get('directory', dirname(path));
69
+ await handle.removeEntry(basename(path), { recursive: true }).catch(ex => _throw(convertException(ex, path)));
29
70
  }
30
- async sync(path, data) {
31
- await this.writeFile(path, data);
71
+ removeSync(path) {
72
+ throw log.crit(ErrnoError.With('ENOSYS', path));
32
73
  }
33
- async rename(oldPath, newPath) {
34
- const handle = await this.getHandle(oldPath);
35
- if (handle instanceof FileSystemDirectoryHandle) {
36
- const files = await this.readdir(oldPath);
37
- await this.mkdir(newPath);
38
- if (!files.length) {
39
- await this.unlink(oldPath);
40
- return;
41
- }
42
- for (const file of files) {
43
- await this.rename(join(oldPath, file), join(newPath, file));
44
- await this.unlink(oldPath);
45
- }
74
+ async read(path, buffer, offset, end) {
75
+ if (end <= offset)
46
76
  return;
47
- }
48
- if (!(handle instanceof FileSystemFileHandle)) {
49
- throw new ErrnoError(Errno.ENOTSUP, 'Not a file or directory handle', oldPath, 'rename');
50
- }
51
- const oldFile = await handle.getFile().catch((ex) => {
52
- throw convertException(ex, oldPath, 'rename');
53
- }), destFolder = await this.getHandle(dirname(newPath));
54
- if (!(destFolder instanceof FileSystemDirectoryHandle)) {
55
- return;
56
- }
57
- const newFile = await destFolder.getFileHandle(basename(newPath), { create: true }).catch((ex) => {
58
- throw convertException(ex, newPath, 'rename');
59
- });
60
- const writable = await newFile.createWritable();
61
- await writable.write(await oldFile.arrayBuffer());
62
- await writable.close();
63
- await this.unlink(oldPath);
77
+ const handle = this.get('file', path, 'write');
78
+ const file = await handle.getFile();
79
+ const data = await file.arrayBuffer();
80
+ if (data.byteLength < end - offset)
81
+ throw ErrnoError.With('ENODATA', path, 'read');
82
+ buffer.set(new Uint8Array(data, offset, end - offset));
64
83
  }
65
- async writeFile(path, data) {
66
- if (isResizable(data.buffer)) {
67
- throw new ErrnoError(Errno.EINVAL, 'Resizable buffers can not be written', path, 'write');
68
- }
69
- const handle = await this.getHandle(dirname(path));
70
- if (!(handle instanceof FileSystemDirectoryHandle)) {
84
+ async write(path, buffer, offset) {
85
+ if (isResizable(buffer.buffer)) {
86
+ const newBuffer = new Uint8Array(new ArrayBuffer(buffer.byteLength), buffer.byteOffset, buffer.byteLength);
87
+ newBuffer.set(buffer);
88
+ buffer = newBuffer;
89
+ }
90
+ const inode = this.index.get(path);
91
+ if (!inode)
92
+ throw ErrnoError.With('ENOENT', path, 'write');
93
+ const isDir = (inode.mode & S_IFMT) == S_IFDIR;
94
+ let handle;
95
+ try {
96
+ handle = this.get(isDir ? 'directory' : 'file', path, 'write');
97
+ }
98
+ catch {
99
+ const parent = this.get('directory', dirname(path), 'write');
100
+ handle = await parent[isDir ? 'getDirectoryHandle' : 'getFileHandle'](basename(path), { create: true }).catch((ex) => _throw(convertException(ex, path)));
101
+ this._handles.set(path, handle);
102
+ }
103
+ if (isDir)
104
+ return;
105
+ if (isKind(handle, 'directory')) {
106
+ log.crit(new ErrnoError(Errno.EIO, 'Mismatch in entry kind on write', path, 'write'));
71
107
  return;
72
108
  }
73
- const file = await handle.getFileHandle(basename(path), { create: true });
74
- const writable = await file.createWritable();
75
- await writable.write(data);
76
- await writable.close();
77
- }
78
- async createFile(path, flag) {
79
- await this.writeFile(path, new Uint8Array());
80
- return this.openFile(path, flag);
81
- }
82
- async stat(path) {
83
- const handle = await this.getHandle(path);
84
- if (!handle) {
85
- throw ErrnoError.With('ENOENT', path, 'stat');
86
- }
87
- if (handle instanceof FileSystemDirectoryHandle) {
88
- return new Stats({ mode: 0o777 | constants.S_IFDIR, size: 4096 });
89
- }
90
- if (handle instanceof FileSystemFileHandle) {
91
- const { lastModified, size } = await handle.getFile();
92
- return new Stats({ mode: 0o777 | constants.S_IFREG, size, mtimeMs: lastModified });
93
- }
94
- throw new ErrnoError(Errno.EBADE, 'Handle is not a directory or file', path, 'stat');
95
- }
96
- async openFile(path, flag) {
97
- const handle = await this.getHandle(path);
98
- if (!(handle instanceof FileSystemFileHandle)) {
99
- throw ErrnoError.With('EISDIR', path, 'openFile');
109
+ const writable = await handle.createWritable();
110
+ try {
111
+ await writable.seek(offset);
100
112
  }
101
- const file = await handle.getFile().catch((ex) => {
102
- throw convertException(ex, path, 'openFile');
103
- });
104
- const data = new Uint8Array(await file.arrayBuffer());
105
- const stats = new Stats({ mode: 0o777 | constants.S_IFREG, size: file.size, mtimeMs: file.lastModified });
106
- return new PreloadFile(this, path, flag, stats, data);
107
- }
108
- async unlink(path) {
109
- const handle = await this.getHandle(dirname(path));
110
- if (!(handle instanceof FileSystemDirectoryHandle)) {
111
- throw ErrnoError.With('ENOTDIR', dirname(path), 'unlink');
113
+ catch {
114
+ await writable.write({ type: 'seek', position: offset });
112
115
  }
113
- await handle.removeEntry(basename(path), { recursive: true }).catch((ex) => {
114
- throw convertException(ex, path, 'unlink');
115
- });
116
- }
117
- // eslint-disable-next-line @typescript-eslint/require-await
118
- async link(srcpath) {
119
- return;
116
+ await writable.write(buffer);
117
+ await writable.close();
118
+ const { size, lastModified } = await handle.getFile();
119
+ inode.update({ size, mtimeMs: lastModified });
120
+ this.index.set(path, inode);
120
121
  }
121
- async rmdir(path) {
122
- return this.unlink(path);
122
+ /**
123
+ * Do not use!
124
+ * @deprecated @internal @hidden
125
+ */
126
+ async writeFile(path, data) {
127
+ return this.write(path, data, 0);
123
128
  }
124
129
  async mkdir(path, mode, options) {
125
- const existingHandle = await this.getHandle(path).catch((ex) => {
126
- if (ex.code != 'ENOENT') {
127
- throw ex;
128
- }
129
- });
130
- if (existingHandle) {
131
- throw ErrnoError.With('EEXIST', path, 'mkdir');
132
- }
133
- const handle = await this.getHandle(dirname(path));
134
- if (!(handle instanceof FileSystemDirectoryHandle)) {
135
- throw ErrnoError.With('ENOTDIR', path, 'mkdir');
136
- }
137
- await handle.getDirectoryHandle(basename(path), { create: true });
130
+ await super.mkdir(path, mode, options);
131
+ const handle = this.get('directory', dirname(path), 'mkdir');
132
+ const dir = await handle.getDirectoryHandle(basename(path), { create: true }).catch((ex) => _throw(convertException(ex, path)));
133
+ this._handles.set(path, dir);
138
134
  }
139
- async readdir(path) {
140
- const handle = await this.getHandle(path);
141
- if (!(handle instanceof FileSystemDirectoryHandle)) {
142
- throw ErrnoError.With('ENOTDIR', path, 'readdir');
143
- }
144
- const entries = [];
145
- for await (const k of handle.keys()) {
146
- entries.push(k);
147
- }
148
- return entries;
149
- }
150
- async getHandle(path) {
151
- if (this._handles.has(path)) {
152
- return this._handles.get(path);
153
- }
154
- let walked = '/';
155
- for (const part of path.split('/').slice(1)) {
156
- const handle = this._handles.get(walked);
157
- if (!(handle instanceof FileSystemDirectoryHandle)) {
158
- throw ErrnoError.With('ENOTDIR', walked, 'getHandle');
159
- }
160
- walked = join(walked, part);
161
- const child = await handle.getDirectoryHandle(part).catch((ex) => {
162
- switch (ex.name) {
163
- case 'TypeMismatchError':
164
- return handle.getFileHandle(part).catch((ex) => {
165
- //throw convertException(ex, walked, 'getHandle');
166
- });
167
- case 'TypeError':
168
- throw new ErrnoError(Errno.ENOENT, ex.message, walked, 'getHandle');
169
- default:
170
- throw convertException(ex, walked, 'getHandle');
171
- }
172
- });
173
- if (child)
174
- this._handles.set(walked, child);
175
- }
176
- return this._handles.get(path);
135
+ get(kind = null, path, syscall) {
136
+ const handle = this._handles.get(path);
137
+ if (!handle)
138
+ throw ErrnoError.With('ENODATA', path, syscall);
139
+ if (kind && !isKind(handle, kind))
140
+ throw ErrnoError.With(kind == 'directory' ? 'ENOTDIR' : 'EISDIR', path, syscall);
141
+ return handle;
177
142
  }
178
143
  }
179
144
  const _WebAccess = {
180
145
  name: 'WebAccess',
181
146
  options: {
182
147
  handle: { type: 'object', required: true },
148
+ metadata: { type: 'string', required: false },
183
149
  },
184
- isAvailable() {
185
- return typeof FileSystemHandle == 'function';
186
- },
187
- create(options) {
188
- return new WebAccessFS(options.handle);
150
+ async create(options) {
151
+ const fs = new WebAccessFS(options.handle);
152
+ await fs._loadHandles('/', options.handle);
153
+ await fs._loadMetadata(options.metadata);
154
+ return fs;
189
155
  },
190
156
  };
191
157
  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
@@ -5,13 +5,14 @@ import { ErrnoError, Errno } from '@zenfs/core';
5
5
  */
6
6
  function errnoForDOMException(ex) {
7
7
  switch (ex.name) {
8
+ case 'TypeMismatchError':
9
+ return 'EPERM';
8
10
  case 'IndexSizeError':
9
11
  case 'HierarchyRequestError':
10
12
  case 'InvalidCharacterError':
11
13
  case 'InvalidStateError':
12
14
  case 'SyntaxError':
13
15
  case 'NamespaceError':
14
- case 'TypeMismatchError':
15
16
  case 'ConstraintError':
16
17
  case 'VersionError':
17
18
  case 'URLMismatchError':
@@ -57,9 +58,8 @@ function errnoForDOMException(ex) {
57
58
  * @internal
58
59
  */
59
60
  export function convertException(ex, path, syscall) {
60
- if (ex instanceof ErrnoError) {
61
+ if (ex instanceof ErrnoError)
61
62
  return ex;
62
- }
63
63
  const code = ex instanceof DOMException ? Errno[errnoForDOMException(ex)] : Errno.EIO;
64
64
  const error = new ErrnoError(code, ex.message, path, syscall);
65
65
  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,19 +1,19 @@
1
- import { constants, decodeRaw, encodeRaw, Errno, ErrnoError, FileSystem, PreloadFile, Stats, Sync } from '@zenfs/core';
1
+ import { _inode_fields, constants, decodeRaw, encodeRaw, Errno, ErrnoError, FileSystem, LazyFile, Stats, Sync } from '@zenfs/core';
2
2
  import { basename, dirname } from '@zenfs/core/vfs/path.js';
3
- const statsLikeKeys = ['size', 'mode', 'atimeMs', 'mtimeMs', 'ctimeMs', 'birthtimeMs', 'uid', 'gid', 'ino', 'nlink'];
4
3
  function get_stats(node) {
5
4
  const stats = {};
6
- for (const key of statsLikeKeys) {
5
+ for (const key of _inode_fields) {
7
6
  const value = node.getAttribute(key);
8
- stats[key] = value != null ? parseInt(value, 16) : undefined;
7
+ if (value !== null && value !== undefined)
8
+ stats[key] = parseInt(value, 16);
9
9
  }
10
10
  return new Stats(stats);
11
11
  }
12
12
  function set_stats(node, stats) {
13
- for (const key of statsLikeKeys) {
14
- if (stats[key] != undefined) {
15
- node.setAttribute(key, stats[key].toString(16));
16
- }
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));
17
17
  }
18
18
  }
19
19
  function get_paths(node, contents = false) {
@@ -30,11 +30,12 @@ function get_paths(node, contents = false) {
30
30
  export class XMLFS extends Sync(FileSystem) {
31
31
  constructor(
32
32
  /**
33
- * @inheritdoc XMLOptions.root
33
+ * @inheritDoc XMLOptions.root
34
34
  */
35
35
  root = new DOMParser().parseFromString('<fs></fs>', 'application/xml').documentElement) {
36
- super();
36
+ super(0x20786d6c, 'xmltmpfs');
37
37
  this.root = root;
38
+ this.attributes.set('setid');
38
39
  try {
39
40
  this.mkdirSync('/', 0o777, { uid: 0, gid: 0 });
40
41
  }
@@ -44,9 +45,6 @@ export class XMLFS extends Sync(FileSystem) {
44
45
  throw error;
45
46
  }
46
47
  }
47
- metadata() {
48
- return { ...super.metadata(), features: ['setid'] };
49
- }
50
48
  renameSync(oldPath, newPath) {
51
49
  const node = this.get('rename', oldPath);
52
50
  this.remove('rename', node, oldPath);
@@ -57,7 +55,7 @@ export class XMLFS extends Sync(FileSystem) {
57
55
  }
58
56
  openFileSync(path, flag) {
59
57
  const node = this.get('openFile', path);
60
- return new PreloadFile(this, path, flag, get_stats(node), encodeRaw(node.textContent));
58
+ return new LazyFile(this, path, flag, get_stats(node));
61
59
  }
62
60
  createFileSync(path, flag, mode, { uid, gid }) {
63
61
  const parent = this.statSync(dirname(path));
@@ -67,7 +65,7 @@ export class XMLFS extends Sync(FileSystem) {
67
65
  gid: parent.mode & constants.S_ISGID ? parent.gid : gid,
68
66
  });
69
67
  this.create('createFile', path, stats);
70
- return new PreloadFile(this, path, flag, stats);
68
+ return new LazyFile(this, path, flag, stats);
71
69
  }
72
70
  unlinkSync(path) {
73
71
  const node = this.get('unlink', path);
@@ -107,11 +105,23 @@ export class XMLFS extends Sync(FileSystem) {
107
105
  const node = this.get('link', target);
108
106
  this.add('link', node, link);
109
107
  }
110
- syncSync(path, data, stats) {
108
+ syncSync(path, data, stats = {}) {
111
109
  const node = this.get('sync', path);
112
- node.textContent = decodeRaw(data);
110
+ if (data)
111
+ node.textContent = decodeRaw(data);
113
112
  set_stats(node, stats);
114
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
+ }
115
125
  toString() {
116
126
  return new XMLSerializer().serializeToString(this.root);
117
127
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenfs/dom",
3
- "version": "1.1.3",
3
+ "version": "1.1.5",
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.1",
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"]