@zenfs/core 2.3.9 → 2.3.10

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.
@@ -21,6 +21,13 @@ export function _fnOpt(name, fn) {
21
21
  Object.defineProperty(fn, 'name', { value: name });
22
22
  return fn;
23
23
  }
24
+ function _isClass(func) {
25
+ if (!(func && func.constructor === Function) || func.prototype === undefined)
26
+ return false;
27
+ if (Function.prototype !== Object.getPrototypeOf(func))
28
+ return true;
29
+ return Object.getOwnPropertyNames(func.prototype).length > 1;
30
+ }
24
31
  /**
25
32
  * Checks that `options` object is valid for the file system options.
26
33
  * @category Backends and Configuration
@@ -41,7 +48,7 @@ export function checkOptions(backend, options) {
41
48
  throw err(withErrno('EINVAL', 'Missing required option: ' + optName));
42
49
  }
43
50
  const isType = (type, _ = value) => typeof type == 'function'
44
- ? Symbol.hasInstance in type && type.prototype
51
+ ? _isClass(type)
45
52
  ? value instanceof type
46
53
  : type(value)
47
54
  : typeof value === type || value?.constructor?.name === type;
@@ -33,11 +33,6 @@ export declare class PortFS<T extends RPC.Channel = RPC.Channel> extends PortFS_
33
33
  readonly channel: T;
34
34
  readonly timeout: number;
35
35
  readonly port: RPC.Port<T>;
36
- /**
37
- * A map of outstanding RPC requests
38
- * @internal @hidden
39
- */
40
- readonly _executors: Map<string, RPC.Executor>;
41
36
  /**
42
37
  * @hidden
43
38
  */
@@ -19,11 +19,6 @@ export class PortFS extends Async(FileSystem) {
19
19
  channel;
20
20
  timeout;
21
21
  port;
22
- /**
23
- * A map of outstanding RPC requests
24
- * @internal @hidden
25
- */
26
- _executors = new Map();
27
22
  /**
28
23
  * @hidden
29
24
  */
@@ -61,10 +56,8 @@ export class PortFS extends Async(FileSystem) {
61
56
  await this.rpc('touch', path, new Uint8Array(inode.buffer, inode.byteOffset, inode.byteLength));
62
57
  }
63
58
  async sync() {
59
+ await super.sync();
64
60
  await this.rpc('sync');
65
- for (const executor of this._executors.values()) {
66
- await executor.promise.catch(() => { });
67
- }
68
61
  }
69
62
  async createFile(path, options) {
70
63
  if (options instanceof Inode)
package/dist/config.d.ts CHANGED
@@ -87,3 +87,4 @@ export declare function addDevice(driver: DeviceDriver, options?: object): Devic
87
87
  * @see Configuration
88
88
  */
89
89
  export declare function configure<T extends ConfigMounts>(configuration: Partial<Configuration<T>>): Promise<void>;
90
+ export declare function sync(): Promise<void>;
package/dist/config.js CHANGED
@@ -65,7 +65,7 @@ export async function resolveMountConfig(configuration, _depth = 0) {
65
65
  * @category Backends and Configuration
66
66
  */
67
67
  export async function configureSingle(configuration) {
68
- if (!isBackendConfig(configuration)) {
68
+ if (!isMountConfig(configuration)) {
69
69
  throw new TypeError('Invalid single mount point configuration');
70
70
  }
71
71
  const resolved = await resolveMountConfig(configuration);
@@ -137,3 +137,7 @@ export async function configure(configuration) {
137
137
  await mount('/dev', devfs);
138
138
  }
139
139
  }
140
+ export async function sync() {
141
+ for (const fs of mounts.values())
142
+ await fs.sync();
143
+ }
@@ -93,9 +93,8 @@ export class IndexFS extends FileSystem {
93
93
  throw withErrno('ENOTDIR');
94
94
  if (isDir && isUnlink)
95
95
  throw withErrno('EISDIR');
96
- if (isDir && this.readdirSync(path).length)
97
- throw withErrno('ENOTEMPTY');
98
- this.index.delete(path);
96
+ if (!isDir)
97
+ this.index.delete(path);
99
98
  }
100
99
  async unlink(path) {
101
100
  this._remove(path, true);
@@ -107,10 +106,17 @@ export class IndexFS extends FileSystem {
107
106
  }
108
107
  async rmdir(path) {
109
108
  this._remove(path, false);
109
+ const entries = await this.readdir(path);
110
+ if (entries.length)
111
+ throw withErrno('ENOTEMPTY');
112
+ this.index.delete(path);
110
113
  await this.remove(path);
111
114
  }
112
115
  rmdirSync(path) {
113
116
  this._remove(path, false);
117
+ if (this.readdirSync(path).length)
118
+ throw withErrno('ENOTEMPTY');
119
+ this.index.delete(path);
114
120
  this.removeSync(path);
115
121
  }
116
122
  create(path, options) {
@@ -10,14 +10,17 @@ export function isPort(port) {
10
10
  * Creates a new RPC port from a `Worker` or `MessagePort` that extends `EventTarget`
11
11
  */
12
12
  export function fromWeb(port) {
13
+ const _handlers = new Map();
13
14
  return {
14
15
  channel: port,
15
16
  send: port.postMessage.bind(port),
16
17
  addHandler(handler) {
17
- port.addEventListener('message', (event) => handler(event.data));
18
+ const _handler = (event) => handler(event.data);
19
+ _handlers.set(handler, _handler);
20
+ port.addEventListener('message', _handler);
18
21
  },
19
22
  removeHandler(handler) {
20
- port.removeEventListener('message', (event) => handler(event.data));
23
+ port.removeEventListener('message', _handlers.get(handler));
21
24
  },
22
25
  };
23
26
  }
@@ -133,7 +136,6 @@ function disposeExecutors(id) {
133
136
  if (typeof executor.timeout == 'object')
134
137
  executor.timeout.unref();
135
138
  }
136
- executor.fs._executors.delete(id);
137
139
  executors.delete(id);
138
140
  }
139
141
  /**
@@ -145,15 +147,14 @@ export function request(request, { port, timeout: ms = 1000, fs }) {
145
147
  if (!port)
146
148
  throw err(withErrno('EINVAL', 'Can not make an RPC request without a port'));
147
149
  const { resolve, reject, promise } = Promise.withResolvers();
148
- const id = Math.random().toString(16).slice(10);
150
+ const id = Math.random().toString(16).slice(5);
149
151
  const timeout = setTimeout(() => {
150
- const error = err(withErrno('EIO', 'RPC Failed'));
152
+ const error = err(withErrno('ETIMEDOUT', 'RPC request timed out'));
151
153
  error.stack += stack;
152
154
  disposeExecutors(id);
153
155
  reject(error);
154
156
  }, ms);
155
157
  const executor = { resolve, reject, promise, fs, timeout };
156
- fs._executors.set(id, executor);
157
158
  executors.set(id, executor);
158
159
  port.send({ ...request, _zenfs: true, id, stack });
159
160
  return promise;
@@ -20,10 +20,8 @@ export interface AsyncMixin extends Pick<FileSystem, Exclude<_SyncFSKeys, 'exist
20
20
  * @deprecated Use {@link sync | `sync`} instead
21
21
  */
22
22
  queueDone(): Promise<void>;
23
- /**
24
- * @deprecated Use {@link sync | `sync`} instead
25
- */
26
23
  ready(): Promise<void>;
24
+ sync(): Promise<void>;
27
25
  }
28
26
  /**
29
27
  * Async() implements synchronous methods on an asynchronous file system
@@ -29,8 +29,9 @@ export function Async(FS) {
29
29
  return this.sync();
30
30
  }
31
31
  _promise = Promise.resolve();
32
- _async(promise) {
33
- this._promise = this._promise.then(() => promise);
32
+ _async(thunk) {
33
+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
34
+ this._promise = this._promise.finally(() => thunk());
34
35
  }
35
36
  _isInitialized = false;
36
37
  /** Tracks how many updates to the sync. cache we skipped during initialization */
@@ -41,9 +42,9 @@ export function Async(FS) {
41
42
  }
42
43
  async ready() {
43
44
  await super.ready();
44
- await this._promise;
45
45
  if (this._isInitialized || this.attributes.has('no_async_preload'))
46
46
  return;
47
+ await this._promise;
47
48
  this.checkSync();
48
49
  await this._sync.ready();
49
50
  // optimization: for 2 storeFS', we copy at a lower abstraction level.
@@ -79,7 +80,7 @@ export function Async(FS) {
79
80
  renameSync(oldPath, newPath) {
80
81
  this.checkSync();
81
82
  this._sync.renameSync(oldPath, newPath);
82
- this._async(this.rename(oldPath, newPath));
83
+ this._async(() => this.rename(oldPath, newPath));
83
84
  }
84
85
  statSync(path) {
85
86
  this.checkSync();
@@ -88,27 +89,29 @@ export function Async(FS) {
88
89
  touchSync(path, metadata) {
89
90
  this.checkSync();
90
91
  this._sync.touchSync(path, metadata);
91
- this._async(this.touch(path, metadata));
92
+ this._async(() => this.touch(path, metadata));
92
93
  }
93
94
  createFileSync(path, options) {
94
95
  this.checkSync();
95
- this._async(this.createFile(path, options));
96
- return this._sync.createFileSync(path, options);
96
+ const result = this._sync.createFileSync(path, options);
97
+ this._async(() => this.createFile(path, options));
98
+ return result;
97
99
  }
98
100
  unlinkSync(path) {
99
101
  this.checkSync();
100
- this._async(this.unlink(path));
101
102
  this._sync.unlinkSync(path);
103
+ this._async(() => this.unlink(path));
102
104
  }
103
105
  rmdirSync(path) {
104
106
  this.checkSync();
105
107
  this._sync.rmdirSync(path);
106
- this._async(this.rmdir(path));
108
+ this._async(() => this.rmdir(path));
107
109
  }
108
110
  mkdirSync(path, options) {
109
111
  this.checkSync();
110
- this._async(this.mkdir(path, options));
111
- return this._sync.mkdirSync(path, options);
112
+ const result = this._sync.mkdirSync(path, options);
113
+ this._async(() => this.mkdir(path, options));
114
+ return result;
112
115
  }
113
116
  readdirSync(path) {
114
117
  this.checkSync();
@@ -117,12 +120,12 @@ export function Async(FS) {
117
120
  linkSync(srcpath, dstpath) {
118
121
  this.checkSync();
119
122
  this._sync.linkSync(srcpath, dstpath);
120
- this._async(this.link(srcpath, dstpath));
123
+ this._async(() => this.link(srcpath, dstpath));
121
124
  }
122
125
  async sync() {
123
126
  if (!this.attributes.has('no_async_preload') && this._sync)
124
127
  this._sync.syncSync();
125
- await this._promise;
128
+ await this._promise.catch(() => { });
126
129
  }
127
130
  syncSync() {
128
131
  this.checkSync();
@@ -139,7 +142,7 @@ export function Async(FS) {
139
142
  writeSync(path, buffer, offset) {
140
143
  this.checkSync();
141
144
  this._sync.writeSync(path, buffer, offset);
142
- this._async(this.write(path, buffer, offset));
145
+ this._async(() => this.write(path, buffer, offset));
143
146
  }
144
147
  streamWrite(path, options) {
145
148
  this.checkSync();
@@ -186,18 +189,27 @@ export function Async(FS) {
186
189
  * Patch all async methods to also call their synchronous counterparts unless called from themselves (either sync or async)
187
190
  */
188
191
  _patchAsync() {
189
- debug(`Async: patched ${_asyncFSKeys.length} methods`);
190
- for (const key of _asyncFSKeys) {
192
+ const noPatch = ['read', 'readdir', 'stat', 'exists'];
193
+ const toPatch = _asyncFSKeys.filter(key => !noPatch.includes(key));
194
+ for (const key of toPatch) {
191
195
  // TS does not narrow the union based on the key
192
196
  const originalMethod = this[key].bind(this);
193
- this[key] = async (...args) => {
194
- const result = await originalMethod(...args);
195
- const stack = new Error().stack.split('\n').slice(2).join('\n');
197
+ function isInLoop(depth, error) {
198
+ if (!error) {
199
+ error = new Error();
200
+ Error.captureStackTrace(error, isInLoop);
201
+ }
202
+ if (!error.stack)
203
+ return false;
204
+ const stack = error.stack.split('\n').slice(depth).join('\n');
196
205
  // From the async queue
197
- if (!stack
198
- || stack.includes(`at <computed> [as ${key}]`)
206
+ return (stack.includes(`at <computed> [as ${key}]`)
199
207
  || stack.includes(`at async <computed> [as ${key}]`)
200
- || stack.includes(`${key}Sync `))
208
+ || stack.includes(`${key}Sync `));
209
+ }
210
+ this[key] = async (...args) => {
211
+ const result = await originalMethod(...args);
212
+ if (isInLoop(2))
201
213
  return result;
202
214
  if (!this._isInitialized) {
203
215
  this._skippedCacheUpdates++;
@@ -208,10 +220,7 @@ export function Async(FS) {
208
220
  this._sync?.[`${key}Sync`]?.(...args);
209
221
  }
210
222
  catch (e) {
211
- const stack = e.stack.split('\n').slice(3).join('\n');
212
- if (stack.includes(`at <computed> [as ${key}]`)
213
- || stack.includes(`at async <computed> [as ${key}]`)
214
- || stack.includes(`${key}Sync `))
223
+ if (isInLoop(3, e))
215
224
  return result;
216
225
  e.message += ' (Out of sync!)';
217
226
  throw err(e);
@@ -219,6 +228,7 @@ export function Async(FS) {
219
228
  return result;
220
229
  };
221
230
  }
231
+ debug(`Async: patched ${toPatch.length} methods`);
222
232
  }
223
233
  }
224
234
  return AsyncFS;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenfs/core",
3
- "version": "2.3.9",
3
+ "version": "2.3.10",
4
4
  "description": "A filesystem, anywhere",
5
5
  "funding": {
6
6
  "type": "individual",
@@ -21,13 +21,13 @@ await suite('Timeout', { timeout: 1000 }, () => {
21
21
  },
22
22
  });
23
23
 
24
- await assert.rejects(configured, { code: 'EIO', message: /RPC Failed/ });
24
+ await assert.rejects(configured, { code: 'ETIMEDOUT' });
25
25
  });
26
26
 
27
27
  test('Remote not attached', async () => {
28
28
  const configured = configureSingle({ backend: Port, port: timeoutChannel.port1, timeout: 100 });
29
29
 
30
- await assert.rejects(configured, { code: 'EIO', message: /RPC Failed/ });
30
+ await assert.rejects(configured, { code: 'ETIMEDOUT' });
31
31
  });
32
32
 
33
33
  after(() => {
package/tests/common.ts CHANGED
@@ -1,12 +1,17 @@
1
1
  import { join, resolve } from 'node:path';
2
2
  import { fs as defaultFS } from '../dist/index.js';
3
3
  import { setupLogs } from './logs.js';
4
+ import { styleText } from 'node:util';
4
5
  export type * from '../dist/index.js';
5
6
 
6
7
  setupLogs();
7
8
 
8
9
  const setupPath = resolve(process.env.SETUP || join(import.meta.dirname, 'setup/memory.ts'));
9
10
 
11
+ process.on('unhandledRejection', (reason: Error) => {
12
+ console.error('Unhandled rejection:', styleText('red', reason.stack || reason.message));
13
+ });
14
+
10
15
  const setup = await import(setupPath).catch(error => {
11
16
  console.log('Failed to import test setup:');
12
17
  throw error;
@@ -3,7 +3,8 @@ import assert from 'node:assert/strict';
3
3
  import type { OpenMode, PathLike } from 'node:fs';
4
4
  import { suite, test } from 'node:test';
5
5
  import { promisify } from 'node:util';
6
- import { fs, type Callback } from '../common.js';
6
+ import { sync } from '../../dist/config.js';
7
+ import { fs } from '../common.js';
7
8
 
8
9
  const filepath = 'x.txt';
9
10
  const expected = 'xyz\n';
@@ -73,6 +74,8 @@ suite('read', () => {
73
74
  const path = '/text.txt';
74
75
 
75
76
  fs.writeFileSync(path, 'hello world');
77
+ await sync();
78
+
76
79
  const fd: number = (await promisify<PathLike, OpenMode, number | string>(fs.open)(path, 0, 0)) as any;
77
80
 
78
81
  const read = promisify(fs.read);
@@ -1,5 +1,6 @@
1
1
  import assert from 'node:assert/strict';
2
2
  import { suite, test } from 'node:test';
3
+ import { sync } from '../../dist/config.js';
3
4
  import { fs } from '../common.js';
4
5
 
5
6
  const n_files = 130;
@@ -14,6 +15,7 @@ suite('Scaling', () => {
14
15
  fs.writeFileSync('/n/' + i, i.toString(16));
15
16
  }
16
17
 
18
+ await sync();
17
19
  assert.equal(fs.readdirSync('/n').length, n_files);
18
20
 
19
21
  const results = [];
@@ -1,6 +1,6 @@
1
- import { after } from 'node:test';
1
+ import { after, afterEach } from 'node:test';
2
2
  import { MessageChannel } from 'node:worker_threads';
3
- import { InMemory, Port, configureSingle, fs, resolveMountConfig, resolveRemoteMount } from '../../dist/index.js';
3
+ import { InMemory, Port, configureSingle, fs, resolveMountConfig, resolveRemoteMount, sync } from '../../dist/index.js';
4
4
  import { copySync, data } from '../setup.js';
5
5
 
6
6
  const { port1: localPort, port2: remotePort } = new MessageChannel();
@@ -15,6 +15,8 @@ await resolveRemoteMount(remotePort, tmpfs);
15
15
 
16
16
  await configureSingle({ backend: Port, port: localPort });
17
17
 
18
+ afterEach(sync);
19
+
18
20
  after(() => {
19
21
  localPort.close();
20
22
  remotePort.close();