@zenfs/core 0.9.6 → 0.10.0
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/dist/backends/AsyncStore.d.ts +3 -2
- package/dist/backends/AsyncStore.js +40 -33
- package/dist/backends/Fetch.d.ts +84 -0
- package/dist/backends/Fetch.js +171 -0
- package/dist/backends/InMemory.d.ts +1 -1
- package/dist/backends/Index.d.ts +7 -10
- package/dist/backends/Index.js +26 -24
- package/dist/backends/Locked.d.ts +11 -11
- package/dist/backends/Locked.js +50 -49
- package/dist/backends/Overlay.js +22 -22
- package/dist/backends/SyncStore.d.ts +6 -6
- package/dist/backends/SyncStore.js +30 -30
- package/dist/backends/backend.d.ts +5 -4
- package/dist/backends/backend.js +6 -6
- package/dist/backends/port/fs.d.ts +124 -0
- package/dist/backends/port/fs.js +241 -0
- package/dist/backends/port/rpc.d.ts +60 -0
- package/dist/backends/port/rpc.js +71 -0
- package/dist/backends/port/store.d.ts +30 -0
- package/dist/backends/port/store.js +142 -0
- package/dist/browser.min.js +4 -4
- package/dist/browser.min.js.map +4 -4
- package/dist/config.d.ts +9 -11
- package/dist/config.js +13 -13
- package/dist/emulation/async.d.ts +76 -77
- package/dist/emulation/async.js +45 -45
- package/dist/emulation/dir.js +8 -7
- package/dist/emulation/index.d.ts +1 -1
- package/dist/emulation/index.js +1 -1
- package/dist/emulation/path.d.ts +3 -2
- package/dist/emulation/path.js +19 -45
- package/dist/emulation/promises.d.ts +112 -113
- package/dist/emulation/promises.js +167 -173
- package/dist/emulation/shared.d.ts +6 -17
- package/dist/emulation/shared.js +9 -9
- package/dist/emulation/streams.js +3 -2
- package/dist/emulation/sync.d.ts +71 -64
- package/dist/emulation/sync.js +62 -63
- package/dist/{ApiError.d.ts → error.d.ts} +15 -15
- package/dist/error.js +292 -0
- package/dist/file.d.ts +6 -4
- package/dist/file.js +17 -9
- package/dist/filesystem.d.ts +1 -1
- package/dist/filesystem.js +18 -15
- package/dist/index.d.ts +4 -1
- package/dist/index.js +4 -1
- package/dist/mutex.js +4 -3
- package/dist/stats.d.ts +7 -7
- package/dist/stats.js +50 -10
- package/dist/utils.d.ts +10 -9
- package/dist/utils.js +15 -15
- package/package.json +5 -5
- package/readme.md +19 -11
- package/src/backends/AsyncStore.ts +42 -36
- package/src/backends/Fetch.ts +230 -0
- package/src/backends/Index.ts +33 -29
- package/src/backends/Locked.ts +50 -49
- package/src/backends/Overlay.ts +24 -24
- package/src/backends/SyncStore.ts +34 -34
- package/src/backends/backend.ts +13 -11
- package/src/backends/port/fs.ts +308 -0
- package/src/backends/port/readme.md +59 -0
- package/src/backends/port/rpc.ts +144 -0
- package/src/backends/port/store.ts +187 -0
- package/src/config.ts +25 -29
- package/src/emulation/async.ts +191 -199
- package/src/emulation/dir.ts +8 -8
- package/src/emulation/index.ts +1 -1
- package/src/emulation/path.ts +25 -49
- package/src/emulation/promises.ts +286 -287
- package/src/emulation/shared.ts +14 -23
- package/src/emulation/streams.ts +9 -8
- package/src/emulation/sync.ts +182 -182
- package/src/{ApiError.ts → error.ts} +91 -89
- package/src/file.ts +23 -13
- package/src/filesystem.ts +26 -22
- package/src/index.ts +4 -1
- package/src/mutex.ts +6 -4
- package/src/stats.ts +32 -23
- package/src/utils.ts +23 -24
- package/tsconfig.json +4 -3
- package/dist/ApiError.js +0 -292
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { ErrnoError, Errno } from '../../error.js';
|
|
3
|
+
import type { Cred } from '../../cred.js';
|
|
4
|
+
import { FileSystem, type FileSystemMetadata, Async } from '../../filesystem.js';
|
|
5
|
+
import { File } from '../../file.js';
|
|
6
|
+
import { Stats, type FileType } from '../../stats.js';
|
|
7
|
+
import { InMemory } from '../InMemory.js';
|
|
8
|
+
import type { SyncStoreFS } from '../SyncStore.js';
|
|
9
|
+
import type { Backend } from '../backend.js';
|
|
10
|
+
import * as RPC from './rpc.js';
|
|
11
|
+
import type { ExtractProperties } from 'utilium';
|
|
12
|
+
import type { FileReadResult } from 'node:fs/promises';
|
|
13
|
+
|
|
14
|
+
type FileMethods = ExtractProperties<File, (...args: any[]) => Promise<any>>;
|
|
15
|
+
type FileMethod = keyof FileMethods;
|
|
16
|
+
interface FileRequest<TMethod extends FileMethod & string = FileMethod & string> extends RPC.Request<'file', TMethod, Parameters<FileMethods[TMethod]>> {
|
|
17
|
+
fd: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class PortFile extends File {
|
|
21
|
+
constructor(
|
|
22
|
+
public readonly fs: PortFS,
|
|
23
|
+
public readonly fd: number,
|
|
24
|
+
public readonly path: string,
|
|
25
|
+
public position: number
|
|
26
|
+
) {
|
|
27
|
+
super();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public rpc<const T extends FileMethod & string>(method: T, ...args: Parameters<FileMethods[T]>): Promise<Awaited<ReturnType<FileMethods[T]>>> {
|
|
31
|
+
return RPC.request<FileRequest<T>, Awaited<ReturnType<FileMethods[T]>>>(
|
|
32
|
+
{
|
|
33
|
+
scope: 'file',
|
|
34
|
+
fd: this.fd,
|
|
35
|
+
method,
|
|
36
|
+
args,
|
|
37
|
+
},
|
|
38
|
+
this.fs.options
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public stat(): Promise<Stats> {
|
|
43
|
+
return this.rpc('stat');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public statSync(): Stats {
|
|
47
|
+
throw ErrnoError.With('ENOSYS', this.path, 'PortFile.stat');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public truncate(len: number): Promise<void> {
|
|
51
|
+
return this.rpc('truncate', len);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public truncateSync(): void {
|
|
55
|
+
throw ErrnoError.With('ENOSYS', this.path, 'PortFile.truncate');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public write(buffer: Uint8Array, offset?: number, length?: number, position?: number): Promise<number> {
|
|
59
|
+
return this.rpc('write', buffer, offset, length, position);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public writeSync(): number {
|
|
63
|
+
throw ErrnoError.With('ENOSYS', this.path, 'PortFile.write');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public async read<TBuffer extends NodeJS.ArrayBufferView>(buffer: TBuffer, offset?: number, length?: number, position?: number): Promise<FileReadResult<TBuffer>> {
|
|
67
|
+
return (await this.rpc('read', buffer, offset, length, position)) as FileReadResult<TBuffer>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public readSync(): number {
|
|
71
|
+
throw ErrnoError.With('ENOSYS', this.path, 'PortFile.read');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public chown(uid: number, gid: number): Promise<void> {
|
|
75
|
+
return this.rpc('chown', uid, gid);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
public chownSync(): void {
|
|
79
|
+
throw ErrnoError.With('ENOSYS', this.path, 'PortFile.chown');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
public chmod(mode: number): Promise<void> {
|
|
83
|
+
return this.rpc('chmod', mode);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
public chmodSync(): void {
|
|
87
|
+
throw ErrnoError.With('ENOSYS', this.path, 'PortFile.chmod');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
public utimes(atime: Date, mtime: Date): Promise<void> {
|
|
91
|
+
return this.rpc('utimes', atime, mtime);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
public utimesSync(): void {
|
|
95
|
+
throw ErrnoError.With('ENOSYS', this.path, 'PortFile.utimes');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
public _setType(type: FileType): Promise<void> {
|
|
99
|
+
return this.rpc('_setType', type);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
public _setTypeSync(): void {
|
|
103
|
+
throw ErrnoError.With('ENOSYS', this.path, 'PortFile._setType');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
public close(): Promise<void> {
|
|
107
|
+
return this.rpc('close');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
public closeSync(): void {
|
|
111
|
+
throw ErrnoError.With('ENOSYS', this.path, 'PortFile.close');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
public sync(): Promise<void> {
|
|
115
|
+
return this.rpc('sync');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
public syncSync(): void {
|
|
119
|
+
throw ErrnoError.With('ENOSYS', this.path, 'PortFile.sync');
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
type FSMethods = ExtractProperties<FileSystem, (...args: any[]) => Promise<any> | FileSystemMetadata>;
|
|
124
|
+
type FSMethod = keyof FSMethods;
|
|
125
|
+
type FSRequest<TMethod extends FSMethod = FSMethod> = RPC.Request<'fs', TMethod, Parameters<FSMethods[TMethod]>>;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* PortFS lets you access a ZenFS instance that is running in a port, or the other way around.
|
|
129
|
+
*
|
|
130
|
+
* Note that synchronous operations are not permitted on the PortFS, regardless
|
|
131
|
+
* of the configuration option of the remote FS.
|
|
132
|
+
*/
|
|
133
|
+
export class PortFS extends Async(FileSystem) {
|
|
134
|
+
public readonly port: RPC.Port;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* @hidden
|
|
138
|
+
*/
|
|
139
|
+
_sync: SyncStoreFS = InMemory.create({ name: 'port-tmpfs' });
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Constructs a new PortFS instance that connects with ZenFS running on
|
|
143
|
+
* the specified port.
|
|
144
|
+
*/
|
|
145
|
+
public constructor(public readonly options: RPC.Options) {
|
|
146
|
+
super();
|
|
147
|
+
this.port = options.port;
|
|
148
|
+
RPC.attach<RPC.Response>(this.port, RPC.handleResponse);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
public metadata(): FileSystemMetadata {
|
|
152
|
+
return {
|
|
153
|
+
...super.metadata(),
|
|
154
|
+
name: 'PortFS',
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
protected rpc<const T extends FSMethod>(method: T, ...args: Parameters<FSMethods[T]>): Promise<Awaited<ReturnType<FSMethods[T]>>> {
|
|
159
|
+
return RPC.request<FSRequest<T>, Awaited<ReturnType<FSMethods[T]>>>(
|
|
160
|
+
{
|
|
161
|
+
scope: 'fs',
|
|
162
|
+
method,
|
|
163
|
+
args,
|
|
164
|
+
},
|
|
165
|
+
{ ...this.options, fs: this }
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
public async ready(): Promise<this> {
|
|
170
|
+
await this.rpc('ready');
|
|
171
|
+
await super.ready();
|
|
172
|
+
return this;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
public rename(oldPath: string, newPath: string, cred: Cred): Promise<void> {
|
|
176
|
+
return this.rpc('rename', oldPath, newPath, cred);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
public async stat(p: string, cred: Cred): Promise<Stats> {
|
|
180
|
+
return new Stats(await this.rpc('stat', p, cred));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
public sync(path: string, data: Uint8Array, stats: Readonly<Stats>): Promise<void> {
|
|
184
|
+
return this.rpc('sync', path, data, stats);
|
|
185
|
+
}
|
|
186
|
+
public openFile(p: string, flag: string, cred: Cred): Promise<File> {
|
|
187
|
+
return this.rpc('openFile', p, flag, cred);
|
|
188
|
+
}
|
|
189
|
+
public createFile(p: string, flag: string, mode: number, cred: Cred): Promise<File> {
|
|
190
|
+
return this.rpc('createFile', p, flag, mode, cred);
|
|
191
|
+
}
|
|
192
|
+
public unlink(p: string, cred: Cred): Promise<void> {
|
|
193
|
+
return this.rpc('unlink', p, cred);
|
|
194
|
+
}
|
|
195
|
+
public rmdir(p: string, cred: Cred): Promise<void> {
|
|
196
|
+
return this.rpc('rmdir', p, cred);
|
|
197
|
+
}
|
|
198
|
+
public mkdir(p: string, mode: number, cred: Cred): Promise<void> {
|
|
199
|
+
return this.rpc('mkdir', p, mode, cred);
|
|
200
|
+
}
|
|
201
|
+
public readdir(p: string, cred: Cred): Promise<string[]> {
|
|
202
|
+
return this.rpc('readdir', p, cred);
|
|
203
|
+
}
|
|
204
|
+
public exists(p: string, cred: Cred): Promise<boolean> {
|
|
205
|
+
return this.rpc('exists', p, cred);
|
|
206
|
+
}
|
|
207
|
+
public link(srcpath: string, dstpath: string, cred: Cred): Promise<void> {
|
|
208
|
+
return this.rpc('link', srcpath, dstpath, cred);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
let nextFd = 0;
|
|
213
|
+
|
|
214
|
+
const descriptors: Map<number, File> = new Map();
|
|
215
|
+
|
|
216
|
+
type FileOrFSRequest = FSRequest | FileRequest;
|
|
217
|
+
|
|
218
|
+
async function handleRequest(port: RPC.Port, fs: FileSystem, request: FileOrFSRequest): Promise<void> {
|
|
219
|
+
if (!RPC.isMessage(request)) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const { method, args, id, scope, stack } = request;
|
|
223
|
+
|
|
224
|
+
let value,
|
|
225
|
+
error: boolean = false;
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
switch (scope) {
|
|
229
|
+
case 'fs':
|
|
230
|
+
// @ts-expect-error 2556
|
|
231
|
+
value = await fs[method](...args);
|
|
232
|
+
if (value instanceof File) {
|
|
233
|
+
descriptors.set(++nextFd, value);
|
|
234
|
+
value = {
|
|
235
|
+
fd: nextFd,
|
|
236
|
+
path: value.path,
|
|
237
|
+
position: value.position,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
break;
|
|
241
|
+
case 'file':
|
|
242
|
+
const { fd } = request;
|
|
243
|
+
if (!descriptors.has(fd)) {
|
|
244
|
+
throw new ErrnoError(Errno.EBADF);
|
|
245
|
+
}
|
|
246
|
+
// @ts-expect-error 2556
|
|
247
|
+
value = await descriptors.get(fd);
|
|
248
|
+
if (method == 'close') {
|
|
249
|
+
descriptors.delete(fd);
|
|
250
|
+
}
|
|
251
|
+
break;
|
|
252
|
+
default:
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
} catch (e) {
|
|
256
|
+
value = e;
|
|
257
|
+
error = true;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
port.postMessage({
|
|
261
|
+
_zenfs: true,
|
|
262
|
+
scope,
|
|
263
|
+
id,
|
|
264
|
+
error,
|
|
265
|
+
method,
|
|
266
|
+
stack,
|
|
267
|
+
value: value instanceof ErrnoError ? value.toJSON() : value,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function attachFS(port: RPC.Port, fs: FileSystem): void {
|
|
272
|
+
RPC.attach<FileOrFSRequest>(port, request => handleRequest(port, fs, request));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function detachFS(port: RPC.Port, fs: FileSystem): void {
|
|
276
|
+
RPC.detach<FileOrFSRequest>(port, request => handleRequest(port, fs, request));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export const Port = {
|
|
280
|
+
name: 'Port',
|
|
281
|
+
|
|
282
|
+
options: {
|
|
283
|
+
port: {
|
|
284
|
+
type: 'object',
|
|
285
|
+
required: true,
|
|
286
|
+
description: 'The target port that you want to connect to',
|
|
287
|
+
validator(port: RPC.Port) {
|
|
288
|
+
// Check for a `postMessage` function.
|
|
289
|
+
if (typeof port?.postMessage != 'function') {
|
|
290
|
+
throw new ErrnoError(Errno.EINVAL, 'option must be a port.');
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
timeout: {
|
|
295
|
+
type: 'number',
|
|
296
|
+
required: false,
|
|
297
|
+
description: 'How long to wait before the request times out',
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
|
|
301
|
+
async isAvailable(port?: RPC.Port): Promise<boolean> {
|
|
302
|
+
return true;
|
|
303
|
+
},
|
|
304
|
+
|
|
305
|
+
create(options: RPC.Options) {
|
|
306
|
+
return new PortFS(options);
|
|
307
|
+
},
|
|
308
|
+
} satisfies Backend<PortFS, RPC.Options>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Port Backend
|
|
2
|
+
|
|
3
|
+
A backend for usage with ports and workers. See the examples below.
|
|
4
|
+
|
|
5
|
+
#### Accessing an FS on a remote Worker from the main thread
|
|
6
|
+
|
|
7
|
+
Main:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { configure } from '@zenfs/core';
|
|
11
|
+
import { Port } from '@zenfs/port';
|
|
12
|
+
import { Worker } from 'node:worker_threads';
|
|
13
|
+
|
|
14
|
+
const worker = new Worker('worker.js');
|
|
15
|
+
|
|
16
|
+
await configure({
|
|
17
|
+
mounts: {
|
|
18
|
+
'/worker': {
|
|
19
|
+
backend: Port,
|
|
20
|
+
port: worker,
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Worker:
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { InMemory, resolveMountConfig } from '@zenfs/core';
|
|
30
|
+
import { attachFS } from '@zenfs/port';
|
|
31
|
+
import { parentPort } from 'node:worker_threads';
|
|
32
|
+
|
|
33
|
+
const tmpfs = await resolveMountConfig({ backend: InMemory, name: 'tmp' });
|
|
34
|
+
attachFS(parentPort, tmpfs);
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
If you are using using web workers, you would use `self` instead of importing `parentPort` in the worker, and would not need to import `Worker` in the main thread.
|
|
38
|
+
|
|
39
|
+
#### Using with multiple ports on the same thread
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import { InMemory, fs, resolveMountConfig } from '@zenfs/core';
|
|
43
|
+
import { Port, attachFS } from '@zenfs/port';
|
|
44
|
+
import { MessageChannel } from 'node:worker_threads';
|
|
45
|
+
|
|
46
|
+
const { port1, port2 } = new MessageChannel();
|
|
47
|
+
|
|
48
|
+
const tmpfs = await resolveMountConfig({ backend: InMemory, name: 'tmp' });
|
|
49
|
+
attachFS(port2, tmpfs);
|
|
50
|
+
fs.mount('/port', await resolveMountConfig({ backend: Port, port: port1 }));
|
|
51
|
+
console.log('/port');
|
|
52
|
+
|
|
53
|
+
const content = 'FS is in a port';
|
|
54
|
+
|
|
55
|
+
await fs.promises.writeFile('/port/test', content);
|
|
56
|
+
|
|
57
|
+
fs.readFileSync('/tmp/test', 'utf8'); // FS is in a port
|
|
58
|
+
await fs.promises.readFile('/port/test', 'utf8'); // FS is in a port
|
|
59
|
+
```
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
/// !<reference lib="DOM" />
|
|
3
|
+
import type { TransferListItem } from 'worker_threads';
|
|
4
|
+
import { ErrnoError, Errno, type ErrnoErrorJSON } from '../../error.js';
|
|
5
|
+
import { PortFile, type PortFS } from './fs.js';
|
|
6
|
+
|
|
7
|
+
type _MessageEvent<T = any> = T | { data: T };
|
|
8
|
+
|
|
9
|
+
export interface Port {
|
|
10
|
+
postMessage(value: unknown, transferList?: ReadonlyArray<TransferListItem>): void;
|
|
11
|
+
on?(event: 'message', listener: (value: unknown) => void): this;
|
|
12
|
+
off?(event: 'message', listener: (value: unknown) => void): this;
|
|
13
|
+
addEventListener?(type: 'message', listener: (this: Port, ev: _MessageEvent) => void): void;
|
|
14
|
+
removeEventListener?(type: 'message', listener: (this: Port, ev: _MessageEvent) => void): void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface Options {
|
|
18
|
+
/**
|
|
19
|
+
* The target port that you want to connect to, or the current port if in a port context.
|
|
20
|
+
*/
|
|
21
|
+
port: Port;
|
|
22
|
+
/**
|
|
23
|
+
* How long to wait for a request to complete
|
|
24
|
+
*/
|
|
25
|
+
timeout?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* An RPC message
|
|
30
|
+
*/
|
|
31
|
+
export interface Message<TScope extends string = string, TMethod extends string = string> {
|
|
32
|
+
_zenfs: true;
|
|
33
|
+
scope: TScope;
|
|
34
|
+
id: string;
|
|
35
|
+
method: TMethod;
|
|
36
|
+
stack: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface Request<TScope extends string = string, TMethod extends string = string, TArgs extends unknown[] = unknown[]> extends Message<TScope, TMethod> {
|
|
40
|
+
args: TArgs;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface Response<TScope extends string = string, TMethod extends string = string, TValue = unknown> extends Message<TScope, TMethod> {
|
|
44
|
+
error: boolean;
|
|
45
|
+
value: Awaited<TValue> extends File ? FileData : Awaited<TValue>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface FileData {
|
|
49
|
+
fd: number;
|
|
50
|
+
path: string;
|
|
51
|
+
position: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isFileData(value: unknown): value is FileData {
|
|
55
|
+
return typeof value == 'object' && value != null && 'fd' in value && 'path' in value && 'position' in value;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export { FileData as File };
|
|
59
|
+
|
|
60
|
+
// general types
|
|
61
|
+
|
|
62
|
+
export function isMessage(arg: unknown): arg is Message<string, string> {
|
|
63
|
+
return typeof arg == 'object' && arg != null && '_zenfs' in arg && !!arg._zenfs;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
type _Executor = Parameters<ConstructorParameters<typeof Promise<any>>[0]>;
|
|
67
|
+
|
|
68
|
+
export interface Executor {
|
|
69
|
+
resolve: _Executor[0];
|
|
70
|
+
reject: _Executor[1];
|
|
71
|
+
fs?: PortFS;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const executors: Map<string, Executor> = new Map();
|
|
75
|
+
|
|
76
|
+
export function request<const TRequest extends Request, TValue>(
|
|
77
|
+
request: Omit<TRequest, 'id' | 'stack' | '_zenfs'>,
|
|
78
|
+
{ port, timeout = 1000, fs }: Partial<Options> & { fs?: PortFS } = {}
|
|
79
|
+
): Promise<TValue> {
|
|
80
|
+
const stack = new Error().stack!.slice('Error:'.length);
|
|
81
|
+
if (!port) {
|
|
82
|
+
throw ErrnoError.With('EINVAL');
|
|
83
|
+
}
|
|
84
|
+
return new Promise<TValue>((resolve, reject) => {
|
|
85
|
+
const id = Math.random().toString(16).slice(10);
|
|
86
|
+
executors.set(id, { resolve, reject, fs });
|
|
87
|
+
port.postMessage({ ...request, _zenfs: true, id, stack });
|
|
88
|
+
setTimeout(() => {
|
|
89
|
+
const error = new ErrnoError(Errno.EIO, 'RPC Failed');
|
|
90
|
+
error.stack += stack;
|
|
91
|
+
reject(error);
|
|
92
|
+
}, timeout);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function handleResponse<const TResponse extends Response>(response: TResponse): void {
|
|
97
|
+
if (!isMessage(response)) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const { id, value, error, stack } = response;
|
|
101
|
+
if (!executors.has(id)) {
|
|
102
|
+
const error = new ErrnoError(Errno.EIO, 'Invalid RPC id:' + id);
|
|
103
|
+
error.stack += stack;
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
const { resolve, reject, fs } = executors.get(id)!;
|
|
107
|
+
if (error) {
|
|
108
|
+
const e = ErrnoError.fromJSON(value as ErrnoErrorJSON);
|
|
109
|
+
e.stack += stack;
|
|
110
|
+
reject(e);
|
|
111
|
+
executors.delete(id);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (isFileData(value)) {
|
|
116
|
+
const { fd, path, position } = value;
|
|
117
|
+
const file = new PortFile(fs!, fd, path, position);
|
|
118
|
+
resolve(file);
|
|
119
|
+
executors.delete(id);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
resolve(value);
|
|
124
|
+
executors.delete(id);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function attach<T extends Message>(port: Port, handler: (message: T) => unknown) {
|
|
129
|
+
if (!port) {
|
|
130
|
+
throw ErrnoError.With('EINVAL');
|
|
131
|
+
}
|
|
132
|
+
port['on' in port ? 'on' : 'addEventListener']!('message', (message: T | _MessageEvent<T>) => {
|
|
133
|
+
handler('data' in message ? message.data : message);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function detach<T extends Message>(port: Port, handler: (message: T) => unknown) {
|
|
138
|
+
if (!port) {
|
|
139
|
+
throw ErrnoError.With('EINVAL');
|
|
140
|
+
}
|
|
141
|
+
port['off' in port ? 'off' : 'removeEventListener']!('message', (message: T | _MessageEvent<T>) => {
|
|
142
|
+
handler('data' in message ? message.data : message);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { ErrnoError, Errno } from '../../error.js';
|
|
3
|
+
import { type AsyncStore, type AsyncTransaction, type AsyncStoreOptions, AsyncStoreFS } from '../AsyncStore.js';
|
|
4
|
+
import type { SyncTransaction, SyncStore } from '../SyncStore.js';
|
|
5
|
+
import type { Backend } from '../backend.js';
|
|
6
|
+
import * as RPC from './rpc.js';
|
|
7
|
+
import type { ExtractProperties } from 'utilium';
|
|
8
|
+
|
|
9
|
+
export class PortStore implements AsyncStore {
|
|
10
|
+
public readonly port: RPC.Port;
|
|
11
|
+
public constructor(
|
|
12
|
+
public readonly options: RPC.Options,
|
|
13
|
+
public readonly name: string = 'port'
|
|
14
|
+
) {
|
|
15
|
+
this.port = options.port;
|
|
16
|
+
RPC.attach<RPC.Response>(this.port, RPC.handleResponse);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public clear(): Promise<void> {
|
|
20
|
+
return RPC.request(
|
|
21
|
+
{
|
|
22
|
+
scope: 'store',
|
|
23
|
+
method: 'clear',
|
|
24
|
+
args: [],
|
|
25
|
+
},
|
|
26
|
+
this.options
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public beginTransaction(): PortTransaction {
|
|
31
|
+
const id = RPC.request<RPC.Request, number>(
|
|
32
|
+
{
|
|
33
|
+
scope: 'store',
|
|
34
|
+
method: 'beginTransaction',
|
|
35
|
+
args: [],
|
|
36
|
+
},
|
|
37
|
+
this.options
|
|
38
|
+
);
|
|
39
|
+
return new PortTransaction(this, id);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type TxMethods = ExtractProperties<AsyncTransaction, (...args: any[]) => Promise<any>>;
|
|
44
|
+
type TxMethod = keyof TxMethods;
|
|
45
|
+
interface TxRequest<TMethod extends TxMethod = TxMethod> extends RPC.Request<'transaction', TMethod | 'close', Parameters<TxMethods[TMethod]>> {
|
|
46
|
+
tx: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class PortTransaction implements AsyncTransaction {
|
|
50
|
+
constructor(
|
|
51
|
+
public readonly store: PortStore,
|
|
52
|
+
public readonly id: number | Promise<number>
|
|
53
|
+
) {}
|
|
54
|
+
|
|
55
|
+
public async rpc<const T extends TxMethod>(method: T, ...args: Parameters<TxMethods[T]>): Promise<Awaited<ReturnType<TxMethods[T]>>> {
|
|
56
|
+
return RPC.request<TxRequest<T>, Awaited<ReturnType<TxMethods[T]>>>(
|
|
57
|
+
{
|
|
58
|
+
scope: 'transaction',
|
|
59
|
+
tx: await this.id,
|
|
60
|
+
method,
|
|
61
|
+
args,
|
|
62
|
+
},
|
|
63
|
+
this.store.options
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public get(key: bigint): Promise<Uint8Array> {
|
|
68
|
+
return this.rpc('get', key);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
public async put(key: bigint, data: Uint8Array, overwrite: boolean): Promise<boolean> {
|
|
72
|
+
return await this.rpc('put', key, data, overwrite);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
public async remove(key: bigint): Promise<void> {
|
|
76
|
+
return await this.rpc('remove', key);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
public async commit(): Promise<void> {
|
|
80
|
+
return await this.rpc('commit');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
public async abort(): Promise<void> {
|
|
84
|
+
return await this.rpc('abort');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let nextTx = 0;
|
|
89
|
+
|
|
90
|
+
const transactions: Map<number, AsyncTransaction | SyncTransaction> = new Map();
|
|
91
|
+
|
|
92
|
+
type StoreOrTxRequest = TxRequest | RPC.Request<'store', keyof ExtractProperties<AsyncStore, (...args: any[]) => any>>;
|
|
93
|
+
|
|
94
|
+
async function handleRequest(port: RPC.Port, store: AsyncStore | SyncStore, request: StoreOrTxRequest): Promise<void> {
|
|
95
|
+
if (!RPC.isMessage(request)) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const { method, args, id, scope, stack } = request;
|
|
99
|
+
|
|
100
|
+
let value,
|
|
101
|
+
error: boolean = false;
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
switch (scope) {
|
|
105
|
+
case 'store':
|
|
106
|
+
// @ts-expect-error 2556
|
|
107
|
+
value = await store[method](...args);
|
|
108
|
+
if (method == 'beginTransaction') {
|
|
109
|
+
transactions.set(++nextTx, value!);
|
|
110
|
+
value = nextTx;
|
|
111
|
+
}
|
|
112
|
+
break;
|
|
113
|
+
case 'transaction':
|
|
114
|
+
const { tx } = request as TxRequest;
|
|
115
|
+
if (!transactions.has(tx)) {
|
|
116
|
+
throw new ErrnoError(Errno.EBADF);
|
|
117
|
+
}
|
|
118
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
119
|
+
// @ts-ignore 2556
|
|
120
|
+
value = await transactions.get(tx);
|
|
121
|
+
if (method == 'close') {
|
|
122
|
+
transactions.delete(tx);
|
|
123
|
+
}
|
|
124
|
+
break;
|
|
125
|
+
default:
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
} catch (e) {
|
|
129
|
+
value = e;
|
|
130
|
+
error = true;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
port.postMessage({
|
|
134
|
+
_zenfs: true,
|
|
135
|
+
scope,
|
|
136
|
+
id,
|
|
137
|
+
error,
|
|
138
|
+
method,
|
|
139
|
+
stack,
|
|
140
|
+
value: value instanceof ErrnoError ? value.toJSON() : value,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function attachStore(port: RPC.Port, store: SyncStore | AsyncStore): void {
|
|
145
|
+
RPC.attach(port, (request: StoreOrTxRequest) => handleRequest(port, store, request));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function detachStore(port: RPC.Port, store: SyncStore | AsyncStore): void {
|
|
149
|
+
RPC.detach(port, (request: StoreOrTxRequest) => handleRequest(port, store, request));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export const PortStoreBackend: Backend = {
|
|
153
|
+
name: 'PortStore',
|
|
154
|
+
|
|
155
|
+
options: {
|
|
156
|
+
port: {
|
|
157
|
+
type: 'object',
|
|
158
|
+
description: 'The target port that you want to connect to',
|
|
159
|
+
validator(port: RPC.Port) {
|
|
160
|
+
// Check for a `postMessage` function.
|
|
161
|
+
if (typeof port?.postMessage != 'function') {
|
|
162
|
+
throw new ErrnoError(Errno.EINVAL, 'option must be a port.');
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
async isAvailable(): Promise<boolean> {
|
|
169
|
+
if ('WorkerGlobalScope' in globalThis && globalThis instanceof (globalThis as typeof globalThis & { WorkerGlobalScope: any }).WorkerGlobalScope) {
|
|
170
|
+
// Web Worker
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const worker_threads = await import('node:worker_threads');
|
|
176
|
+
|
|
177
|
+
// NodeJS worker
|
|
178
|
+
return 'Worker' in worker_threads;
|
|
179
|
+
} catch (e) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
create(options: RPC.Options & AsyncStoreOptions & { name?: string }) {
|
|
185
|
+
return new AsyncStoreFS({ ...options, store: new PortStore(options, options?.name) });
|
|
186
|
+
},
|
|
187
|
+
};
|