@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.
- package/LICENSE +21 -0
- package/README.md +294 -2
- package/dist/cjs/index.cjs +796 -0
- package/dist/client.d.ts +45 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/errors.d.ts +13 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/esm/index.js +791 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/keyed.d.ts +21 -0
- package/dist/keyed.d.ts.map +1 -0
- package/dist/server.d.ts +51 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/types.d.ts +108 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +54 -3
- package/src/client.ts +225 -0
- package/src/errors.ts +34 -0
- package/src/index.ts +21 -0
- package/src/keyed.ts +57 -0
- package/src/server.ts +736 -0
- package/src/types.ts +120 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022-2026 statewalker
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,295 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @statewalker/webrun-files-rpc
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## What it is
|
|
4
|
+
|
|
5
|
+
A `FilesApi` (from `@statewalker/webrun-files`) over any RPC, with streamed reads and writes.
|
|
6
|
+
|
|
7
|
+
- **Server** (`newFilesRpcServer({ fs })`): serves any `FilesApi` as plain method calls.
|
|
8
|
+
- **Client** (`newFilesRpcClient({ rpc })`): a `FilesApi` whose every call goes through those
|
|
9
|
+
methods: a Durable Object or Workers RPC stub, comlink, `@statewalker/webrun-rpc`, or the server
|
|
10
|
+
object itself. It also appends (`append(path, content)`), which `FilesApi` does not.
|
|
11
|
+
|
|
12
|
+
Content moves as **chunks keyed by `(streamId, chunkId)`**. Both sides keep chunks by their key
|
|
13
|
+
and wait for the ones still on their way:
|
|
14
|
+
- the server answers `readChunk(streamId, k)` once chunk `k` exists;
|
|
15
|
+
- it writes chunk `k` once chunks `0..k-1` are written, whatever order they arrived in.
|
|
16
|
+
|
|
17
|
+
So the client can keep several chunks in flight, the transport may deliver calls out of order or
|
|
18
|
+
twice, and every byte still lands in order.
|
|
19
|
+
|
|
20
|
+
## Why it exists
|
|
21
|
+
|
|
22
|
+
A `FilesApi` streams: `read()` yields chunks and `write()` takes them. An RPC usually doesn't:
|
|
23
|
+
- Durable Object RPC carries a `ReadableStream` only on workerd; other Workers-compatible runtimes
|
|
24
|
+
refuse it ("RPC streams cannot cross isolate boundaries").
|
|
25
|
+
- JSON and message-based transports have no streams at all.
|
|
26
|
+
|
|
27
|
+
`@statewalker/webrun-files-http` solves this for HTTP, where request and response bodies stream.
|
|
28
|
+
This package solves it for everything else: a stream becomes a sequence of ordinary calls, each
|
|
29
|
+
carrying one chunk, and any transport that can make a call can carry a file of any size.
|
|
30
|
+
|
|
31
|
+
Keys make the calls safe on real transports:
|
|
32
|
+
- **Pipelining:** several `readChunk` or `writeChunk` calls may be in flight; the answers are put
|
|
33
|
+
back in order by `chunkId`.
|
|
34
|
+
- **Reordering:** a chunk that arrives early is kept until the ones before it are written.
|
|
35
|
+
- **Retries:**
|
|
36
|
+
- a chunk written twice is written once;
|
|
37
|
+
- a chunk read twice is answered twice;
|
|
38
|
+
- an open with the same `streamId` opens one stream;
|
|
39
|
+
- a `closeWrite` delivered again answers as the first did.
|
|
40
|
+
- a metadata call (`mkdir`, `remove`, `move`, `copy`) with a `requestId` runs once: delivered
|
|
41
|
+
again, concurrently or later, it gets the first answer (or failure). The server keeps answers
|
|
42
|
+
for `idleTimeout`, at most `maxRequests` of them. The client sends a fresh `requestId` with
|
|
43
|
+
every such call.
|
|
44
|
+
- Without a `requestId`, a metadata call delivered twice runs twice: the end state is the same
|
|
45
|
+
when nothing else writes in between, but the second may report `false` though the first did
|
|
46
|
+
it, and a late `copy` retry copies again.
|
|
47
|
+
|
|
48
|
+
## How to use
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pnpm add @statewalker/webrun-files-rpc @statewalker/webrun-files
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Inside this repository, depend on it with `"@statewalker/webrun-files-rpc": "workspace:^"`.
|
|
55
|
+
|
|
56
|
+
One entry point: ESM (`dist/esm/index.js`), CommonJS (`dist/cjs/index.cjs`), types
|
|
57
|
+
(`dist/index.d.ts`); sources in `src/`. The bundle has no imports: `@statewalker/webrun-files` is
|
|
58
|
+
used for its types only.
|
|
59
|
+
|
|
60
|
+
Exports:
|
|
61
|
+
- `newFilesRpcServer`, `newFilesRpcClient`, `FilesRpcClient`;
|
|
62
|
+
- `FilesRpcError` and `errorCode`;
|
|
63
|
+
- types `FilesRpc`, `FilesRpcServer`, `FilesRpcServerOptions`, `FilesRpcClientOptions`,
|
|
64
|
+
`AppendableFilesApi`, `ReadResult`, `ListPage`, `ListPageOptions`, `OpenOptions`,
|
|
65
|
+
`OpenReadOptions`, `OpenWriteOptions`, `RequestOptions`, `FilesRpcErrorCode`.
|
|
66
|
+
|
|
67
|
+
### `newFilesRpcServer(options)`
|
|
68
|
+
|
|
69
|
+
| Option | Default | |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| `fs` | (required) | The `FilesApi` served. |
|
|
72
|
+
| `chunkSize` | 256 KiB | Largest chunk a read answers. |
|
|
73
|
+
| `readAhead` | 4 | Chunks produced ahead, counting from the highest one asked. |
|
|
74
|
+
| `retain` | 16 | A delivered chunk is dropped once the reader asked for one `retain` chunks past it (until then it can be asked again). |
|
|
75
|
+
| `window` | 64 | A read request may name a chunk at most `window` past the lowest chunk not yet delivered (`INVALID` beyond): what one stream makes the server pull and hold stays within `window + readAhead` chunks. |
|
|
76
|
+
| `writeWindow` | 16 | Chunks of a write kept ahead of the next one to write; a chunk beyond it waits (backpressure), one beyond two windows is refused. |
|
|
77
|
+
| `maxChunkSize` | 4 MiB | Largest chunk a write takes. |
|
|
78
|
+
| `idleTimeout` | 2 min | A stream not used for this long is dropped (a read closed, a write aborted). |
|
|
79
|
+
| `maxStreams` | 256 | Open streams at most (`TOO_MANY_STREAMS` beyond). |
|
|
80
|
+
| `maxPageSize` | 256 | Largest listing page. |
|
|
81
|
+
| `maxRequests` | 1024 | Answers of metadata calls kept by `requestId` (oldest dropped beyond). |
|
|
82
|
+
| `sweepInterval` | none | Sweep idle streams every this many ms. Without it, opening a stream sweeps; a server that stops receiving opens needs this (or calls to `sweep()`). |
|
|
83
|
+
| `now` | `Date.now` | The clock (tests). |
|
|
84
|
+
|
|
85
|
+
The server is a `FilesRpc` plus:
|
|
86
|
+
- `sweep()`: drops idle streams and returns how many (opening a stream also sweeps);
|
|
87
|
+
- `openStreams()`: how many streams are open;
|
|
88
|
+
- `close()`: ends every stream and stops the sweep timer. It doesn't wait for the FilesApi's
|
|
89
|
+
writes to settle, and waits at most a second for sources to end.
|
|
90
|
+
|
|
91
|
+
### `newFilesRpcClient(options)`
|
|
92
|
+
|
|
93
|
+
| Option | Default | |
|
|
94
|
+
|---|---|---|
|
|
95
|
+
| `rpc` | (required) | The server's methods, through any transport. |
|
|
96
|
+
| `chunkSize` | 256 KiB | Bytes per chunk written (at most the server's `maxChunkSize`). |
|
|
97
|
+
| `readAhead` | 4 | `readChunk` calls in flight while reading (below the server's `window`). |
|
|
98
|
+
| `writeAhead` | 4 | `writeChunk` calls not yet answered while writing. |
|
|
99
|
+
| `pageSize` | 256 | Listing page size. |
|
|
100
|
+
|
|
101
|
+
Besides the `FilesApi` methods, the client has `append(path, content)`: it adds `content` to the
|
|
102
|
+
end of `path` (creating it if it does not exist), streamed exactly as `write` streams.
|
|
103
|
+
|
|
104
|
+
### The protocol (`FilesRpc`)
|
|
105
|
+
|
|
106
|
+
Every argument and result is a plain value: strings, numbers, booleans, plain objects and
|
|
107
|
+
`Uint8Array`.
|
|
108
|
+
|
|
109
|
+
| Call | Answer |
|
|
110
|
+
|---|---|
|
|
111
|
+
| `openRead(path, {start?, length?, streamId?})` | `{streamId}`; a file that does not exist reads as nothing |
|
|
112
|
+
| `readChunk(streamId, chunkId)` | `{data, done}` once chunk `chunkId` exists; past the end `{data: empty, done: true}` |
|
|
113
|
+
| `closeRead(streamId)` | ends the stream (unknown or ended: nothing happens) |
|
|
114
|
+
| `openWrite(path, {streamId?, append?})` | `{streamId}`; the `FilesApi`'s `write` (or, with `append`, the append) starts consuming chunks in order |
|
|
115
|
+
| `writeChunk(streamId, chunkId, data)` | resolves once the chunk is kept (within `writeWindow` of the next to write: at once) |
|
|
116
|
+
| `closeWrite(streamId, chunks)` | resolves once all `chunks` chunks are written |
|
|
117
|
+
| `abortWrite(streamId, reason?)` | cancels the write (unknown or ended: nothing happens) |
|
|
118
|
+
| `list(path, {recursive?, after?, limit?})` | `{entries, more}`; ask again with `after` = the last path |
|
|
119
|
+
| `stats`, `exists` | as the `FilesApi` |
|
|
120
|
+
| `mkdir(path, {requestId?})`, `remove(path, {requestId?})`, `move(source, target, {requestId?})`, `copy(source, target, {requestId?})` | as the `FilesApi`; with a `requestId`, once (a call delivered again gets the first answer) |
|
|
121
|
+
|
|
122
|
+
- **Stream ids:** `streamId` may be chosen by the caller: 16 to 128 characters of `[A-Za-z0-9_-]`,
|
|
123
|
+
random, such as `crypto.randomUUID()` without dashes. Then an open delivered twice opens one
|
|
124
|
+
stream; the client always does this.
|
|
125
|
+
- **Errors:** protocol errors are `FilesRpcError` with a `code`:
|
|
126
|
+
- `UNKNOWN_STREAM`, `CHUNK_RELEASED`, `TOO_MANY_STREAMS`, `ABORTED`, `INVALID`.
|
|
127
|
+
- The code also leads the message (`UNKNOWN_STREAM: …`), because a structured clone keeps an
|
|
128
|
+
error's message but not its other properties. `errorCode(e)` reads it on either side.
|
|
129
|
+
|
|
130
|
+
## Examples
|
|
131
|
+
|
|
132
|
+
### Both ends in one process
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
import { MemFilesApi } from "@statewalker/webrun-files-mem";
|
|
136
|
+
import { newFilesRpcClient, newFilesRpcServer } from "@statewalker/webrun-files-rpc";
|
|
137
|
+
|
|
138
|
+
const server = newFilesRpcServer({ fs: new MemFilesApi() });
|
|
139
|
+
const files = newFilesRpcClient({ rpc: server }); // any transport carrying these calls works
|
|
140
|
+
|
|
141
|
+
await files.write("/notes/today.md", [new TextEncoder().encode("# Today\n")]);
|
|
142
|
+
for await (const chunk of files.read("/notes/today.md")) console.log(new TextDecoder().decode(chunk));
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Served by a Durable Object, called through its stub
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
import { DurableObject } from "cloudflare:workers";
|
|
149
|
+
import { newFilesRpcClient, newFilesRpcServer, type FilesRpcServer } from "@statewalker/webrun-files-rpc";
|
|
150
|
+
|
|
151
|
+
export class Files extends DurableObject {
|
|
152
|
+
#files: FilesRpcServer = newFilesRpcServer({ fs: makeFilesApi(this.ctx) }); // any FilesApi
|
|
153
|
+
openRead(path: string, o?: object) { return this.#files.openRead(path, o); }
|
|
154
|
+
readChunk(id: string, k: number) { return this.#files.readChunk(id, k); }
|
|
155
|
+
closeRead(id: string) { return this.#files.closeRead(id); }
|
|
156
|
+
openWrite(path: string, o?: object) { return this.#files.openWrite(path, o); }
|
|
157
|
+
writeChunk(id: string, k: number, d: Uint8Array) { return this.#files.writeChunk(id, k, d); }
|
|
158
|
+
closeWrite(id: string, n: number) { return this.#files.closeWrite(id, n); }
|
|
159
|
+
abortWrite(id: string, r?: string) { return this.#files.abortWrite(id, r); }
|
|
160
|
+
list(path: string, o?: object) { return this.#files.list(path, o); }
|
|
161
|
+
stats(path: string) { return this.#files.stats(path); }
|
|
162
|
+
exists(path: string) { return this.#files.exists(path); }
|
|
163
|
+
mkdir(path: string) { return this.#files.mkdir(path); }
|
|
164
|
+
remove(path: string) { return this.#files.remove(path); }
|
|
165
|
+
move(a: string, b: string) { return this.#files.move(a, b); }
|
|
166
|
+
copy(a: string, b: string) { return this.#files.copy(a, b); }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// In a Worker: the stub has the same methods, so it is the client's `rpc`.
|
|
170
|
+
const files = newFilesRpcClient({ rpc: env.FILES.get(env.FILES.idFromName("main")) as never });
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
A stream lives in the server's memory: keep one stream's calls on one Durable Object (they are,
|
|
174
|
+
through one stub).
|
|
175
|
+
|
|
176
|
+
### The protocol by hand
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
const server = newFilesRpcServer({ fs: new MemFilesApi(), chunkSize: 4 });
|
|
180
|
+
const streamId = crypto.randomUUID().replace(/-/g, "");
|
|
181
|
+
await server.openWrite("/a.txt", { streamId });
|
|
182
|
+
await Promise.all([
|
|
183
|
+
server.writeChunk(streamId, 1, new TextEncoder().encode("world")), // out of order: kept
|
|
184
|
+
server.writeChunk(streamId, 0, new TextEncoder().encode("hello ")),
|
|
185
|
+
]);
|
|
186
|
+
await server.closeWrite(streamId, 2); // resolves once both are written, in order
|
|
187
|
+
|
|
188
|
+
const { streamId: r } = await server.openRead("/a.txt");
|
|
189
|
+
for (let chunkId = 0; ; chunkId++) {
|
|
190
|
+
const { data, done } = await server.readChunk(r, chunkId); // "hell", "o wo", "rld"
|
|
191
|
+
if (done) break;
|
|
192
|
+
}
|
|
193
|
+
await server.closeRead(r);
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
`tests/readme.test.ts` runs the first and the last example as written.
|
|
197
|
+
|
|
198
|
+
## Internals
|
|
199
|
+
|
|
200
|
+
- **Reads:** `openRead` opens the `FilesApi`'s `read()` iterator; nothing is pulled yet.
|
|
201
|
+
- `readChunk(k)` raises the highest id asked. A pump pulls the source until `readAhead` chunks
|
|
202
|
+
from it exist, cutting the bytes into chunks of exactly `chunkSize`.
|
|
203
|
+
- A chunk is cut only once more bytes follow it or the source has ended, so the last chunk
|
|
204
|
+
carries `done: true` and no empty chunk is needed.
|
|
205
|
+
- Chunks wait in a keyed store (`src/keyed.ts`); a request for a chunk not produced yet waits
|
|
206
|
+
there.
|
|
207
|
+
- If the source fails, the bytes before the failure are still served, and later chunks fail
|
|
208
|
+
with its error.
|
|
209
|
+
- **Release:** a delivered chunk is dropped once the reader asked for one `retain` chunks past it.
|
|
210
|
+
- A chunk below that line that nobody has received yet (asked out of order, or still to be
|
|
211
|
+
produced) is kept until its first delivery. So a client asking further ahead than the server
|
|
212
|
+
retains still gets every chunk.
|
|
213
|
+
- A chunk delivered and dropped is `CHUNK_RELEASED`.
|
|
214
|
+
- **Writes:** `openWrite` starts the `FilesApi`'s `write()` at once, on a generator that yields
|
|
215
|
+
the stream's chunks in key order and waits for the next one.
|
|
216
|
+
- `writeChunk` keeps a copy of its chunk; a view of a larger buffer is cut out.
|
|
217
|
+
- A chunk at or beyond `next + writeWindow` waits until the window moves; one at or beyond
|
|
218
|
+
`next + 2 × writeWindow` is refused (`INVALID`). So a writer makes the server hold at most two
|
|
219
|
+
windows of chunks.
|
|
220
|
+
- `closeWrite(n)` tells the generator where to stop and waits for `write()` to finish. If more
|
|
221
|
+
than `n` chunks arrived, the write fails with `INVALID`.
|
|
222
|
+
- `abortWrite`, expiry and `close()` make the generator throw, so `write()` fails. What then
|
|
223
|
+
remains of the file is up to the `FilesApi`: `MemFilesApi` writes only at the end, so the file
|
|
224
|
+
keeps what it had.
|
|
225
|
+
- `write()` starts at `openWrite`, not at the first chunk. A FilesApi that truncates when a
|
|
226
|
+
write starts has emptied the file already if the write is then aborted.
|
|
227
|
+
- **Appends:** `openWrite(path, { append: true })` streams like a write, and the chunks go to the
|
|
228
|
+
end of the file.
|
|
229
|
+
- A served FilesApi with an `append(path, content)` method (`AppendableFilesApi`) appends
|
|
230
|
+
through it, with whatever atomicity it gives.
|
|
231
|
+
- Any other FilesApi is appended to with `read`, `write` and `remove` only, in two steps:
|
|
232
|
+
1. the file's content and the chunks are written to a temporary sibling,
|
|
233
|
+
`.<name>.append-<id>` (the name cut to 64 characters, the stream id to 16);
|
|
234
|
+
2. the sibling is written back over the file, then removed.
|
|
235
|
+
- It never reads and writes one path at once, so a FilesApi that empties a file when a write
|
|
236
|
+
starts is safe. Only a directory is refused (`INVALID`); `stats` saying the file is missing is
|
|
237
|
+
not trusted, since reading a missing file yields nothing and the file is created all the same.
|
|
238
|
+
- A failure or an abort during step 1 leaves the file as it was and removes the sibling. An
|
|
239
|
+
abort counts until step 2 starts, even after every chunk was sent. A failure during step 2
|
|
240
|
+
may leave the file incomplete, so the sibling, the only complete copy, is kept and its path
|
|
241
|
+
is in the error.
|
|
242
|
+
- Appends to one file (by its normal path) through one server run one after the other. Other
|
|
243
|
+
writers are not excluded. The emulation copies the file twice and shows the sibling in
|
|
244
|
+
listings while it runs.
|
|
245
|
+
- `close()` and expiry leave a write whose outcome is the FilesApi's alone (every chunk handed
|
|
246
|
+
over, or an emulated append writing back), so a `closeWrite` waiting on it gets the real
|
|
247
|
+
outcome.
|
|
248
|
+
- **Ended streams:** ids that ended are remembered for `idleTimeout`. Within that time, an open
|
|
249
|
+
delivered after its stream ended does not reopen it, and a `closeWrite` delivered again gets the
|
|
250
|
+
first answer.
|
|
251
|
+
- **Idle and busy:** a stream is idle when nothing happened on it for `idleTimeout`: no call
|
|
252
|
+
started or ended, and (writes) the FilesApi took no chunk.
|
|
253
|
+
- A `readChunk` waiting on a slow source keeps its stream alive, and so does a write whose next
|
|
254
|
+
chunk is there for the FilesApi to take.
|
|
255
|
+
- A write waiting only for the holder (`writeChunk` waiting for room, `closeWrite` waiting for a
|
|
256
|
+
missing chunk) is swept once idle; its waiting calls fail with `ABORTED`.
|
|
257
|
+
- A write whose chunks were all handed to the FilesApi is never swept. An `abortWrite` arriving
|
|
258
|
+
then is too late and does nothing (the commit stands).
|
|
259
|
+
- **Failures:** a write the FilesApi fails keeps its stream until it is closed, aborted or swept,
|
|
260
|
+
so the next `writeChunk` or `closeWrite` reports that failure, not `UNKNOWN_STREAM`. A source
|
|
261
|
+
yielding something that is not bytes fails the stream with `INVALID`.
|
|
262
|
+
- **Bounds:** a read request beyond `window` is refused at once, so asking for chunk
|
|
263
|
+
1,000,000,000 neither loops nor loads anything.
|
|
264
|
+
- **The client:**
|
|
265
|
+
- **Read:** it keeps `readAhead` `readChunk` calls in flight and yields the answers in key
|
|
266
|
+
order. Breaking out of the loop, or aborting through `signal`, closes the stream.
|
|
267
|
+
- **Write and append:** it cuts any sequence of byte arrays into chunks of exactly `chunkSize`
|
|
268
|
+
and keeps `writeAhead` unanswered. Any failure (of the source, or of a call) aborts the write.
|
|
269
|
+
- **List:** it asks page after page with `after`. The server also applies `after` itself, in
|
|
270
|
+
code-point order, so a FilesApi that ignores it cannot make pages repeat.
|
|
271
|
+
- **Not covered:**
|
|
272
|
+
- A stream lives in one server's memory; a server restart loses open streams (`UNKNOWN_STREAM`).
|
|
273
|
+
- Serializing `Uint8Array` for a JSON-only transport (base64, msgpack) is the transport's job.
|
|
274
|
+
- Per-caller authorization is the `FilesApi`'s or the transport's job: serve a guarded
|
|
275
|
+
`FilesApi`.
|
|
276
|
+
|
|
277
|
+
## Tests
|
|
278
|
+
|
|
279
|
+
`pnpm --filter @statewalker/webrun-files-rpc test` (Vitest; build the workspace first, the
|
|
280
|
+
shared suites are imported from `webrun-files-tests`' `dist/`). The protocol tests use their own
|
|
281
|
+
pseudo-random bytes and the transport simulator in `tests/helpers.ts`; byte collection and text
|
|
282
|
+
encoding come from `webrun-files-tests`.
|
|
283
|
+
|
|
284
|
+
| File | Covers |
|
|
285
|
+
|---|---|
|
|
286
|
+
| `server-read.test.ts` | numbered chunks with `done`, out-of-order and concurrent requests, ranges, missing files, retries, release, bounded read-ahead, closing, failing sources |
|
|
287
|
+
| `server-write.test.ts` | in-order and out-of-order chunks, duplicates, backpressure, `closeWrite` waiting for a gap or refusing a wrong count, empty files, abort, failing writes, byte types |
|
|
288
|
+
| `server-lifecycle.test.ts` | idle expiry, the stream limit, `close()`, unguessable ids, paged listing, metadata |
|
|
289
|
+
| `protocol.test.ts` | caller-chosen keys (opens and `closeWrite` delivered twice, after the end), what is refused, waiting requests failed by a close, error codes across a structured clone, many streams at once, release never dropping an undelivered chunk, metadata calls delivered twice (with a `requestId`: once, the same answer, failures too, kept for `idleTimeout` and `maxRequests`; without: the same end state) |
|
|
290
|
+
| `robustness.test.ts` | the window (huge chunk ids refused at once, bounded pulls), busy streams not swept, writes too far ahead refused, writes waiting for the holder swept, the sweep timer, a failing write's error reaching the client, a late abort not undoing a commit, a source yielding non-bytes, waiters past the count, aborting a hanging read, `close()` not hanging, listing with a FilesApi that ignores `after` |
|
|
291
|
+
| `client.test.ts` | the `FilesApi` round trip, iterables, ranges, pipelining depth, early stop and abort, failing sources, write depth, listing; delayed, reordered and duplicated calls (five seeds) |
|
|
292
|
+
| `append.test.ts` | appends through the FilesApi's own `append` and through the emulation, also on a FilesApi that writes as it goes: existing and missing files, large files, aborts (also after every chunk was sent) and failing sources leaving the file unchanged and no temporary file, a failed write-back keeping the temporary copy, `close()` during the write-back, two appends at once, `stats` not trusted, odd paths and long names, directories refused; over the transport simulator; an open delivered twice |
|
|
293
|
+
| `conformance.test.ts` | the repository's `FilesApi` suite (`webrun-files-tests`) on the client over the server: directly, and through the transport simulator (cloned, reordered, every call delivered twice at random) |
|
|
294
|
+
| `big-files.test.ts` | the repository's big-file suite (64 MiB) on the client over the server, every call structured-cloned |
|
|
295
|
+
| `readme.test.ts` | the examples above |
|