@statewalker/webrun-files-rpc 0.1.0 → 0.1.2
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/README.md +43 -13
- package/dist/cjs/index.cjs +147 -26
- package/dist/client.d.ts +5 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/esm/index.js +147 -26
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/server.d.ts +2 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/types.d.ts +35 -7
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/client.ts +31 -6
- package/src/errors.ts +13 -4
- package/src/index.ts +3 -0
- package/src/server.ts +158 -13
- package/src/types.ts +38 -7
package/dist/esm/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// src/client.ts
|
|
2
2
|
const toBytes$1 = (v) => v instanceof Uint8Array ? v : ArrayBuffer.isView(v) ? new Uint8Array(v.buffer, v.byteOffset, v.byteLength) : new Uint8Array(v);
|
|
3
3
|
const newStreamId$1 = () => crypto.randomUUID().replace(/-/g, "");
|
|
4
4
|
/** Cuts any sequence of byte arrays into chunks of exactly `size` bytes (the last may be shorter). */
|
|
@@ -55,8 +55,9 @@ var FilesRpcClient = class {
|
|
|
55
55
|
streamId: newStreamId$1()
|
|
56
56
|
});
|
|
57
57
|
const pending = /* @__PURE__ */ new Map();
|
|
58
|
+
let fail;
|
|
58
59
|
const aborted = signal ? new Promise((_, reject) => {
|
|
59
|
-
|
|
60
|
+
fail = () => reject(signal.reason ?? /* @__PURE__ */ new Error("aborted"));
|
|
60
61
|
if (signal.aborted) fail();
|
|
61
62
|
else signal.addEventListener("abort", fail, { once: true });
|
|
62
63
|
}) : void 0;
|
|
@@ -81,6 +82,7 @@ var FilesRpcClient = class {
|
|
|
81
82
|
signal?.throwIfAborted();
|
|
82
83
|
}
|
|
83
84
|
} finally {
|
|
85
|
+
if (fail) signal?.removeEventListener("abort", fail);
|
|
84
86
|
await this.#rpc.closeRead(streamId).catch(() => {});
|
|
85
87
|
}
|
|
86
88
|
}
|
|
@@ -89,7 +91,20 @@ var FilesRpcClient = class {
|
|
|
89
91
|
* then `closeWrite` with their count; any failure aborts the write.
|
|
90
92
|
*/
|
|
91
93
|
async write(path, content) {
|
|
92
|
-
|
|
94
|
+
return this.#send(path, content, false);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Adds `content` to the end of `path` (creating it if it does not exist), streamed as `write`
|
|
98
|
+
* streams. The server appends through the served FilesApi's own `append`, or emulates it.
|
|
99
|
+
*/
|
|
100
|
+
async append(path, content) {
|
|
101
|
+
return this.#send(path, content, true);
|
|
102
|
+
}
|
|
103
|
+
async #send(path, content, append) {
|
|
104
|
+
const { streamId } = await this.#rpc.openWrite(path, {
|
|
105
|
+
streamId: newStreamId$1(),
|
|
106
|
+
...append ? { append: true } : {}
|
|
107
|
+
});
|
|
93
108
|
const inFlight = /* @__PURE__ */ new Set();
|
|
94
109
|
let failure;
|
|
95
110
|
let count = 0;
|
|
@@ -114,7 +129,7 @@ var FilesRpcClient = class {
|
|
|
114
129
|
}
|
|
115
130
|
}
|
|
116
131
|
async mkdir(path) {
|
|
117
|
-
await this.#rpc.mkdir(path);
|
|
132
|
+
await this.#rpc.mkdir(path, { requestId: newStreamId$1() });
|
|
118
133
|
}
|
|
119
134
|
/** Pages of `pageSize`, each asked after the last path of the one before. */
|
|
120
135
|
async *list(path, options = {}) {
|
|
@@ -137,21 +152,20 @@ var FilesRpcClient = class {
|
|
|
137
152
|
return this.#rpc.exists(path);
|
|
138
153
|
}
|
|
139
154
|
remove(path) {
|
|
140
|
-
return this.#rpc.remove(path);
|
|
155
|
+
return this.#rpc.remove(path, { requestId: newStreamId$1() });
|
|
141
156
|
}
|
|
142
157
|
move(source, target) {
|
|
143
|
-
return this.#rpc.move(source, target);
|
|
158
|
+
return this.#rpc.move(source, target, { requestId: newStreamId$1() });
|
|
144
159
|
}
|
|
145
160
|
copy(source, target) {
|
|
146
|
-
return this.#rpc.copy(source, target);
|
|
161
|
+
return this.#rpc.copy(source, target, { requestId: newStreamId$1() });
|
|
147
162
|
}
|
|
148
163
|
};
|
|
149
164
|
/** A FilesApi over a FilesRpc. */
|
|
150
165
|
function newFilesRpcClient(options) {
|
|
151
166
|
return new FilesRpcClient(options);
|
|
152
167
|
}
|
|
153
|
-
|
|
154
|
-
//#region src/errors.ts
|
|
168
|
+
// src/errors.ts
|
|
155
169
|
/**
|
|
156
170
|
* An error of the protocol itself. The code also leads the message (`UNKNOWN_STREAM: …`), since
|
|
157
171
|
* a structured clone keeps an error's name and message but not its other properties:
|
|
@@ -168,11 +182,19 @@ var FilesRpcError = class extends Error {
|
|
|
168
182
|
/** The FilesRpcError code of an error, also after it crossed a transport; undefined otherwise. */
|
|
169
183
|
function errorCode(e) {
|
|
170
184
|
const code = e?.code;
|
|
171
|
-
if (typeof code === "string") return code;
|
|
172
|
-
return
|
|
185
|
+
if (typeof code === "string" && CODES.has(code)) return code;
|
|
186
|
+
return LEADING_CODE.exec(String(e?.message ?? ""))?.[1];
|
|
173
187
|
}
|
|
174
|
-
|
|
175
|
-
|
|
188
|
+
const CODES = /* @__PURE__ */ new Set([
|
|
189
|
+
"UNKNOWN_STREAM",
|
|
190
|
+
"CHUNK_RELEASED",
|
|
191
|
+
"TOO_MANY_STREAMS",
|
|
192
|
+
"ABORTED",
|
|
193
|
+
"INVALID"
|
|
194
|
+
]);
|
|
195
|
+
/** A message led by a code, as FilesRpcError writes it (`CODE: …`). */
|
|
196
|
+
const LEADING_CODE = new RegExp(`^(${[...CODES].join("|")}):`);
|
|
197
|
+
// src/keyed.ts
|
|
176
198
|
/**
|
|
177
199
|
* Values kept by key, and waiters for keys that have no value yet: what both ends of a stream
|
|
178
200
|
* use to keep chunks by chunkId and wait for the ones still on their way.
|
|
@@ -235,8 +257,23 @@ var Keyed = class {
|
|
|
235
257
|
this.#waiters.clear();
|
|
236
258
|
}
|
|
237
259
|
};
|
|
238
|
-
|
|
239
|
-
|
|
260
|
+
// src/server.ts
|
|
261
|
+
/**
|
|
262
|
+
* Whether a write's outcome is the FilesApi's alone: every chunk was handed to it (an emulated
|
|
263
|
+
* append, until its write-back starts, can still be cancelled).
|
|
264
|
+
*/
|
|
265
|
+
const committed = (s) => s.total !== void 0 && s.next >= s.total && (!s.emulated || s.committing === true);
|
|
266
|
+
/** `path` in normal form: one leading slash, no empty, "." or ".." segments, no trailing slash. */
|
|
267
|
+
function normalPath(path) {
|
|
268
|
+
const out = [];
|
|
269
|
+
for (const seg of String(path).split("/")) {
|
|
270
|
+
if (!seg || seg === ".") continue;
|
|
271
|
+
if (seg === "..") out.pop();
|
|
272
|
+
else out.push(seg);
|
|
273
|
+
}
|
|
274
|
+
return `/${out.join("/")}`;
|
|
275
|
+
}
|
|
276
|
+
const messageOf = (e) => String(e?.message ?? e);
|
|
240
277
|
const EMPTY = /* @__PURE__ */ new Uint8Array();
|
|
241
278
|
const STREAM_ID = /^[A-Za-z0-9_-]{16,128}$/;
|
|
242
279
|
function newStreamId() {
|
|
@@ -299,6 +336,40 @@ function newFilesRpcServer(options) {
|
|
|
299
336
|
* open it anew, and a closeWrite delivered again answers as the first one did.
|
|
300
337
|
*/
|
|
301
338
|
const ended = /* @__PURE__ */ new Map();
|
|
339
|
+
/** Answers of metadata calls, by method and requestId, oldest first. */
|
|
340
|
+
const requests = /* @__PURE__ */ new Map();
|
|
341
|
+
const maxRequests = Math.max(1, options.maxRequests ?? 1024);
|
|
342
|
+
/** Runs `call` once per requestId (within idleTimeout): a call delivered again gets its answer. */
|
|
343
|
+
const once = (method, opts, call) => {
|
|
344
|
+
const id = opts?.requestId;
|
|
345
|
+
if (id === void 0) return call();
|
|
346
|
+
if (typeof id !== "string" || !STREAM_ID.test(id)) return Promise.reject(new FilesRpcError("INVALID", "a requestId is 16 to 128 characters of [A-Za-z0-9_-]"));
|
|
347
|
+
const key = `${method} ${id}`;
|
|
348
|
+
const kept = requests.get(key);
|
|
349
|
+
if (kept) return kept.answer;
|
|
350
|
+
const answer = call();
|
|
351
|
+
answer.catch(() => {});
|
|
352
|
+
const entry = {
|
|
353
|
+
at: now(),
|
|
354
|
+
settled: false,
|
|
355
|
+
answer
|
|
356
|
+
};
|
|
357
|
+
requests.set(key, entry);
|
|
358
|
+
const settle = () => {
|
|
359
|
+
entry.at = now();
|
|
360
|
+
entry.settled = true;
|
|
361
|
+
};
|
|
362
|
+
answer.then(settle, settle);
|
|
363
|
+
while (requests.size > maxRequests) {
|
|
364
|
+
let victim;
|
|
365
|
+
for (const [k, r] of requests) if (r.settled) {
|
|
366
|
+
victim = k;
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
requests.delete(victim ?? requests.keys().next().value);
|
|
370
|
+
}
|
|
371
|
+
return answer;
|
|
372
|
+
};
|
|
302
373
|
const finish = (id, failure) => {
|
|
303
374
|
streams.delete(id);
|
|
304
375
|
ended.set(id, {
|
|
@@ -441,6 +512,52 @@ function newFilesRpcServer(options) {
|
|
|
441
512
|
s.wake = void 0;
|
|
442
513
|
}
|
|
443
514
|
}
|
|
515
|
+
/** Emulated appends in progress, by normal path: one at a time per file. */
|
|
516
|
+
const appending = /* @__PURE__ */ new Map();
|
|
517
|
+
/**
|
|
518
|
+
* Appends `content` to `path`: through the FilesApi's own `append`, or with read, write and
|
|
519
|
+
* remove only. The emulation never reads and writes one path at once (a FilesApi may empty a
|
|
520
|
+
* file when a write starts):
|
|
521
|
+
* 1. the file's content and the chunks go to a temporary sibling;
|
|
522
|
+
* 2. that sibling is written back over the file, then removed.
|
|
523
|
+
* A failure or an abort in step 1 leaves the file as it was and removes the sibling. A failure
|
|
524
|
+
* in step 2 keeps the sibling (the only complete copy) and names it in the error. Appends to
|
|
525
|
+
* one file through this server run one after the other; other writers are not excluded.
|
|
526
|
+
*/
|
|
527
|
+
const append = async (path, content, id, s) => {
|
|
528
|
+
const own = fs.append;
|
|
529
|
+
if (typeof own === "function") return own.call(fs, path, content);
|
|
530
|
+
s.emulated = true;
|
|
531
|
+
const file = normalPath(path);
|
|
532
|
+
const run = (appending.get(file) ?? Promise.resolve()).then(async () => {
|
|
533
|
+
if ((await fs.stats(file))?.kind === "directory") throw new FilesRpcError("INVALID", `cannot append to a directory: ${file}`);
|
|
534
|
+
const slash = file.lastIndexOf("/");
|
|
535
|
+
const temp = `${file.slice(0, slash + 1)}.${file.slice(slash + 1, slash + 65)}.append-${id.slice(0, 16)}`;
|
|
536
|
+
try {
|
|
537
|
+
await fs.write(temp, (async function* () {
|
|
538
|
+
yield* fs.read(file);
|
|
539
|
+
yield* content;
|
|
540
|
+
})());
|
|
541
|
+
if (s.failure !== void 0) throw s.failure;
|
|
542
|
+
} catch (e) {
|
|
543
|
+
await fs.remove(temp).catch(() => {});
|
|
544
|
+
throw e;
|
|
545
|
+
}
|
|
546
|
+
s.committing = true;
|
|
547
|
+
try {
|
|
548
|
+
await fs.write(file, fs.read(temp));
|
|
549
|
+
} catch (e) {
|
|
550
|
+
throw Object.assign(/* @__PURE__ */ new Error(`${messageOf(e)} (the append failed while writing ${file} back: it may be incomplete; its content and the appended chunks are kept in ${temp})`), { cause: e });
|
|
551
|
+
}
|
|
552
|
+
await fs.remove(temp).catch(() => {});
|
|
553
|
+
});
|
|
554
|
+
const settled = run.catch(() => {});
|
|
555
|
+
appending.set(file, settled);
|
|
556
|
+
settled.then(() => {
|
|
557
|
+
if (appending.get(file) === settled) appending.delete(file);
|
|
558
|
+
});
|
|
559
|
+
return run;
|
|
560
|
+
};
|
|
444
561
|
const failWrite = (s, failure) => {
|
|
445
562
|
s.failure ??= failure;
|
|
446
563
|
for (const r of s.room.splice(0)) r.reject(s.failure);
|
|
@@ -453,17 +570,17 @@ function newFilesRpcServer(options) {
|
|
|
453
570
|
async exists(path) {
|
|
454
571
|
return fs.exists(path);
|
|
455
572
|
},
|
|
456
|
-
async mkdir(path) {
|
|
457
|
-
await fs.mkdir(path);
|
|
573
|
+
async mkdir(path, opts) {
|
|
574
|
+
await once("mkdir", opts, () => fs.mkdir(path));
|
|
458
575
|
},
|
|
459
|
-
async remove(path) {
|
|
460
|
-
return fs.remove(path);
|
|
576
|
+
async remove(path, opts) {
|
|
577
|
+
return once("remove", opts, () => fs.remove(path));
|
|
461
578
|
},
|
|
462
|
-
async move(source, target) {
|
|
463
|
-
return fs.move(source, target);
|
|
579
|
+
async move(source, target, opts) {
|
|
580
|
+
return once("move", opts, () => fs.move(source, target));
|
|
464
581
|
},
|
|
465
|
-
async copy(source, target) {
|
|
466
|
-
return fs.copy(source, target);
|
|
582
|
+
async copy(source, target, opts) {
|
|
583
|
+
return once("copy", opts, () => fs.copy(source, target));
|
|
467
584
|
},
|
|
468
585
|
async list(path, opts = {}) {
|
|
469
586
|
const limit = Math.min(Math.max(1, Math.floor(Number(opts.limit) || maxPageSize)), maxPageSize);
|
|
@@ -556,7 +673,7 @@ function newFilesRpcServer(options) {
|
|
|
556
673
|
room: [],
|
|
557
674
|
done: Promise.resolve()
|
|
558
675
|
};
|
|
559
|
-
s.done = fs.write(path, ordered(s)).then(() => {
|
|
676
|
+
s.done = (opts.append ? append(path, ordered(s), id, s) : fs.write(path, ordered(s))).then(() => {
|
|
560
677
|
if (s.failure !== void 0) throw s.failure;
|
|
561
678
|
}, (e) => {
|
|
562
679
|
failWrite(s, e);
|
|
@@ -620,7 +737,7 @@ function newFilesRpcServer(options) {
|
|
|
620
737
|
const id = String(streamId);
|
|
621
738
|
const s = streams.get(id);
|
|
622
739
|
if (s?.kind !== "write") return;
|
|
623
|
-
if (s
|
|
740
|
+
if (committed(s)) return;
|
|
624
741
|
const error = new FilesRpcError("ABORTED", reason ? String(reason) : "the write was aborted");
|
|
625
742
|
finish(id, error);
|
|
626
743
|
failWrite(s, error);
|
|
@@ -639,6 +756,7 @@ function newFilesRpcServer(options) {
|
|
|
639
756
|
else failWrite(s, error);
|
|
640
757
|
}
|
|
641
758
|
for (const [id, e] of [...ended]) if (t - e.at > idleTimeout) ended.delete(id);
|
|
759
|
+
for (const [key, r] of [...requests]) if (r.settled && t - r.at > idleTimeout) requests.delete(key);
|
|
642
760
|
return dropped;
|
|
643
761
|
},
|
|
644
762
|
openStreams() {
|
|
@@ -648,6 +766,10 @@ function newFilesRpcServer(options) {
|
|
|
648
766
|
if (timer) clearInterval(timer);
|
|
649
767
|
const sources = [];
|
|
650
768
|
for (const [id, s] of [...streams]) {
|
|
769
|
+
if (s.kind === "write" && committed(s)) {
|
|
770
|
+
streams.delete(id);
|
|
771
|
+
continue;
|
|
772
|
+
}
|
|
651
773
|
const error = new FilesRpcError("ABORTED", "the server closed");
|
|
652
774
|
finish(id, error);
|
|
653
775
|
if (s.kind === "read") sources.push(closeSource(s));
|
|
@@ -662,5 +784,4 @@ function newFilesRpcServer(options) {
|
|
|
662
784
|
timer?.unref?.();
|
|
663
785
|
return server;
|
|
664
786
|
}
|
|
665
|
-
//#endregion
|
|
666
787
|
export { FilesRpcClient, FilesRpcError, errorCode, newFilesRpcClient, newFilesRpcServer };
|
package/dist/index.d.ts
CHANGED
|
@@ -7,5 +7,5 @@
|
|
|
7
7
|
export { FilesRpcClient, type FilesRpcClientOptions, newFilesRpcClient } from "./client.js";
|
|
8
8
|
export { errorCode, FilesRpcError } from "./errors.js";
|
|
9
9
|
export { type FilesRpcServer, type FilesRpcServerOptions, newFilesRpcServer } from "./server.js";
|
|
10
|
-
export type { FilesRpc, FilesRpcErrorCode, ListPage, ListPageOptions, OpenOptions, OpenReadOptions, ReadResult, } from "./types.js";
|
|
10
|
+
export type { AppendableFilesApi, FilesRpc, FilesRpcErrorCode, ListPage, ListPageOptions, OpenOptions, OpenReadOptions, OpenWriteOptions, ReadResult, RequestOptions, } from "./types.js";
|
|
11
11
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,cAAc,EAAE,KAAK,qBAAqB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAC5F,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACvD,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,qBAAqB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACjG,YAAY,EACV,QAAQ,EACR,iBAAiB,EACjB,QAAQ,EACR,eAAe,EACf,WAAW,EACX,eAAe,EACf,UAAU,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,cAAc,EAAE,KAAK,qBAAqB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAC5F,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACvD,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,qBAAqB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACjG,YAAY,EACV,kBAAkB,EAClB,QAAQ,EACR,iBAAiB,EACjB,QAAQ,EACR,eAAe,EACf,WAAW,EACX,eAAe,EACf,gBAAgB,EAChB,UAAU,EACV,cAAc,GACf,MAAM,YAAY,CAAC"}
|
package/dist/server.d.ts
CHANGED
|
@@ -27,6 +27,8 @@ export interface FilesRpcServerOptions {
|
|
|
27
27
|
maxStreams?: number;
|
|
28
28
|
/** Largest listing page. Default 256. */
|
|
29
29
|
maxPageSize?: number;
|
|
30
|
+
/** Answers of metadata calls kept by their `requestId` at most (default 1024). */
|
|
31
|
+
maxRequests?: number;
|
|
30
32
|
/** Sweeps idle streams every this many ms (default: only when a stream is opened). */
|
|
31
33
|
sweepInterval?: number;
|
|
32
34
|
/** The clock (tests). Default `Date.now`. */
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAY,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAGpE,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAY,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAGpE,OAAO,KAAK,EAEV,QAAQ,EAOT,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,qBAAqB;IACpC,8BAA8B;IAC9B,EAAE,EAAE,QAAQ,CAAC;IACb,4DAA4D;IAC5D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6EAA6E;IAC7E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6FAA6F;IAC7F,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kDAAkD;IAClD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gGAAgG;IAChG,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,yCAAyC;IACzC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,yCAAyC;IACzC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kFAAkF;IAClF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sFAAsF;IACtF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,6CAA6C;IAC7C,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,qDAAqD;AACrD,MAAM,WAAW,cAAe,SAAQ,QAAQ;IAC9C,0EAA0E;IAC1E,KAAK,IAAI,MAAM,CAAC;IAChB,wBAAwB;IACxB,WAAW,IAAI,MAAM,CAAC;IACtB;;;OAGG;IACH,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAwHD,iDAAiD;AACjD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,qBAAqB,GAAG,cAAc,CA0iBhF"}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FileInfo, FileStats } from "@statewalker/webrun-files";
|
|
1
|
+
import type { FileInfo, FileStats, FilesApi } from "@statewalker/webrun-files";
|
|
2
2
|
/** One chunk of a read stream: its bytes, and whether it is the last. */
|
|
3
3
|
export interface ReadResult {
|
|
4
4
|
data: Uint8Array;
|
|
@@ -24,6 +24,31 @@ export interface OpenOptions {
|
|
|
24
24
|
*/
|
|
25
25
|
streamId?: string;
|
|
26
26
|
}
|
|
27
|
+
/** Options of the calls that change metadata (`mkdir`, `remove`, `move`, `copy`). */
|
|
28
|
+
export interface RequestOptions {
|
|
29
|
+
/**
|
|
30
|
+
* The call's id, chosen by the caller (the same form as a stream id). The server keeps each
|
|
31
|
+
* id's answer for `idleTimeout` (at most `maxRequests` of them): the same call delivered again
|
|
32
|
+
* gets that answer and does not run again.
|
|
33
|
+
*/
|
|
34
|
+
requestId?: string;
|
|
35
|
+
}
|
|
36
|
+
export interface OpenWriteOptions extends OpenOptions {
|
|
37
|
+
/**
|
|
38
|
+
* Add the stream's chunks to the end of the file instead of replacing it (a file that does not
|
|
39
|
+
* exist is created). See `AppendableFilesApi` for how the server appends.
|
|
40
|
+
*/
|
|
41
|
+
append?: boolean;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* A FilesApi that can append itself. `FilesApi` has no append; a served FilesApi with this method
|
|
45
|
+
* gets appends through it. Any other is appended to with read, write and remove only: the old
|
|
46
|
+
* content and the new chunks go to a temporary sibling (`.<name>.append-<streamId>`), which is
|
|
47
|
+
* then written back over the file and removed.
|
|
48
|
+
*/
|
|
49
|
+
export interface AppendableFilesApi extends FilesApi {
|
|
50
|
+
append(path: string, content: AsyncIterable<Uint8Array>): Promise<void>;
|
|
51
|
+
}
|
|
27
52
|
export interface OpenReadOptions extends OpenOptions {
|
|
28
53
|
/** First byte (default 0). */
|
|
29
54
|
start?: number;
|
|
@@ -44,10 +69,10 @@ export interface OpenReadOptions extends OpenOptions {
|
|
|
44
69
|
export interface FilesRpc {
|
|
45
70
|
stats(path: string): Promise<FileStats | undefined>;
|
|
46
71
|
exists(path: string): Promise<boolean>;
|
|
47
|
-
mkdir(path: string): Promise<void>;
|
|
48
|
-
remove(path: string): Promise<boolean>;
|
|
49
|
-
move(source: string, target: string): Promise<boolean>;
|
|
50
|
-
copy(source: string, target: string): Promise<boolean>;
|
|
72
|
+
mkdir(path: string, options?: RequestOptions): Promise<void>;
|
|
73
|
+
remove(path: string, options?: RequestOptions): Promise<boolean>;
|
|
74
|
+
move(source: string, target: string, options?: RequestOptions): Promise<boolean>;
|
|
75
|
+
copy(source: string, target: string, options?: RequestOptions): Promise<boolean>;
|
|
51
76
|
list(path: string, options?: ListPageOptions): Promise<ListPage>;
|
|
52
77
|
/** Opens `path` for reading. A file that does not exist reads as nothing. */
|
|
53
78
|
openRead(path: string, options?: OpenReadOptions): Promise<{
|
|
@@ -60,8 +85,11 @@ export interface FilesRpc {
|
|
|
60
85
|
readChunk(streamId: string, chunkId: number): Promise<ReadResult>;
|
|
61
86
|
/** Ends the stream (unknown or closed: nothing happens). */
|
|
62
87
|
closeRead(streamId: string): Promise<void>;
|
|
63
|
-
/**
|
|
64
|
-
|
|
88
|
+
/**
|
|
89
|
+
* Opens `path` for writing (or, with `append`, for appending); the FilesApi consumes the chunks
|
|
90
|
+
* in order.
|
|
91
|
+
*/
|
|
92
|
+
openWrite(path: string, options?: OpenWriteOptions): Promise<{
|
|
65
93
|
streamId: string;
|
|
66
94
|
}>;
|
|
67
95
|
/**
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAE/E,yEAAyE;AACzE,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,UAAU,CAAC;IACjB,IAAI,EAAE,OAAO,CAAC;CACf;AAED,0FAA0F;AAC1F,MAAM,WAAW,QAAQ;IACvB,OAAO,EAAE,QAAQ,EAAE,CAAC;IACpB,IAAI,EAAE,OAAO,CAAC;CACf;AAED,uFAAuF;AACvF,MAAM,WAAW,eAAe;IAC9B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,+CAA+C;AAC/C,MAAM,WAAW,WAAW;IAC1B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,qFAAqF;AACrF,MAAM,WAAW,cAAc;IAC7B;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACnD;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;;;;GAKG;AACH,MAAM,WAAW,kBAAmB,SAAQ,QAAQ;IAClD,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,CAAC,UAAU,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACzE;AAED,MAAM,WAAW,eAAgB,SAAQ,WAAW;IAClD,8BAA8B;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2CAA2C;IAC3C,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,QAAQ;IACvB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC,CAAC;IACpD,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACvC,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7D,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACjE,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACjF,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACjF,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAEjE,6EAA6E;IAC7E,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACjF;;;OAGG;IACH,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAClE,4DAA4D;IAC5D,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE3C;;;OAGG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACnF;;;;OAIG;IACH,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7F,6EAA6E;IAC7E,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5D,8DAA8D;IAC9D,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9D;AAED,oCAAoC;AACpC,MAAM,MAAM,iBAAiB,GACzB,gBAAgB,GAChB,gBAAgB,GAChB,kBAAkB,GAClB,SAAS,GACT,SAAS,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@statewalker/webrun-files-rpc",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "FilesApi over any RPC: streamed reads and writes as chunks kept and awaited by (streamId, chunkId)",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"src"
|
|
35
35
|
],
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@statewalker/webrun-files": "^0.10.
|
|
37
|
+
"@statewalker/webrun-files": "^0.10.5"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@types/node": "^26.6.4",
|
|
@@ -42,8 +42,8 @@
|
|
|
42
42
|
"rolldown": "^1.2.12",
|
|
43
43
|
"typescript": "^7.0.2",
|
|
44
44
|
"vitest": "^4.1.4 || ^5.0.3",
|
|
45
|
-
"@statewalker/webrun-files-
|
|
46
|
-
"@statewalker/webrun-files-
|
|
45
|
+
"@statewalker/webrun-files-tests": "^3.0.0",
|
|
46
|
+
"@statewalker/webrun-files-mem": "^0.10.5"
|
|
47
47
|
},
|
|
48
48
|
"sideEffects": false,
|
|
49
49
|
"publishConfig": {
|
package/src/client.ts
CHANGED
|
@@ -90,9 +90,10 @@ export class FilesRpcClient implements FilesApi {
|
|
|
90
90
|
});
|
|
91
91
|
const pending = new Map<number, Promise<ReadResult>>();
|
|
92
92
|
// An abort interrupts the wait for an answer, not only the loop between answers.
|
|
93
|
+
let fail: (() => void) | undefined;
|
|
93
94
|
const aborted = signal
|
|
94
95
|
? new Promise<never>((_, reject) => {
|
|
95
|
-
|
|
96
|
+
fail = () => reject(signal.reason ?? new Error("aborted"));
|
|
96
97
|
if (signal.aborted) fail();
|
|
97
98
|
else signal.addEventListener("abort", fail, { once: true });
|
|
98
99
|
})
|
|
@@ -118,6 +119,8 @@ export class FilesRpcClient implements FilesApi {
|
|
|
118
119
|
signal?.throwIfAborted();
|
|
119
120
|
}
|
|
120
121
|
} finally {
|
|
122
|
+
// A signal may outlive many reads: each takes its listener away.
|
|
123
|
+
if (fail) signal?.removeEventListener("abort", fail);
|
|
121
124
|
await this.#rpc.closeRead(streamId).catch(() => {});
|
|
122
125
|
}
|
|
123
126
|
}
|
|
@@ -130,7 +133,29 @@ export class FilesRpcClient implements FilesApi {
|
|
|
130
133
|
path: string,
|
|
131
134
|
content: Iterable<Uint8Array> | AsyncIterable<Uint8Array>,
|
|
132
135
|
): Promise<void> {
|
|
133
|
-
|
|
136
|
+
return this.#send(path, content, false);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Adds `content` to the end of `path` (creating it if it does not exist), streamed as `write`
|
|
141
|
+
* streams. The server appends through the served FilesApi's own `append`, or emulates it.
|
|
142
|
+
*/
|
|
143
|
+
async append(
|
|
144
|
+
path: string,
|
|
145
|
+
content: Iterable<Uint8Array> | AsyncIterable<Uint8Array>,
|
|
146
|
+
): Promise<void> {
|
|
147
|
+
return this.#send(path, content, true);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async #send(
|
|
151
|
+
path: string,
|
|
152
|
+
content: Iterable<Uint8Array> | AsyncIterable<Uint8Array>,
|
|
153
|
+
append: boolean,
|
|
154
|
+
): Promise<void> {
|
|
155
|
+
const { streamId } = await this.#rpc.openWrite(path, {
|
|
156
|
+
streamId: newStreamId(),
|
|
157
|
+
...(append ? { append: true } : {}),
|
|
158
|
+
});
|
|
134
159
|
const inFlight = new Set<Promise<void>>();
|
|
135
160
|
let failure: { error: unknown } | undefined;
|
|
136
161
|
let count = 0;
|
|
@@ -159,7 +184,7 @@ export class FilesRpcClient implements FilesApi {
|
|
|
159
184
|
}
|
|
160
185
|
|
|
161
186
|
async mkdir(path: string): Promise<void> {
|
|
162
|
-
await this.#rpc.mkdir(path);
|
|
187
|
+
await this.#rpc.mkdir(path, { requestId: newStreamId() });
|
|
163
188
|
}
|
|
164
189
|
|
|
165
190
|
/** Pages of `pageSize`, each asked after the last path of the one before. */
|
|
@@ -184,13 +209,13 @@ export class FilesRpcClient implements FilesApi {
|
|
|
184
209
|
return this.#rpc.exists(path);
|
|
185
210
|
}
|
|
186
211
|
remove(path: string): Promise<boolean> {
|
|
187
|
-
return this.#rpc.remove(path);
|
|
212
|
+
return this.#rpc.remove(path, { requestId: newStreamId() });
|
|
188
213
|
}
|
|
189
214
|
move(source: string, target: string): Promise<boolean> {
|
|
190
|
-
return this.#rpc.move(source, target);
|
|
215
|
+
return this.#rpc.move(source, target, { requestId: newStreamId() });
|
|
191
216
|
}
|
|
192
217
|
copy(source: string, target: string): Promise<boolean> {
|
|
193
|
-
return this.#rpc.copy(source, target);
|
|
218
|
+
return this.#rpc.copy(source, target, { requestId: newStreamId() });
|
|
194
219
|
}
|
|
195
220
|
}
|
|
196
221
|
|
package/src/errors.ts
CHANGED
|
@@ -16,10 +16,19 @@ export class FilesRpcError extends Error {
|
|
|
16
16
|
|
|
17
17
|
/** The FilesRpcError code of an error, also after it crossed a transport; undefined otherwise. */
|
|
18
18
|
export function errorCode(e: unknown): FilesRpcErrorCode | undefined {
|
|
19
|
+
// Only a protocol code: other errors carry codes of their own (a file system's `ENOENT`).
|
|
19
20
|
const code = (e as { code?: unknown })?.code;
|
|
20
|
-
if (typeof code === "string") return code as FilesRpcErrorCode;
|
|
21
|
-
const m =
|
|
22
|
-
String((e as Error)?.message ?? ""),
|
|
23
|
-
);
|
|
21
|
+
if (typeof code === "string" && CODES.has(code)) return code as FilesRpcErrorCode;
|
|
22
|
+
const m = LEADING_CODE.exec(String((e as Error)?.message ?? ""));
|
|
24
23
|
return m?.[1] as FilesRpcErrorCode | undefined;
|
|
25
24
|
}
|
|
25
|
+
|
|
26
|
+
const CODES: ReadonlySet<string> = new Set<FilesRpcErrorCode>([
|
|
27
|
+
"UNKNOWN_STREAM",
|
|
28
|
+
"CHUNK_RELEASED",
|
|
29
|
+
"TOO_MANY_STREAMS",
|
|
30
|
+
"ABORTED",
|
|
31
|
+
"INVALID",
|
|
32
|
+
]);
|
|
33
|
+
/** A message led by a code, as FilesRpcError writes it (`CODE: …`). */
|
|
34
|
+
const LEADING_CODE = new RegExp(`^(${[...CODES].join("|")}):`);
|
package/src/index.ts
CHANGED
|
@@ -8,11 +8,14 @@ export { FilesRpcClient, type FilesRpcClientOptions, newFilesRpcClient } from ".
|
|
|
8
8
|
export { errorCode, FilesRpcError } from "./errors.js";
|
|
9
9
|
export { type FilesRpcServer, type FilesRpcServerOptions, newFilesRpcServer } from "./server.js";
|
|
10
10
|
export type {
|
|
11
|
+
AppendableFilesApi,
|
|
11
12
|
FilesRpc,
|
|
12
13
|
FilesRpcErrorCode,
|
|
13
14
|
ListPage,
|
|
14
15
|
ListPageOptions,
|
|
15
16
|
OpenOptions,
|
|
16
17
|
OpenReadOptions,
|
|
18
|
+
OpenWriteOptions,
|
|
17
19
|
ReadResult,
|
|
20
|
+
RequestOptions,
|
|
18
21
|
} from "./types.js";
|