@statewalker/webrun-files-rpc 0.0.0-stage → 0.1.1

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.
@@ -0,0 +1,791 @@
1
+ //#region src/client.ts
2
+ const toBytes$1 = (v) => v instanceof Uint8Array ? v : ArrayBuffer.isView(v) ? new Uint8Array(v.buffer, v.byteOffset, v.byteLength) : new Uint8Array(v);
3
+ const newStreamId$1 = () => crypto.randomUUID().replace(/-/g, "");
4
+ /** Cuts any sequence of byte arrays into chunks of exactly `size` bytes (the last may be shorter). */
5
+ async function* rechunk(content, size) {
6
+ let carry;
7
+ let carried = 0;
8
+ for await (const piece of content) {
9
+ let part = toBytes$1(piece);
10
+ while (part.length) {
11
+ if (!carry && part.length >= size) {
12
+ yield part.slice(0, size);
13
+ part = part.subarray(size);
14
+ continue;
15
+ }
16
+ carry ??= new Uint8Array(size);
17
+ const n = Math.min(size - carried, part.length);
18
+ carry.set(part.subarray(0, n), carried);
19
+ carried += n;
20
+ part = part.subarray(n);
21
+ if (carried === size) {
22
+ yield carry;
23
+ carry = void 0;
24
+ carried = 0;
25
+ }
26
+ }
27
+ }
28
+ if (carry && carried) yield carry.slice(0, carried);
29
+ }
30
+ /** A FilesApi whose every call goes through a FilesRpc; content moves in keyed chunks. */
31
+ var FilesRpcClient = class {
32
+ #rpc;
33
+ #chunkSize;
34
+ #readAhead;
35
+ #writeAhead;
36
+ #pageSize;
37
+ constructor(options) {
38
+ this.#rpc = options.rpc;
39
+ this.#chunkSize = Math.max(1, options.chunkSize ?? 262144);
40
+ this.#readAhead = Math.max(1, options.readAhead ?? 4);
41
+ this.#writeAhead = Math.max(1, options.writeAhead ?? 4);
42
+ this.#pageSize = Math.max(1, options.pageSize ?? 256);
43
+ }
44
+ /**
45
+ * Asks for chunks 0, 1, 2, … keeping `readAhead` requests in flight, and yields them in key
46
+ * order whatever order the answers come in.
47
+ */
48
+ async *read(path, options = {}) {
49
+ const { start, length, signal } = options;
50
+ if (length === 0) return;
51
+ signal?.throwIfAborted();
52
+ const { streamId } = await this.#rpc.openRead(path, {
53
+ start,
54
+ length,
55
+ streamId: newStreamId$1()
56
+ });
57
+ const pending = /* @__PURE__ */ new Map();
58
+ let fail;
59
+ const aborted = signal ? new Promise((_, reject) => {
60
+ fail = () => reject(signal.reason ?? /* @__PURE__ */ new Error("aborted"));
61
+ if (signal.aborted) fail();
62
+ else signal.addEventListener("abort", fail, { once: true });
63
+ }) : void 0;
64
+ aborted?.catch(() => {});
65
+ let asked = 0;
66
+ let finished = false;
67
+ try {
68
+ for (let next = 0;; next++) {
69
+ while (!finished && asked < next + this.#readAhead) {
70
+ const request = this.#rpc.readChunk(streamId, asked);
71
+ request.catch(() => {});
72
+ pending.set(asked++, request);
73
+ }
74
+ const answer = pending.get(next);
75
+ pending.delete(next);
76
+ const { data, done } = await (aborted ? Promise.race([answer, aborted]) : answer);
77
+ signal?.throwIfAborted();
78
+ if (done) finished = true;
79
+ const bytes = toBytes$1(data);
80
+ if (bytes.length) yield bytes;
81
+ if (done) return;
82
+ signal?.throwIfAborted();
83
+ }
84
+ } finally {
85
+ if (fail) signal?.removeEventListener("abort", fail);
86
+ await this.#rpc.closeRead(streamId).catch(() => {});
87
+ }
88
+ }
89
+ /**
90
+ * Sends chunks of `chunkSize` bytes keyed 0, 1, 2, … with up to `writeAhead` unacknowledged,
91
+ * then `closeWrite` with their count; any failure aborts the write.
92
+ */
93
+ async write(path, content) {
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
+ });
108
+ const inFlight = /* @__PURE__ */ new Set();
109
+ let failure;
110
+ let count = 0;
111
+ try {
112
+ for await (const chunk of rechunk(content, this.#chunkSize)) {
113
+ if (failure) throw failure.error;
114
+ const sent = this.#rpc.writeChunk(streamId, count++, chunk).then(() => {
115
+ inFlight.delete(sent);
116
+ }, (error) => {
117
+ inFlight.delete(sent);
118
+ failure ??= { error };
119
+ });
120
+ inFlight.add(sent);
121
+ while (inFlight.size >= this.#writeAhead) await Promise.race(inFlight);
122
+ }
123
+ await Promise.all(inFlight);
124
+ if (failure) throw failure.error;
125
+ await this.#rpc.closeWrite(streamId, count);
126
+ } catch (e) {
127
+ await this.#rpc.abortWrite(streamId, String(e?.message ?? e)).catch(() => {});
128
+ throw e;
129
+ }
130
+ }
131
+ async mkdir(path) {
132
+ await this.#rpc.mkdir(path, { requestId: newStreamId$1() });
133
+ }
134
+ /** Pages of `pageSize`, each asked after the last path of the one before. */
135
+ async *list(path, options = {}) {
136
+ let after = options.after;
137
+ for (;;) {
138
+ const page = await this.#rpc.list(path, {
139
+ recursive: options.recursive,
140
+ after,
141
+ limit: this.#pageSize
142
+ });
143
+ yield* page.entries;
144
+ if (!page.more || page.entries.length === 0) return;
145
+ after = page.entries[page.entries.length - 1]?.path;
146
+ }
147
+ }
148
+ stats(path) {
149
+ return this.#rpc.stats(path);
150
+ }
151
+ exists(path) {
152
+ return this.#rpc.exists(path);
153
+ }
154
+ remove(path) {
155
+ return this.#rpc.remove(path, { requestId: newStreamId$1() });
156
+ }
157
+ move(source, target) {
158
+ return this.#rpc.move(source, target, { requestId: newStreamId$1() });
159
+ }
160
+ copy(source, target) {
161
+ return this.#rpc.copy(source, target, { requestId: newStreamId$1() });
162
+ }
163
+ };
164
+ /** A FilesApi over a FilesRpc. */
165
+ function newFilesRpcClient(options) {
166
+ return new FilesRpcClient(options);
167
+ }
168
+ //#endregion
169
+ //#region src/errors.ts
170
+ /**
171
+ * An error of the protocol itself. The code also leads the message (`UNKNOWN_STREAM: …`), since
172
+ * a structured clone keeps an error's name and message but not its other properties:
173
+ * `errorCode(e)` reads it back on the other side of a transport.
174
+ */
175
+ var FilesRpcError = class extends Error {
176
+ code;
177
+ constructor(code, message) {
178
+ super(`${code}: ${message}`);
179
+ this.name = "FilesRpcError";
180
+ this.code = code;
181
+ }
182
+ };
183
+ /** The FilesRpcError code of an error, also after it crossed a transport; undefined otherwise. */
184
+ function errorCode(e) {
185
+ const code = e?.code;
186
+ if (typeof code === "string" && CODES.has(code)) return code;
187
+ return LEADING_CODE.exec(String(e?.message ?? ""))?.[1];
188
+ }
189
+ const CODES = /* @__PURE__ */ new Set([
190
+ "UNKNOWN_STREAM",
191
+ "CHUNK_RELEASED",
192
+ "TOO_MANY_STREAMS",
193
+ "ABORTED",
194
+ "INVALID"
195
+ ]);
196
+ /** A message led by a code, as FilesRpcError writes it (`CODE: …`). */
197
+ const LEADING_CODE = new RegExp(`^(${[...CODES].join("|")}):`);
198
+ //#endregion
199
+ //#region src/keyed.ts
200
+ /**
201
+ * Values kept by key, and waiters for keys that have no value yet: what both ends of a stream
202
+ * use to keep chunks by chunkId and wait for the ones still on their way.
203
+ */
204
+ var Keyed = class {
205
+ #values = /* @__PURE__ */ new Map();
206
+ #waiters = /* @__PURE__ */ new Map();
207
+ #failure;
208
+ get size() {
209
+ return this.#values.size;
210
+ }
211
+ has(key) {
212
+ return this.#values.has(key);
213
+ }
214
+ get(key) {
215
+ return this.#values.get(key);
216
+ }
217
+ keys() {
218
+ return this.#values.keys();
219
+ }
220
+ /** Keeps `value` under `key` and wakes whoever waits for it. */
221
+ put(key, value) {
222
+ this.#values.set(key, value);
223
+ const waiting = this.#waiters.get(key);
224
+ if (!waiting) return;
225
+ this.#waiters.delete(key);
226
+ for (const w of waiting) w.resolve(value);
227
+ }
228
+ delete(key) {
229
+ this.#values.delete(key);
230
+ }
231
+ /** The value of `key`: now if kept, else when it is put (or the whole set fails). */
232
+ wait(key) {
233
+ if (this.#values.has(key)) return Promise.resolve(this.#values.get(key));
234
+ if (this.#failure) return Promise.reject(this.#failure.error);
235
+ return new Promise((resolve, reject) => {
236
+ const list = this.#waiters.get(key);
237
+ if (list) list.push({
238
+ resolve,
239
+ reject
240
+ });
241
+ else this.#waiters.set(key, [{
242
+ resolve,
243
+ reject
244
+ }]);
245
+ });
246
+ }
247
+ /** Answers every waiter for a key at or above `from` with `value` (the end of a stream). */
248
+ settleFrom(from, value) {
249
+ for (const [key, waiting] of [...this.#waiters]) {
250
+ if (key < from) continue;
251
+ this.#waiters.delete(key);
252
+ for (const w of waiting) w.resolve(value(key));
253
+ }
254
+ }
255
+ /** Fails every waiter, now and later. */
256
+ fail(error) {
257
+ this.#failure ??= { error };
258
+ for (const waiting of this.#waiters.values()) for (const w of waiting) w.reject(error);
259
+ this.#waiters.clear();
260
+ }
261
+ };
262
+ //#endregion
263
+ //#region src/server.ts
264
+ /**
265
+ * Whether a write's outcome is the FilesApi's alone: every chunk was handed to it (an emulated
266
+ * append, until its write-back starts, can still be cancelled).
267
+ */
268
+ const committed = (s) => s.total !== void 0 && s.next >= s.total && (!s.emulated || s.committing === true);
269
+ /** `path` in normal form: one leading slash, no empty, "." or ".." segments, no trailing slash. */
270
+ function normalPath(path) {
271
+ const out = [];
272
+ for (const seg of String(path).split("/")) {
273
+ if (!seg || seg === ".") continue;
274
+ if (seg === "..") out.pop();
275
+ else out.push(seg);
276
+ }
277
+ return `/${out.join("/")}`;
278
+ }
279
+ const messageOf = (e) => String(e?.message ?? e);
280
+ const EMPTY = /* @__PURE__ */ new Uint8Array();
281
+ const STREAM_ID = /^[A-Za-z0-9_-]{16,128}$/;
282
+ function newStreamId() {
283
+ return crypto.randomUUID().replace(/-/g, "");
284
+ }
285
+ function toBytes(data) {
286
+ if (data instanceof Uint8Array) return data;
287
+ if (ArrayBuffer.isView(data)) return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
288
+ if (data instanceof ArrayBuffer) return new Uint8Array(data);
289
+ throw new FilesRpcError("INVALID", "a chunk is a Uint8Array or an ArrayBuffer");
290
+ }
291
+ function chunkIdOf(v) {
292
+ const n = Number(v);
293
+ if (!Number.isSafeInteger(n) || n < 0) throw new FilesRpcError("INVALID", `a chunk id is an integer from 0 (got ${String(v)})`);
294
+ return n;
295
+ }
296
+ /** Orders paths by Unicode code point, as `FilesApi.list()` does (webrun-files' comparePaths). */
297
+ function comparePaths(a, b) {
298
+ const x = a[Symbol.iterator]();
299
+ const y = b[Symbol.iterator]();
300
+ for (;;) {
301
+ const p = x.next();
302
+ const q = y.next();
303
+ if (p.done || q.done) return p.done && q.done ? 0 : p.done ? -1 : 1;
304
+ const d = p.value.codePointAt(0) - q.value.codePointAt(0);
305
+ if (d !== 0) return d;
306
+ }
307
+ }
308
+ /** Joins the first `n` buffered bytes into one chunk. */
309
+ function take(s, n) {
310
+ const out = new Uint8Array(n);
311
+ let at = 0;
312
+ while (at < n) {
313
+ const head = s.buffer[0];
314
+ const part = head.subarray(0, n - at);
315
+ out.set(part, at);
316
+ at += part.length;
317
+ if (part.length === head.length) s.buffer.shift();
318
+ else s.buffer[0] = head.subarray(part.length);
319
+ }
320
+ s.buffered -= n;
321
+ return out;
322
+ }
323
+ /** Serves a FilesApi as the FilesRpc methods. */
324
+ function newFilesRpcServer(options) {
325
+ const fs = options.fs;
326
+ const chunkSize = Math.max(1, options.chunkSize ?? 262144);
327
+ const readAhead = Math.max(1, options.readAhead ?? 4);
328
+ const retain = Math.max(1, options.retain ?? 16);
329
+ const window = Math.max(1, options.window ?? 64);
330
+ const writeWindow = Math.max(1, options.writeWindow ?? 16);
331
+ const maxChunkSize = Math.max(1, options.maxChunkSize ?? 4194304);
332
+ const idleTimeout = options.idleTimeout ?? 12e4;
333
+ const maxStreams = options.maxStreams ?? 256;
334
+ const maxPageSize = Math.max(1, options.maxPageSize ?? 256);
335
+ const now = options.now ?? Date.now;
336
+ const streams = /* @__PURE__ */ new Map();
337
+ /**
338
+ * Streams that ended recently, by id: an open delivered again after its stream ended must not
339
+ * open it anew, and a closeWrite delivered again answers as the first one did.
340
+ */
341
+ const ended = /* @__PURE__ */ new Map();
342
+ /** Answers of metadata calls, by method and requestId, oldest first. */
343
+ const requests = /* @__PURE__ */ new Map();
344
+ const maxRequests = Math.max(1, options.maxRequests ?? 1024);
345
+ /** Runs `call` once per requestId (within idleTimeout): a call delivered again gets its answer. */
346
+ const once = (method, opts, call) => {
347
+ const id = opts?.requestId;
348
+ if (id === void 0) return call();
349
+ 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_-]"));
350
+ const key = `${method} ${id}`;
351
+ const kept = requests.get(key);
352
+ if (kept) return kept.answer;
353
+ const answer = call();
354
+ answer.catch(() => {});
355
+ const entry = {
356
+ at: now(),
357
+ settled: false,
358
+ answer
359
+ };
360
+ requests.set(key, entry);
361
+ const settle = () => {
362
+ entry.at = now();
363
+ entry.settled = true;
364
+ };
365
+ answer.then(settle, settle);
366
+ while (requests.size > maxRequests) {
367
+ let victim;
368
+ for (const [k, r] of requests) if (r.settled) {
369
+ victim = k;
370
+ break;
371
+ }
372
+ requests.delete(victim ?? requests.keys().next().value);
373
+ }
374
+ return answer;
375
+ };
376
+ const finish = (id, failure) => {
377
+ streams.delete(id);
378
+ ended.set(id, {
379
+ at: now(),
380
+ failure
381
+ });
382
+ };
383
+ const stream = (id, kind) => {
384
+ const s = streams.get(String(id));
385
+ if (!s || s.kind !== kind) throw new FilesRpcError("UNKNOWN_STREAM", `no open ${kind} stream '${String(id)}'`);
386
+ s.touched = now();
387
+ return s;
388
+ };
389
+ /** Runs `work` as a call in progress on `s`: the stream is not idle meanwhile. */
390
+ const busy = async (s, work) => {
391
+ s.busy++;
392
+ try {
393
+ return await work();
394
+ } finally {
395
+ s.busy--;
396
+ s.touched = now();
397
+ }
398
+ };
399
+ /** An id for a new stream, or the stream an open delivered twice already opened. */
400
+ const claim = (opts, kind) => {
401
+ const wanted = opts?.streamId;
402
+ if (wanted !== void 0) {
403
+ if (typeof wanted !== "string" || !STREAM_ID.test(wanted)) throw new FilesRpcError("INVALID", "a stream id is 16 to 128 characters of [A-Za-z0-9_-]");
404
+ const existing = streams.get(wanted);
405
+ if (existing) {
406
+ if (existing.kind !== kind) throw new FilesRpcError("INVALID", `stream '${wanted}' is a ${existing.kind} stream`);
407
+ return {
408
+ id: wanted,
409
+ existing
410
+ };
411
+ }
412
+ if (ended.has(wanted)) throw new FilesRpcError("INVALID", `stream '${wanted}' has ended`);
413
+ }
414
+ server.sweep();
415
+ if (streams.size >= maxStreams) throw new FilesRpcError("TOO_MANY_STREAMS", `at most ${maxStreams} open streams`);
416
+ return { id: wanted ?? newStreamId() };
417
+ };
418
+ /** Pulls the source until `readAhead` chunks from the highest asked exist, or it ends. */
419
+ const wanted = (s) => !s.ended && !s.closed && s.error === void 0 && s.produced < s.asked + readAhead;
420
+ const pump = (s) => {
421
+ if (s.pumping) return s.pumping;
422
+ const run = (async () => {
423
+ await void 0;
424
+ while (wanted(s)) {
425
+ if (s.buffered > chunkSize) {
426
+ const id = s.produced++;
427
+ s.chunks.put(id, {
428
+ data: take(s, chunkSize),
429
+ done: false
430
+ });
431
+ continue;
432
+ }
433
+ let next;
434
+ try {
435
+ next = await s.source.next();
436
+ } catch (e) {
437
+ while (s.buffered > 0) {
438
+ const id = s.produced++;
439
+ s.chunks.put(id, {
440
+ data: take(s, Math.min(chunkSize, s.buffered)),
441
+ done: false
442
+ });
443
+ }
444
+ s.error = e;
445
+ s.chunks.fail(e);
446
+ return;
447
+ }
448
+ if (next.done) {
449
+ while (s.buffered > chunkSize) {
450
+ const id = s.produced++;
451
+ s.chunks.put(id, {
452
+ data: take(s, chunkSize),
453
+ done: false
454
+ });
455
+ }
456
+ const id = s.produced++;
457
+ s.chunks.put(id, {
458
+ data: s.buffered ? take(s, s.buffered) : EMPTY,
459
+ done: true
460
+ });
461
+ s.ended = true;
462
+ s.chunks.settleFrom(s.produced, () => ({
463
+ data: EMPTY,
464
+ done: true
465
+ }));
466
+ return;
467
+ }
468
+ let part;
469
+ try {
470
+ part = toBytes(next.value);
471
+ } catch {
472
+ s.error = new FilesRpcError("INVALID", "the source yielded something that is not bytes");
473
+ s.chunks.fail(s.error);
474
+ return;
475
+ }
476
+ if (part.length) {
477
+ s.buffer.push(part);
478
+ s.buffered += part.length;
479
+ }
480
+ }
481
+ })();
482
+ s.pumping = run;
483
+ run.finally(() => {
484
+ s.pumping = void 0;
485
+ if (wanted(s)) pump(s);
486
+ });
487
+ return run;
488
+ };
489
+ const closeSource = async (s) => {
490
+ s.closed = true;
491
+ s.chunks.fail(new FilesRpcError("UNKNOWN_STREAM", "the stream was closed"));
492
+ await s.pumping?.catch(() => {});
493
+ await Promise.resolve(s.source.return?.()).catch(() => {});
494
+ };
495
+ /** The chunks of a write stream in order, as they arrive: what the FilesApi's write consumes. */
496
+ async function* ordered(s) {
497
+ for (;;) {
498
+ s.touched = now();
499
+ if (s.failure !== void 0) throw s.failure;
500
+ const chunk = s.chunks.get(s.next);
501
+ if (chunk) {
502
+ s.chunks.delete(s.next);
503
+ s.next++;
504
+ for (const r of s.room.filter((r) => r.chunkId < s.next + writeWindow)) {
505
+ s.room.splice(s.room.indexOf(r), 1);
506
+ r.resolve();
507
+ }
508
+ yield chunk;
509
+ continue;
510
+ }
511
+ if (s.total !== void 0 && s.next >= s.total) return;
512
+ await new Promise((resolve) => {
513
+ s.wake = resolve;
514
+ });
515
+ s.wake = void 0;
516
+ }
517
+ }
518
+ /** Emulated appends in progress, by normal path: one at a time per file. */
519
+ const appending = /* @__PURE__ */ new Map();
520
+ /**
521
+ * Appends `content` to `path`: through the FilesApi's own `append`, or with read, write and
522
+ * remove only. The emulation never reads and writes one path at once (a FilesApi may empty a
523
+ * file when a write starts):
524
+ * 1. the file's content and the chunks go to a temporary sibling;
525
+ * 2. that sibling is written back over the file, then removed.
526
+ * A failure or an abort in step 1 leaves the file as it was and removes the sibling. A failure
527
+ * in step 2 keeps the sibling (the only complete copy) and names it in the error. Appends to
528
+ * one file through this server run one after the other; other writers are not excluded.
529
+ */
530
+ const append = async (path, content, id, s) => {
531
+ const own = fs.append;
532
+ if (typeof own === "function") return own.call(fs, path, content);
533
+ s.emulated = true;
534
+ const file = normalPath(path);
535
+ const run = (appending.get(file) ?? Promise.resolve()).then(async () => {
536
+ if ((await fs.stats(file))?.kind === "directory") throw new FilesRpcError("INVALID", `cannot append to a directory: ${file}`);
537
+ const slash = file.lastIndexOf("/");
538
+ const temp = `${file.slice(0, slash + 1)}.${file.slice(slash + 1, slash + 65)}.append-${id.slice(0, 16)}`;
539
+ try {
540
+ await fs.write(temp, (async function* () {
541
+ yield* fs.read(file);
542
+ yield* content;
543
+ })());
544
+ if (s.failure !== void 0) throw s.failure;
545
+ } catch (e) {
546
+ await fs.remove(temp).catch(() => {});
547
+ throw e;
548
+ }
549
+ s.committing = true;
550
+ try {
551
+ await fs.write(file, fs.read(temp));
552
+ } catch (e) {
553
+ 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 });
554
+ }
555
+ await fs.remove(temp).catch(() => {});
556
+ });
557
+ const settled = run.catch(() => {});
558
+ appending.set(file, settled);
559
+ settled.then(() => {
560
+ if (appending.get(file) === settled) appending.delete(file);
561
+ });
562
+ return run;
563
+ };
564
+ const failWrite = (s, failure) => {
565
+ s.failure ??= failure;
566
+ for (const r of s.room.splice(0)) r.reject(s.failure);
567
+ s.wake?.();
568
+ };
569
+ const server = {
570
+ async stats(path) {
571
+ return fs.stats(path);
572
+ },
573
+ async exists(path) {
574
+ return fs.exists(path);
575
+ },
576
+ async mkdir(path, opts) {
577
+ await once("mkdir", opts, () => fs.mkdir(path));
578
+ },
579
+ async remove(path, opts) {
580
+ return once("remove", opts, () => fs.remove(path));
581
+ },
582
+ async move(source, target, opts) {
583
+ return once("move", opts, () => fs.move(source, target));
584
+ },
585
+ async copy(source, target, opts) {
586
+ return once("copy", opts, () => fs.copy(source, target));
587
+ },
588
+ async list(path, opts = {}) {
589
+ const limit = Math.min(Math.max(1, Math.floor(Number(opts.limit) || maxPageSize)), maxPageSize);
590
+ const entries = [];
591
+ let more = false;
592
+ const after = opts.after;
593
+ for await (const entry of fs.list(path, {
594
+ recursive: !!opts.recursive,
595
+ after
596
+ })) {
597
+ if (after !== void 0 && comparePaths(entry.path, after) <= 0) continue;
598
+ if (entries.length === limit) {
599
+ more = true;
600
+ break;
601
+ }
602
+ entries.push({ ...entry });
603
+ }
604
+ return {
605
+ entries,
606
+ more
607
+ };
608
+ },
609
+ async openRead(path, opts = {}) {
610
+ const { id, existing } = claim(opts, "read");
611
+ if (existing) return { streamId: id };
612
+ const length = opts.length;
613
+ const source = length === 0 ? (async function* () {})() : fs.read(path, {
614
+ start: opts.start,
615
+ length
616
+ })[Symbol.asyncIterator]();
617
+ streams.set(id, {
618
+ kind: "read",
619
+ touched: now(),
620
+ busy: 0,
621
+ source,
622
+ chunks: new Keyed(),
623
+ buffer: [],
624
+ buffered: 0,
625
+ produced: 0,
626
+ low: 0,
627
+ delivered: /* @__PURE__ */ new Set(),
628
+ asked: 0,
629
+ released: 0,
630
+ ended: false,
631
+ closed: false
632
+ });
633
+ return { streamId: id };
634
+ },
635
+ async readChunk(streamId, chunkIdArg) {
636
+ const s = stream(streamId, "read");
637
+ const chunkId = chunkIdOf(chunkIdArg);
638
+ if (chunkId >= s.low + window) throw new FilesRpcError("INVALID", `chunk ${chunkId} is more than ${window} chunks past the lowest undelivered (${s.low})`);
639
+ if (chunkId < s.released && chunkId < s.produced && !s.chunks.has(chunkId)) throw new FilesRpcError("CHUNK_RELEASED", `chunk ${chunkId} of '${streamId}' was released (the reader moved past it)`);
640
+ if (chunkId > s.asked) {
641
+ s.asked = chunkId;
642
+ const keepFrom = s.asked - retain + 1;
643
+ for (; s.released < keepFrom; s.released++) if (s.released < s.low || s.delivered.has(s.released)) s.chunks.delete(s.released);
644
+ }
645
+ if (s.ended && chunkId >= s.produced) return {
646
+ data: EMPTY,
647
+ done: true
648
+ };
649
+ return busy(s, async () => {
650
+ const answer = s.chunks.wait(chunkId);
651
+ pump(s);
652
+ const result = await answer;
653
+ if (chunkId === s.low) {
654
+ s.low++;
655
+ while (s.delivered.delete(s.low)) s.low++;
656
+ } else if (chunkId > s.low) s.delivered.add(chunkId);
657
+ if (chunkId < s.released) s.chunks.delete(chunkId);
658
+ return result;
659
+ });
660
+ },
661
+ async closeRead(streamId) {
662
+ const s = streams.get(String(streamId));
663
+ if (s?.kind !== "read") return;
664
+ finish(String(streamId));
665
+ await closeSource(s);
666
+ },
667
+ async openWrite(path, opts = {}) {
668
+ const { id, existing } = claim(opts, "write");
669
+ if (existing) return { streamId: id };
670
+ const s = {
671
+ kind: "write",
672
+ touched: now(),
673
+ busy: 0,
674
+ chunks: /* @__PURE__ */ new Map(),
675
+ next: 0,
676
+ room: [],
677
+ done: Promise.resolve()
678
+ };
679
+ s.done = (opts.append ? append(path, ordered(s), id, s) : fs.write(path, ordered(s))).then(() => {
680
+ if (s.failure !== void 0) throw s.failure;
681
+ }, (e) => {
682
+ failWrite(s, e);
683
+ throw s.failure;
684
+ });
685
+ s.done.catch(() => {});
686
+ streams.set(id, s);
687
+ return { streamId: id };
688
+ },
689
+ async writeChunk(streamId, chunkIdArg, data) {
690
+ const s = stream(streamId, "write");
691
+ const chunkId = chunkIdOf(chunkIdArg);
692
+ const bytes = toBytes(data);
693
+ if (bytes.length > maxChunkSize) throw new FilesRpcError("INVALID", `a chunk is at most ${maxChunkSize} bytes`);
694
+ if (s.failure !== void 0) throw s.failure;
695
+ if (s.total !== void 0 && chunkId >= s.total) throw new FilesRpcError("INVALID", `chunk ${chunkId} is past the stream's ${s.total} chunks`);
696
+ if (chunkId < s.next || s.chunks.has(chunkId)) return;
697
+ if (chunkId >= s.next + 2 * writeWindow) throw new FilesRpcError("INVALID", `chunk ${chunkId} is more than ${2 * writeWindow} chunks past the next to write (${s.next})`);
698
+ if (chunkId >= s.next + writeWindow) {
699
+ await busy(s, () => new Promise((resolve, reject) => s.room.push({
700
+ chunkId,
701
+ resolve,
702
+ reject
703
+ })));
704
+ if (chunkId < s.next || s.chunks.has(chunkId)) return;
705
+ }
706
+ s.chunks.set(chunkId, bytes.slice());
707
+ if (chunkId === s.next) s.wake?.();
708
+ },
709
+ async closeWrite(streamId, chunksArg) {
710
+ const id = String(streamId);
711
+ const done = ended.get(id);
712
+ if (done && !streams.has(id)) {
713
+ if (done.failure !== void 0) throw done.failure;
714
+ return;
715
+ }
716
+ const s = stream(id, "write");
717
+ const total = chunkIdOf(chunksArg);
718
+ const highest = Math.max(s.next - 1, ...s.chunks.keys());
719
+ if (highest >= total) {
720
+ const error = new FilesRpcError("INVALID", `the stream has chunk ${highest}, so more than ${total} chunks`);
721
+ failWrite(s, error);
722
+ finish(id, error);
723
+ throw error;
724
+ }
725
+ s.total = total;
726
+ for (const r of s.room.filter((r) => r.chunkId >= total)) {
727
+ s.room.splice(s.room.indexOf(r), 1);
728
+ r.reject(new FilesRpcError("INVALID", `chunk ${r.chunkId} is past the stream's ${total} chunks`));
729
+ }
730
+ s.wake?.();
731
+ try {
732
+ await busy(s, () => s.done);
733
+ if (streams.get(id) === s) finish(id);
734
+ } catch (e) {
735
+ if (streams.get(id) === s) finish(id, e);
736
+ throw e;
737
+ }
738
+ },
739
+ async abortWrite(streamId, reason) {
740
+ const id = String(streamId);
741
+ const s = streams.get(id);
742
+ if (s?.kind !== "write") return;
743
+ if (committed(s)) return;
744
+ const error = new FilesRpcError("ABORTED", reason ? String(reason) : "the write was aborted");
745
+ finish(id, error);
746
+ failWrite(s, error);
747
+ },
748
+ sweep() {
749
+ const t = now();
750
+ let dropped = 0;
751
+ for (const [id, s] of [...streams]) {
752
+ if (t - s.touched <= idleTimeout) continue;
753
+ if (s.kind === "read" && s.busy > 0) continue;
754
+ if (s.kind === "write" && (s.chunks.has(s.next) || s.total !== void 0 && s.next >= s.total)) continue;
755
+ dropped++;
756
+ const error = new FilesRpcError("ABORTED", "the stream was idle too long");
757
+ finish(id, error);
758
+ if (s.kind === "read") closeSource(s);
759
+ else failWrite(s, error);
760
+ }
761
+ for (const [id, e] of [...ended]) if (t - e.at > idleTimeout) ended.delete(id);
762
+ for (const [key, r] of [...requests]) if (r.settled && t - r.at > idleTimeout) requests.delete(key);
763
+ return dropped;
764
+ },
765
+ openStreams() {
766
+ return streams.size;
767
+ },
768
+ async close() {
769
+ if (timer) clearInterval(timer);
770
+ const sources = [];
771
+ for (const [id, s] of [...streams]) {
772
+ if (s.kind === "write" && committed(s)) {
773
+ streams.delete(id);
774
+ continue;
775
+ }
776
+ const error = new FilesRpcError("ABORTED", "the server closed");
777
+ finish(id, error);
778
+ if (s.kind === "read") sources.push(closeSource(s));
779
+ else failWrite(s, error);
780
+ }
781
+ await Promise.race([Promise.all(sources), new Promise((r) => {
782
+ setTimeout(r, 1e3).unref?.();
783
+ })]);
784
+ }
785
+ };
786
+ const timer = options.sweepInterval && options.sweepInterval > 0 ? setInterval(() => server.sweep(), options.sweepInterval) : void 0;
787
+ timer?.unref?.();
788
+ return server;
789
+ }
790
+ //#endregion
791
+ export { FilesRpcClient, FilesRpcError, errorCode, newFilesRpcClient, newFilesRpcServer };