@earendil-works/pi-durable 1.0.1 → 1.0.3

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.
Files changed (88) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +21 -5
  3. package/dist/env/decode.d.ts +16 -0
  4. package/dist/env/decode.d.ts.map +1 -0
  5. package/dist/env/decode.js +28 -0
  6. package/dist/env/decode.js.map +1 -0
  7. package/dist/env/index.d.ts +155 -3
  8. package/dist/env/index.d.ts.map +1 -1
  9. package/dist/env/index.js +2 -0
  10. package/dist/env/index.js.map +1 -1
  11. package/dist/env/line-scan.d.ts +13 -0
  12. package/dist/env/line-scan.d.ts.map +1 -0
  13. package/dist/env/line-scan.js +147 -0
  14. package/dist/env/line-scan.js.map +1 -0
  15. package/dist/env/node-watch.d.ts +29 -0
  16. package/dist/env/node-watch.d.ts.map +1 -0
  17. package/dist/env/node-watch.js +431 -0
  18. package/dist/env/node-watch.js.map +1 -0
  19. package/dist/env/node.d.ts +11 -2
  20. package/dist/env/node.d.ts.map +1 -1
  21. package/dist/env/node.js +284 -21
  22. package/dist/env/node.js.map +1 -1
  23. package/dist/harness/agent.d.ts +2 -1
  24. package/dist/harness/agent.d.ts.map +1 -1
  25. package/dist/harness/agent.js +5 -0
  26. package/dist/harness/agent.js.map +1 -1
  27. package/dist/harness/compaction.d.ts.map +1 -1
  28. package/dist/harness/compaction.js +2 -0
  29. package/dist/harness/compaction.js.map +1 -1
  30. package/dist/harness/generation.d.ts.map +1 -1
  31. package/dist/harness/generation.js +8 -5
  32. package/dist/harness/generation.js.map +1 -1
  33. package/dist/harness/harness.d.ts.map +1 -1
  34. package/dist/harness/harness.js +4 -1
  35. package/dist/harness/harness.js.map +1 -1
  36. package/dist/harness/output.d.ts +13 -6
  37. package/dist/harness/output.d.ts.map +1 -1
  38. package/dist/harness/output.js +61 -16
  39. package/dist/harness/output.js.map +1 -1
  40. package/dist/harness/provider.d.ts +14 -0
  41. package/dist/harness/provider.d.ts.map +1 -0
  42. package/dist/harness/provider.js +30 -0
  43. package/dist/harness/provider.js.map +1 -0
  44. package/dist/harness/tool.d.ts.map +1 -1
  45. package/dist/harness/tool.js +12 -4
  46. package/dist/harness/tool.js.map +1 -1
  47. package/dist/harness/types.d.ts +24 -3
  48. package/dist/harness/types.d.ts.map +1 -1
  49. package/dist/harness/types.js.map +1 -1
  50. package/dist/harness/view.d.ts +1 -1
  51. package/dist/harness/view.d.ts.map +1 -1
  52. package/dist/harness/view.js +8 -1
  53. package/dist/harness/view.js.map +1 -1
  54. package/dist/index.d.ts +3 -2
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.js +2 -1
  57. package/dist/index.js.map +1 -1
  58. package/dist/testing/env-conformance.d.ts +7 -0
  59. package/dist/testing/env-conformance.d.ts.map +1 -0
  60. package/dist/testing/env-conformance.js +397 -0
  61. package/dist/testing/env-conformance.js.map +1 -0
  62. package/dist/testing/index.d.ts +3 -2
  63. package/dist/testing/index.d.ts.map +1 -1
  64. package/dist/testing/index.js +2 -1
  65. package/dist/testing/index.js.map +1 -1
  66. package/dist/testing/runner.d.ts +4 -2
  67. package/dist/testing/runner.d.ts.map +1 -1
  68. package/dist/testing/runner.js +9 -0
  69. package/dist/testing/runner.js.map +1 -1
  70. package/dist/testing/types.d.ts +18 -0
  71. package/dist/testing/types.d.ts.map +1 -1
  72. package/dist/testing/types.js.map +1 -1
  73. package/dist/tools/bash.d.ts +2 -1
  74. package/dist/tools/bash.d.ts.map +1 -1
  75. package/dist/tools/bash.js +5 -2
  76. package/dist/tools/bash.js.map +1 -1
  77. package/dist/tools/image.d.ts +10 -0
  78. package/dist/tools/image.d.ts.map +1 -1
  79. package/dist/tools/image.js +39 -0
  80. package/dist/tools/image.js.map +1 -1
  81. package/dist/tools/read.d.ts.map +1 -1
  82. package/dist/tools/read.js +148 -75
  83. package/dist/tools/read.js.map +1 -1
  84. package/dist/truncate.d.ts +9 -0
  85. package/dist/truncate.d.ts.map +1 -1
  86. package/dist/truncate.js +12 -4
  87. package/dist/truncate.js.map +1 -1
  88. package/package.json +3 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.0.3] - 2026-10-05
4
+
5
+ ### Breaking Changes
6
+
7
+ - `FileSystem` requires `openBinaryReader()` and `openDirReader()`; custom environments must implement them.
8
+ - `BinaryReader` requires `scanLines()`.
9
+ - `FileSystem` requires `watch()`.
10
+ - `Shell.exec()` accepts an argv array besides a shell string, and `ShellExecOptions.onOutput` receives a third `info` argument naming the stream (`stdout` or `stderr`); custom environments must accept both forms and pass the stream.
11
+
12
+ ### Added
13
+
14
+ - `openBinaryReader()` for bounded positional reads of one opened regular file, with `noFollow` to refuse a final-component symlink.
15
+ - `openDirReader()` for paged directory listings that read metadata only for returned entries.
16
+ - Argv form of `exec()`, which runs a program without a shell.
17
+ - `createEnvConformance()` and `registerEnvConformance()` in `@earendil-works/pi-durable/testing` for checking custom `ExecutionEnv` implementations.
18
+ - `settings.progress` with `partialIntervalMs` and `outputIntervalMs` configures how often generation partials and running tool output are committed; defaults stay 100 ms ([#10357](https://github.com/earendil-works/pi/issues/10357))
19
+ - `ShellExecOptions.window` and `ShellOutputInfo.skipped`: an environment may omit shell output outside the caller's retained tail and report the omission, so remote environments need not transfer output the caller drops. `ToolExecutionApi.outputWindow` provides the window and `output(chunk, skipped)` counts omissions; the `bash` tool passes them through.
20
+ - `BinaryReader.scanLines()` locates and measures a span of lines in one pass inside the environment; `LineScanner`, `StreamDecoder`, `rangeDecoder()` and `startsWithBom()` in `@earendil-works/pi-durable/env` let other environments decode and scan exactly like `NodeExecutionEnv`.
21
+ - `FileSystem.watch()` reports changes to files and directories, recursive with excludes, including missing targets, with explicit `overflow` and `error` and a `native` or `polling` mode. `NodeExecutionEnv` polls on Windows (native watchers there keep directories open, which blocks renaming their parents) and on network and FUSE file systems, and on macOS rescans shortly after installing native watchers because FSEvents misses changes made right after `fs.watch` returns; its `watch` option sets the mode, poll interval, and directory limit.
22
+
23
+ ### Changed
24
+
25
+ - The `read` tool reads only the file's header, one scan, and the lines it shows, instead of loading the whole file; its results are unchanged.
26
+
27
+ ### Fixed
28
+
29
+ - `NodeExecutionEnv.flushFile()` on a directory fails with `is_directory` on Windows, as on POSIX.
30
+ - Tail-retained tool output no longer depends on when progress commits happened: a progress snapshot compacted the stored output to the kept window, which could move where a later window's first line started.
31
+ - `NodeExecutionEnv` shell output and text line readers no longer drop a U+FEFF that follows a chunk boundary; Node's streaming `TextDecoder` with BOM handling dropped it, unlike decoding all of the bytes at once.
32
+
33
+ ## [1.0.2] - 2026-10-04
34
+
35
+ ### Fixed
36
+
37
+ - Persisted a distinct provider session UUID per conversation and forwarded it for prompt-cache and session affinity ([#10424](https://github.com/earendil-works/pi/issues/10424))
38
+
3
39
  ## [1.0.1] - 2026-10-03
4
40
 
5
41
  ## [1.0.0] - 2026-10-01
package/README.md CHANGED
@@ -81,7 +81,7 @@ Every async call takes a Chord `Context`, which carries cancellation. `BACKGROUN
81
81
  - **Conversation**: a transcript. `root()` creates the root conversation on first use; you can create more and fork them. A `Conversation` handle holds no state; compare handles by `id`.
82
82
  - **Entry**: one immutable transcript record, such as a user message (`pi.user`), a model response (`pi.assistant`), a tool result (`pi.tool-result`), a system prompt change (`pi.system`), a reset (`pi.reset`), or your own kind. The model sees the entries from the newest reset onward.
83
83
  - **Commit**: an atomic write. `conversation.commit((tx) => ...)` can append entries, edit documents, and create tasks together; either all of it is stored or none of it.
84
- - **Document**: typed JSON state stored next to the transcript and changed in commits. Built-in ones hold each conversation's agent choices (`pi.agent`), the running generation and tools (`pi.live`), queued submissions (`pi.inbox`), and spend (`pi.usage`).
84
+ - **Document**: typed JSON state stored next to the transcript and changed in commits. Built-in ones hold each conversation's agent choices (`pi.agent`), provider-facing session identity (`pi.provider`), running generation and tools (`pi.live`), queued submissions (`pi.inbox`), and spend (`pi.usage`).
85
85
  - **Task**: a durable state machine that saves a checkpoint at every step, so a restarted process continues from the last one. Every task has an owner: its conversation, or another task. The Harness runs answers as built-in tasks: `pi.generation` calls the model and owns the `pi.tool` tasks of its tool calls, waits for them, and hands the run to the next generation.
86
86
  - **Submission**: something you hand to a conversation, either user input or an entry to write, which you can wait for.
87
87
  - **Turn and run**: a turn is one model response and its tool calls; a run is the turns from an input to its final answer. A conversation is busy while a run is going.
@@ -110,7 +110,9 @@ const root = await harness.root(context); // the same root as last time
110
110
  harness.resume(); // continue any run the last process left unfinished
111
111
  ```
112
112
 
113
- Work interrupted by a crash or close stays pending. `resume()` starts the task scheduler; submitting or waiting starts it too. A retried submission with the same `requestId` returns the existing submission instead of submitting twice:
113
+ Work interrupted by a crash or close stays pending. `resume()` starts the task scheduler; submitting or waiting starts it too. Each conversation has its own persisted UUIDv7 in `pi.provider`, forwarded to pi-ai as `sessionId` for provider prompt-cache and session affinity. It survives reopen, retries, reset, compaction, and model changes; a child or fork receives a fresh identity. A legacy conversation receives and persists one before its first generation or compaction request.
114
+
115
+ A retried submission with the same `requestId` returns the existing submission instead of submitting twice:
114
116
 
115
117
  ```typescript
116
118
  const submission = await root.submit({ type: "input", content: "Hello", requestId: "greeting-1" }, context);
@@ -220,6 +222,7 @@ const harness = await Harness.open(storage, {
220
222
  stream: { timeoutMs: 120_000 },
221
223
  retry: { maxRetries: 3 },
222
224
  compaction: { reserveTokens: 16384 },
225
+ progress: { partialIntervalMs: 100, outputIntervalMs: 100 },
223
226
  toolExecution: "parallel",
224
227
  get followUpMode() {
225
228
  return userSettings.followUpMode;
@@ -244,6 +247,19 @@ const harness = await Harness.open(storage, {
244
247
 
245
248
  A throw from `env` becomes the call's error result. Without an environment, the built-in tools fail with an error result. A fresh environment object per call is fine: `edit` and `write` serialize changes to one file by the environment's `id` and path. A custom `ExecutionEnv` sets `id` so that equal ids see the same files at the same paths, for example one id per container.
246
249
 
250
+ Hosts can use the environment directly too, for example to show a project's files. `openBinaryReader()` reads byte ranges of one opened file, `openDirReader()` pages a directory, and `exec()` with an argv array runs a program without a shell, reporting which stream each output chunk came from:
251
+
252
+ ```typescript
253
+ const status = { stdout: "", stderr: "" };
254
+ await env.exec(["git", "status", "--porcelain=v2", "-z"], {
255
+ onOutput: (text, _context, { stream }) => {
256
+ status[stream] += text;
257
+ },
258
+ }, context);
259
+ ```
260
+
261
+ Abort the context to stop one call; `cleanup()` is for shutting the environment down. A custom environment can check itself with `registerEnvConformance()` from `@earendil-works/pi-durable/testing`, like storage below.
262
+
247
263
  ## Reload
248
264
 
249
265
  Installing an extension with an installed name replaces it in place, in one step:
@@ -265,7 +281,7 @@ const view = await root.viewState(context);
265
281
  view.subscribe((value) => {
266
282
  // value.entries: the active transcript
267
283
  // value.docs["pi.live"]: the running generation (streamed partial, retry, deferred) and tool calls (output, details)
268
- // value.docs["pi.inbox"], value.docs["pi.usage"], value.docs["pi.agent"]
284
+ // value.docs["pi.inbox"], value.docs["pi.usage"], value.docs["pi.agent"], value.docs["pi.provider"]
269
285
  render(value);
270
286
  });
271
287
  // later: view.dispose();
@@ -284,7 +300,7 @@ watch.start(async (value, ops) => {
284
300
 
285
301
  A slow watch keeps at most 100 undelivered frames. After that, the pending frames are replaced by one frame holding the whole newest view. A client that joins late or reconnects starts from the current view; nothing is replayed.
286
302
 
287
- Partial answers and tool output are committed at most every 100 ms, so a crash loses at most that window.
303
+ Partial answers and tool output are committed at most every 100 ms by default, so a crash loses at most that window. `settings.progress` changes the intervals; a host whose storage is remote can commit less often, for example `{ partialIntervalMs: 500, outputIntervalMs: 500 }`.
288
304
 
289
305
  ## Busy Conversations
290
306
 
@@ -392,7 +408,7 @@ const other = await harness.createConversation({ ownership: { kind: "ownerless"
392
408
  const fork = await root.fork(entryId, { ownership: { kind: "ownerless" } }, context);
393
409
  ```
394
410
 
395
- A fork sees its parent's entries up to `entryId` and continues independently. It keeps the parent's agent as of that entry. Both take `agent` and `init`, applied in the creating commit.
411
+ A fork sees its parent's entries up to `entryId` and continues independently. It keeps the parent's agent as of that entry but receives a fresh provider session identity. Both take `agent` and `init`, applied in the creating commit.
396
412
 
397
413
  ## Abort and Subagents
398
414
 
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Decoding that matches `new TextDecoder().decode(bytes)` of the whole input when the input arrives in pieces. Node's
3
+ * streaming decoder with BOM handling can drop a U+FEFF that follows a chunk boundary, not only a leading byte-order
4
+ * mark, so these decoders turn BOM handling off and drop a leading mark themselves.
5
+ */
6
+ /** A streaming decoder for a byte range; callers that start at the beginning of a file skip a leading mark themselves. */
7
+ export declare function rangeDecoder(): InstanceType<typeof TextDecoder>;
8
+ /** Whether decoding the whole input drops its first three bytes as a byte-order mark. */
9
+ export declare function startsWithBom(firstBytes: Uint8Array): boolean;
10
+ /** Decodes one stream chunk by chunk exactly like decoding all of it at once. */
11
+ export declare class StreamDecoder {
12
+ #private;
13
+ /** Text for `bytes`, holding back an incomplete character; without `bytes`, the end of the stream. */
14
+ decode(bytes?: Uint8Array): string;
15
+ }
16
+ //# sourceMappingURL=decode.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decode.d.ts","sourceRoot":"","sources":["../../src/env/decode.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,0HAA0H;AAC1H,wBAAgB,YAAY,IAAI,YAAY,CAAC,OAAO,WAAW,CAAC,CAE/D;AAED,yFAAyF;AACzF,wBAAgB,aAAa,CAAC,UAAU,EAAE,UAAU,GAAG,OAAO,CAE7D;AAED,iFAAiF;AACjF,qBAAa,aAAa;;IAIzB,sGAAsG;IACtG,MAAM,CAAC,KAAK,CAAC,EAAE,UAAU,GAAG,MAAM,CAMjC;CACD"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Decoding that matches `new TextDecoder().decode(bytes)` of the whole input when the input arrives in pieces. Node's
3
+ * streaming decoder with BOM handling can drop a U+FEFF that follows a chunk boundary, not only a leading byte-order
4
+ * mark, so these decoders turn BOM handling off and drop a leading mark themselves.
5
+ */
6
+ /** A streaming decoder for a byte range; callers that start at the beginning of a file skip a leading mark themselves. */
7
+ export function rangeDecoder() {
8
+ return new TextDecoder("utf-8", { ignoreBOM: true });
9
+ }
10
+ /** Whether decoding the whole input drops its first three bytes as a byte-order mark. */
11
+ export function startsWithBom(firstBytes) {
12
+ return firstBytes[0] === 0xef && firstBytes[1] === 0xbb && firstBytes[2] === 0xbf;
13
+ }
14
+ /** Decodes one stream chunk by chunk exactly like decoding all of it at once. */
15
+ export class StreamDecoder {
16
+ #decoder = rangeDecoder();
17
+ #started = false;
18
+ /** Text for `bytes`, holding back an incomplete character; without `bytes`, the end of the stream. */
19
+ decode(bytes) {
20
+ const text = bytes === undefined ? this.#decoder.decode() : this.#decoder.decode(bytes, { stream: true });
21
+ if (this.#started || text === "")
22
+ return text;
23
+ this.#started = true;
24
+ // U+FEFF encodes only as EF BB BF, so a leading U+FEFF is exactly a byte-order mark at the stream's start.
25
+ return text.startsWith("\ufeff") ? text.slice(1) : text;
26
+ }
27
+ }
28
+ //# sourceMappingURL=decode.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decode.js","sourceRoot":"","sources":["../../src/env/decode.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,0HAA0H;AAC1H,MAAM,UAAU,YAAY;IAC3B,OAAO,IAAI,WAAW,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;AACtD,CAAC;AAED,yFAAyF;AACzF,MAAM,UAAU,aAAa,CAAC,UAAsB;IACnD,OAAO,UAAU,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,UAAU,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,UAAU,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC;AACnF,CAAC;AAED,iFAAiF;AACjF,MAAM,OAAO,aAAa;IAChB,QAAQ,GAAG,YAAY,EAAE,CAAC;IACnC,QAAQ,GAAG,KAAK,CAAC;IAEjB,sGAAsG;IACtG,MAAM,CAAC,KAAkB;QACxB,MAAM,IAAI,GAAG,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;QAC1G,IAAI,IAAI,CAAC,QAAQ,IAAI,IAAI,KAAK,EAAE;YAAE,OAAO,IAAI,CAAC;QAC9C,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QACrB,2GAA2G;QAC3G,OAAO,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACzD,CAAC;CACD","sourcesContent":["/**\n * Decoding that matches `new TextDecoder().decode(bytes)` of the whole input when the input arrives in pieces. Node's\n * streaming decoder with BOM handling can drop a U+FEFF that follows a chunk boundary, not only a leading byte-order\n * mark, so these decoders turn BOM handling off and drop a leading mark themselves.\n */\n\n/** A streaming decoder for a byte range; callers that start at the beginning of a file skip a leading mark themselves. */\nexport function rangeDecoder(): InstanceType<typeof TextDecoder> {\n\treturn new TextDecoder(\"utf-8\", { ignoreBOM: true });\n}\n\n/** Whether decoding the whole input drops its first three bytes as a byte-order mark. */\nexport function startsWithBom(firstBytes: Uint8Array): boolean {\n\treturn firstBytes[0] === 0xef && firstBytes[1] === 0xbb && firstBytes[2] === 0xbf;\n}\n\n/** Decodes one stream chunk by chunk exactly like decoding all of it at once. */\nexport class StreamDecoder {\n\treadonly #decoder = rangeDecoder();\n\t#started = false;\n\n\t/** Text for `bytes`, holding back an incomplete character; without `bytes`, the end of the stream. */\n\tdecode(bytes?: Uint8Array): string {\n\t\tconst text = bytes === undefined ? this.#decoder.decode() : this.#decoder.decode(bytes, { stream: true });\n\t\tif (this.#started || text === \"\") return text;\n\t\tthis.#started = true;\n\t\t// U+FEFF encodes only as EF BB BF, so a leading U+FEFF is exactly a byte-order mark at the stream's start.\n\t\treturn text.startsWith(\"\\ufeff\") ? text.slice(1) : text;\n\t}\n}\n"]}
@@ -41,6 +41,90 @@ export interface TextLineReader {
41
41
  readLine(context: Context): Promise<Result<TextLine | undefined, FileError>>;
42
42
  close(context: Context): Promise<void>;
43
43
  }
44
+ /** Positional reads from one opened regular file; all calls see the same file even if its path is renamed. */
45
+ export interface BinaryReader {
46
+ /** Metadata of the opened file, not of whatever its path names now. */
47
+ info(context: Context): Promise<Result<FileInfo, FileError>>;
48
+ /** Up to `length` bytes at `offset`; fewer only at end of file. */
49
+ read(offset: number, length: number, context: Context): Promise<Result<Uint8Array, FileError>>;
50
+ /**
51
+ * One pass over the file that locates lines `[startLine, endLine)` (`endLine` absent: to the end), 0-based, where line
52
+ * `k` starts after the `k`-th newline byte. Decoded sizes are those of the text `new TextDecoder().decode(file)` would
53
+ * produce for that range, so a byte-order mark at the start of the file is not counted.
54
+ */
55
+ scanLines(options: {
56
+ startLine: number;
57
+ endLine?: number;
58
+ }, context: Context): Promise<Result<LineScan, FileError>>;
59
+ close(context: Context): Promise<void>;
60
+ }
61
+ /** A file or directory to watch. It may be missing; creating it is a change. */
62
+ export interface WatchTarget {
63
+ path: string;
64
+ /** Watch everything below a directory, not only its entries. Symbolic links below it are not followed. */
65
+ recursive?: boolean;
66
+ /** Entries below `path` that are neither watched nor reported: names starting with `.`, or these names. */
67
+ exclude?: {
68
+ hidden?: boolean;
69
+ names?: readonly string[];
70
+ };
71
+ }
72
+ /**
73
+ * What changed:
74
+ * - `paths`: something at or below each path may have changed (a directory path covers its whole subtree). Calls may be
75
+ * spurious; a change is never missed while the watcher is healthy.
76
+ * - `overflow`: coverage was uncertain for a while (lost events, reconnect); rescan everything that is watched.
77
+ * - `error`: the watcher stopped, for example because the watched tree grew past the environment's limit; no calls
78
+ * follow.
79
+ */
80
+ export type WatchChange = {
81
+ paths: string[];
82
+ } | {
83
+ overflow: true;
84
+ } | {
85
+ error: FileError;
86
+ };
87
+ export interface FileWatcher {
88
+ /**
89
+ * `native`: changes are reported within about two seconds. `polling`: the environment compares snapshots, because
90
+ * the file system does not report changes reliably (network and FUSE file systems); a change undone between two
91
+ * snapshots can be missed.
92
+ */
93
+ readonly mode: "native" | "polling";
94
+ /** Stop watching; no `onChange` call starts after this resolves. Idempotent. */
95
+ close(context: Context): Promise<void>;
96
+ }
97
+ /** Where lines of a file are, as `BinaryReader.scanLines` found them. */
98
+ export interface LineScan {
99
+ /** Newline bytes in the whole file; it has `newlines + 1` lines. */
100
+ newlines: number;
101
+ /**
102
+ * Byte range of the selected lines: from the start of the first to the end of the last, without the newline that
103
+ * ends it. A selection past the last line is empty at the end of the file.
104
+ */
105
+ start: number;
106
+ end: number;
107
+ /** Where the first selected line ends: its newline, or the end of the file. */
108
+ firstLineEnd: number;
109
+ /** Where the last selected line starts. */
110
+ lastLineStart: number;
111
+ /** UTF-8 byte length of the decoded selection and of its first line. */
112
+ selectedBytes: number;
113
+ firstLineBytes: number;
114
+ }
115
+ /** Pages of one directory's entries. */
116
+ export interface DirReader {
117
+ /**
118
+ * Up to `maxEntries` entries in the order the file system returns them, continuing where the previous call stopped.
119
+ * `done` marks the end; it may come with the last entries or with an empty page. An entry that disappears before its
120
+ * metadata is read is skipped, as are entries of unsupported kinds. After a failed or aborted call, close the reader.
121
+ */
122
+ next(maxEntries: number, context: Context): Promise<Result<{
123
+ entries: FileInfo[];
124
+ done: boolean;
125
+ }, FileError>>;
126
+ close(context: Context): Promise<void>;
127
+ }
44
128
  /** Portable filesystem capability. Operations return failures rather than throwing. */
45
129
  export interface FileSystem {
46
130
  /**
@@ -57,6 +141,14 @@ export interface FileSystem {
57
141
  maxLines?: number;
58
142
  } | undefined, context: Context): Promise<Result<string[], FileError>>;
59
143
  readBinaryFile(path: string, context: Context): Promise<Result<Uint8Array, FileError>>;
144
+ /**
145
+ * Open a regular file for bounded positional reads. A directory fails with `is_directory`, other non-regular files
146
+ * with `invalid`. With `noFollow`, a symbolic link as the final path component fails with `invalid` instead of being
147
+ * followed; earlier components are still resolved.
148
+ */
149
+ openBinaryReader(path: string, options: {
150
+ noFollow?: boolean;
151
+ } | undefined, context: Context): Promise<Result<BinaryReader, FileError>>;
60
152
  writeFile(path: string, content: string | Uint8Array, context: Context): Promise<Result<void, FileError>>;
61
153
  appendFile(path: string, content: string | Uint8Array, context: Context): Promise<Result<void, FileError>>;
62
154
  /** Truncate or extend a file to exactly `size` bytes. */
@@ -66,6 +158,13 @@ export interface FileSystem {
66
158
  renameFile(sourcePath: string, destinationPath: string, context: Context): Promise<Result<void, FileError>>;
67
159
  fileInfo(path: string, context: Context): Promise<Result<FileInfo, FileError>>;
68
160
  listDir(path: string, context: Context): Promise<Result<FileInfo[], FileError>>;
161
+ openDirReader(path: string, context: Context): Promise<Result<DirReader, FileError>>;
162
+ /**
163
+ * Report changes to files and directories, for hosts that load resources from the environment. When the returned
164
+ * watcher exists, coverage is established: a host that watches before it loads cannot miss a change made during the
165
+ * load. See `WatchChange` for what is reported and `FileWatcher.mode` for how reliably.
166
+ */
167
+ watch(targets: readonly WatchTarget[], onChange: (change: WatchChange) => void, context: Context): Promise<Result<FileWatcher, FileError>>;
69
168
  canonicalPath(path: string, context: Context): Promise<Result<string, FileError>>;
70
169
  exists(path: string, context: Context): Promise<Result<boolean, FileError>>;
71
170
  createDir(path: string, options: {
@@ -98,14 +197,67 @@ export interface ShellExecOptions {
98
197
  env?: Record<string, string>;
99
198
  inheritEnv?: boolean;
100
199
  timeout?: number;
101
- /** Every decoded chunk of combined stdout and stderr as it arrives: raw, unbounded, and unthrottled. */
102
- onOutput?: (text: string, context: Context) => void;
200
+ /**
201
+ * Every decoded chunk of stdout and stderr as it arrives, in arrival order, with the stream it came from: raw,
202
+ * unbounded, and unthrottled. Each stream is decoded separately, so a character split across chunks survives.
203
+ */
204
+ onOutput?: (text: string, context: Context, info: ShellOutputInfo) => void;
103
205
  spill?: ShellSpillOptions;
206
+ /**
207
+ * The caller keeps only this tail of the output, so the environment may omit output outside it and report the omission
208
+ * as `info.skipped`. Without it, every chunk is delivered.
209
+ */
210
+ window?: ShellOutputWindow;
211
+ }
212
+ /**
213
+ * The tail of the combined output a caller keeps, and how often it samples it. An environment that transfers output
214
+ * over a slow link uses it to omit what the caller would drop anyway and to send no faster than the caller commits.
215
+ */
216
+ export interface ShellOutputWindow {
217
+ /** UTF-8 bytes of decoded text kept at the end of the output. */
218
+ maxBytes: number;
219
+ /** Lines kept at the end of the output. */
220
+ maxLines: number;
221
+ /** Minimum pause between the caller's samples of the output. */
222
+ minIntervalMs: number;
223
+ /** Each sample also pauses the caller in proportion to its size at this rate. */
224
+ bytesPerSecond: number;
225
+ }
226
+ /**
227
+ * Output an environment omitted, measured on the decoded text `onOutput` would have received: every U+FFFD counts as
228
+ * three bytes, and no sanitizing is applied.
229
+ */
230
+ export interface ShellOutputSkip {
231
+ /** UTF-8 byte length of the omitted text. */
232
+ bytes: number;
233
+ /** Newlines (U+000A) in the omitted text. */
234
+ newlines: number;
235
+ /** Whether the omitted text ends with a newline. */
236
+ endsWithNewline: boolean;
237
+ }
238
+ export interface ShellOutputInfo {
239
+ stream: "stdout" | "stderr";
240
+ /**
241
+ * Output omitted immediately before this chunk, only with `window`. The chunk then holds all output after the
242
+ * omission up to its end, and that is more than the window by at least one byte or one line: more than
243
+ * `window.maxBytes` bytes or more than `window.maxLines` newlines. So the omitted text can never be in the kept tail.
244
+ * The omission and such a chunk may span both streams in arrival order; `stream` then names the chunk's last stream.
245
+ * Callers that need the streams apart do not pass `window`.
246
+ */
247
+ skipped?: ShellOutputSkip;
104
248
  }
105
249
  export interface Shell {
106
- exec(command: string, options: ShellExecOptions | undefined, context: Context): Promise<Result<ShellExecResult, ExecutionError>>;
250
+ /**
251
+ * Run a command. A string runs through the environment's shell. An array runs `command[0]` directly with the rest
252
+ * as its arguments, without a shell, so they reach the program unparsed. Aborting the context or a timeout kills
253
+ * only this command's processes.
254
+ */
255
+ exec(command: string | readonly string[], options: ShellExecOptions | undefined, context: Context): Promise<Result<ShellExecResult, ExecutionError>>;
256
+ /** Kill every command this environment still runs; for its owner's shutdown, never for one request. */
107
257
  cleanup(context: Context): Promise<void>;
108
258
  }
109
259
  export interface ExecutionEnv extends FileSystem, Shell {
110
260
  }
261
+ export { rangeDecoder, StreamDecoder, startsWithBom } from "./decode.ts";
262
+ export { LineScanner } from "./line-scan.ts";
111
263
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/env/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/env/index.ts"],"names":[],"mappings":"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"}
package/dist/env/index.js CHANGED
@@ -44,4 +44,6 @@ export class ExecutionError extends Error {
44
44
  this.code = code;
45
45
  }
46
46
  }
47
+ export { rangeDecoder, StreamDecoder, startsWithBom } from "./decode.js";
48
+ export { LineScanner } from "./line-scan.js";
47
49
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/env/index.ts"],"names":[],"mappings":"AAKA,MAAM,UAAU,EAAE,CAAiB,KAAa;IAC/C,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;AAC5B,CAAC;AAED,MAAM,UAAU,GAAG,CAAiB,KAAa;IAChD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;AAC7B,CAAC;AAED,MAAM,UAAU,UAAU,CAAiB,MAA8B;IACxE,IAAI,CAAC,MAAM,CAAC,EAAE;QAAE,MAAM,MAAM,CAAC,KAAK,CAAC;IACnC,OAAO,MAAM,CAAC,KAAK,CAAC;AACrB,CAAC;AAED,MAAM,UAAU,cAAc,CAAgC,MAA8B;IAC3F,OAAO,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;AAC7C,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,KAAc;IACrC,IAAI,KAAK,YAAY,KAAK;QAAE,OAAO,KAAK,CAAC;IACzC,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,IAAI,KAAK,CAAC,KAAK,CAAC,CAAC;IACvD,IAAI,CAAC;QACJ,OAAO,IAAI,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC;IACzC,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IACjC,CAAC;AACF,CAAC;AAcD,MAAM,OAAO,SAAU,SAAQ,KAAK;IAC5B,IAAI,CAAgB;IACpB,IAAI,CAAU;IAErB,YAAY,IAAmB,EAAE,OAAe,EAAE,IAAa,EAAE,KAAa;QAC7E,KAAK,CAAC,OAAO,EAAE,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;QAC5D,IAAI,CAAC,IAAI,GAAG,WAAW,CAAC;QACxB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IAClB,CAAC;CACD;AAUD,MAAM,OAAO,cAAe,SAAQ,KAAK;IACjC,IAAI,CAAqB;IAChC,2GAA2G;IACpG,SAAS,CAAU;IAE1B,YAAY,IAAwB,EAAE,OAAe,EAAE,KAAa;QACnE,KAAK,CAAC,OAAO,EAAE,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;QAC5D,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;QAC7B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IAClB,CAAC;CACD","sourcesContent":["import type { Context } from \"@earendil-works/chord\";\n\n/** Result of a fallible operation. Expected failures are returned instead of thrown. */\nexport type Result<TValue, TError> = { ok: true; value: TValue } | { ok: false; error: TError };\n\nexport function ok<TValue, TError>(value: TValue): Result<TValue, TError> {\n\treturn { ok: true, value };\n}\n\nexport function err<TValue, TError>(error: TError): Result<TValue, TError> {\n\treturn { ok: false, error };\n}\n\nexport function getOrThrow<TValue, TError>(result: Result<TValue, TError>): TValue {\n\tif (!result.ok) throw result.error;\n\treturn result.value;\n}\n\nexport function getOrUndefined<TValue extends object, TError>(result: Result<TValue, TError>): TValue | undefined {\n\treturn result.ok ? result.value : undefined;\n}\n\nexport function toError(error: unknown): Error {\n\tif (error instanceof Error) return error;\n\tif (typeof error === \"string\") return new Error(error);\n\ttry {\n\t\treturn new Error(JSON.stringify(error));\n\t} catch {\n\t\treturn new Error(String(error));\n\t}\n}\n\nexport type FileKind = \"file\" | \"directory\" | \"symlink\";\n\nexport type FileErrorCode =\n\t| \"aborted\"\n\t| \"not_found\"\n\t| \"permission_denied\"\n\t| \"not_directory\"\n\t| \"is_directory\"\n\t| \"invalid\"\n\t| \"not_supported\"\n\t| \"unknown\";\n\nexport class FileError extends Error {\n\tpublic code: FileErrorCode;\n\tpublic path?: string;\n\n\tconstructor(code: FileErrorCode, message: string, path?: string, cause?: Error) {\n\t\tsuper(message, cause === undefined ? undefined : { cause });\n\t\tthis.name = \"FileError\";\n\t\tthis.code = code;\n\t\tthis.path = path;\n\t}\n}\n\nexport type ExecutionErrorCode =\n\t| \"aborted\"\n\t| \"timeout\"\n\t| \"shell_unavailable\"\n\t| \"spawn_error\"\n\t| \"callback_error\"\n\t| \"unknown\";\n\nexport class ExecutionError extends Error {\n\tpublic code: ExecutionErrorCode;\n\t/** Spill file of a command that timed out or was aborted after its output crossed the spill thresholds. */\n\tpublic spillPath?: string;\n\n\tconstructor(code: ExecutionErrorCode, message: string, cause?: Error) {\n\t\tsuper(message, cause === undefined ? undefined : { cause });\n\t\tthis.name = \"ExecutionError\";\n\t\tthis.code = code;\n\t}\n}\n\nexport interface FileInfo {\n\tname: string;\n\tpath: string;\n\tkind: FileKind;\n\tsize: number;\n\tmtimeMs: number;\n}\n\nexport interface TextLine {\n\ttext: string;\n\tterminated: boolean;\n}\n\nexport interface TextLineReader {\n\treadLine(context: Context): Promise<Result<TextLine | undefined, FileError>>;\n\tclose(context: Context): Promise<void>;\n}\n\n/** Portable filesystem capability. Operations return failures rather than throwing. */\nexport interface FileSystem {\n\t/**\n\t * The file namespace: equal ids see the same files at the same paths, whatever their `cwd`. Every local Node\n\t * environment shares one id; each container or remote host has its own.\n\t */\n\treadonly id: string;\n\tcwd: string;\n\tabsolutePath(path: string, context: Context): Promise<Result<string, FileError>>;\n\tjoinPath(parts: string[], context: Context): Promise<Result<string, FileError>>;\n\treadTextFile(path: string, context: Context): Promise<Result<string, FileError>>;\n\topenTextLineReader(path: string, context: Context): Promise<Result<TextLineReader, FileError>>;\n\treadTextLines(\n\t\tpath: string,\n\t\toptions: { maxLines?: number } | undefined,\n\t\tcontext: Context,\n\t): Promise<Result<string[], FileError>>;\n\treadBinaryFile(path: string, context: Context): Promise<Result<Uint8Array, FileError>>;\n\twriteFile(path: string, content: string | Uint8Array, context: Context): Promise<Result<void, FileError>>;\n\tappendFile(path: string, content: string | Uint8Array, context: Context): Promise<Result<void, FileError>>;\n\t/** Truncate or extend a file to exactly `size` bytes. */\n\ttruncateFile(path: string, size: number, context: Context): Promise<Result<void, FileError>>;\n\t/** Flush file contents and metadata needed to retrieve them from an open file handle. */\n\tflushFile(path: string, context: Context): Promise<Result<void, FileError>>;\n\trenameFile(sourcePath: string, destinationPath: string, context: Context): Promise<Result<void, FileError>>;\n\tfileInfo(path: string, context: Context): Promise<Result<FileInfo, FileError>>;\n\tlistDir(path: string, context: Context): Promise<Result<FileInfo[], FileError>>;\n\tcanonicalPath(path: string, context: Context): Promise<Result<string, FileError>>;\n\texists(path: string, context: Context): Promise<Result<boolean, FileError>>;\n\tcreateDir(\n\t\tpath: string,\n\t\toptions: { recursive?: boolean } | undefined,\n\t\tcontext: Context,\n\t): Promise<Result<void, FileError>>;\n\tremove(\n\t\tpath: string,\n\t\toptions: { recursive?: boolean; force?: boolean } | undefined,\n\t\tcontext: Context,\n\t): Promise<Result<void, FileError>>;\n\tcreateTempDir(prefix: string | undefined, context: Context): Promise<Result<string, FileError>>;\n\tcreateTempFile(\n\t\toptions: { prefix?: string; suffix?: string } | undefined,\n\t\tcontext: Context,\n\t): Promise<Result<string, FileError>>;\n\tcleanup(context: Context): Promise<void>;\n}\n\n/** Spill the complete output to a temporary file once it exceeds either threshold. */\nexport interface ShellSpillOptions {\n\tafterBytes: number;\n\t/** Complete or partial lines. */\n\tafterLines: number;\n}\n\nexport interface ShellExecResult {\n\texitCode: number;\n\t/** Temporary file holding the complete raw output, when the spill thresholds were exceeded. */\n\tspillPath?: string;\n}\n\nexport interface ShellExecOptions {\n\tcwd?: string;\n\tenv?: Record<string, string>;\n\tinheritEnv?: boolean;\n\ttimeout?: number;\n\t/** Every decoded chunk of combined stdout and stderr as it arrives: raw, unbounded, and unthrottled. */\n\tonOutput?: (text: string, context: Context) => void;\n\tspill?: ShellSpillOptions;\n}\n\nexport interface Shell {\n\texec(\n\t\tcommand: string,\n\t\toptions: ShellExecOptions | undefined,\n\t\tcontext: Context,\n\t): Promise<Result<ShellExecResult, ExecutionError>>;\n\tcleanup(context: Context): Promise<void>;\n}\n\nexport interface ExecutionEnv extends FileSystem, Shell {}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/env/index.ts"],"names":[],"mappings":"AAKA,MAAM,UAAU,EAAE,CAAiB,KAAa;IAC/C,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;AAC5B,CAAC;AAED,MAAM,UAAU,GAAG,CAAiB,KAAa;IAChD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;AAC7B,CAAC;AAED,MAAM,UAAU,UAAU,CAAiB,MAA8B;IACxE,IAAI,CAAC,MAAM,CAAC,EAAE;QAAE,MAAM,MAAM,CAAC,KAAK,CAAC;IACnC,OAAO,MAAM,CAAC,KAAK,CAAC;AACrB,CAAC;AAED,MAAM,UAAU,cAAc,CAAgC,MAA8B;IAC3F,OAAO,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;AAC7C,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,KAAc;IACrC,IAAI,KAAK,YAAY,KAAK;QAAE,OAAO,KAAK,CAAC;IACzC,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,IAAI,KAAK,CAAC,KAAK,CAAC,CAAC;IACvD,IAAI,CAAC;QACJ,OAAO,IAAI,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC;IACzC,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IACjC,CAAC;AACF,CAAC;AAcD,MAAM,OAAO,SAAU,SAAQ,KAAK;IAC5B,IAAI,CAAgB;IACpB,IAAI,CAAU;IAErB,YAAY,IAAmB,EAAE,OAAe,EAAE,IAAa,EAAE,KAAa;QAC7E,KAAK,CAAC,OAAO,EAAE,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;QAC5D,IAAI,CAAC,IAAI,GAAG,WAAW,CAAC;QACxB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IAClB,CAAC;CACD;AAUD,MAAM,OAAO,cAAe,SAAQ,KAAK;IACjC,IAAI,CAAqB;IAChC,2GAA2G;IACpG,SAAS,CAAU;IAE1B,YAAY,IAAwB,EAAE,OAAe,EAAE,KAAa;QACnE,KAAK,CAAC,OAAO,EAAE,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;QAC5D,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;QAC7B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IAClB,CAAC;CACD;AA2PD,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACzE,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC","sourcesContent":["import type { Context } from \"@earendil-works/chord\";\n\n/** Result of a fallible operation. Expected failures are returned instead of thrown. */\nexport type Result<TValue, TError> = { ok: true; value: TValue } | { ok: false; error: TError };\n\nexport function ok<TValue, TError>(value: TValue): Result<TValue, TError> {\n\treturn { ok: true, value };\n}\n\nexport function err<TValue, TError>(error: TError): Result<TValue, TError> {\n\treturn { ok: false, error };\n}\n\nexport function getOrThrow<TValue, TError>(result: Result<TValue, TError>): TValue {\n\tif (!result.ok) throw result.error;\n\treturn result.value;\n}\n\nexport function getOrUndefined<TValue extends object, TError>(result: Result<TValue, TError>): TValue | undefined {\n\treturn result.ok ? result.value : undefined;\n}\n\nexport function toError(error: unknown): Error {\n\tif (error instanceof Error) return error;\n\tif (typeof error === \"string\") return new Error(error);\n\ttry {\n\t\treturn new Error(JSON.stringify(error));\n\t} catch {\n\t\treturn new Error(String(error));\n\t}\n}\n\nexport type FileKind = \"file\" | \"directory\" | \"symlink\";\n\nexport type FileErrorCode =\n\t| \"aborted\"\n\t| \"not_found\"\n\t| \"permission_denied\"\n\t| \"not_directory\"\n\t| \"is_directory\"\n\t| \"invalid\"\n\t| \"not_supported\"\n\t| \"unknown\";\n\nexport class FileError extends Error {\n\tpublic code: FileErrorCode;\n\tpublic path?: string;\n\n\tconstructor(code: FileErrorCode, message: string, path?: string, cause?: Error) {\n\t\tsuper(message, cause === undefined ? undefined : { cause });\n\t\tthis.name = \"FileError\";\n\t\tthis.code = code;\n\t\tthis.path = path;\n\t}\n}\n\nexport type ExecutionErrorCode =\n\t| \"aborted\"\n\t| \"timeout\"\n\t| \"shell_unavailable\"\n\t| \"spawn_error\"\n\t| \"callback_error\"\n\t| \"unknown\";\n\nexport class ExecutionError extends Error {\n\tpublic code: ExecutionErrorCode;\n\t/** Spill file of a command that timed out or was aborted after its output crossed the spill thresholds. */\n\tpublic spillPath?: string;\n\n\tconstructor(code: ExecutionErrorCode, message: string, cause?: Error) {\n\t\tsuper(message, cause === undefined ? undefined : { cause });\n\t\tthis.name = \"ExecutionError\";\n\t\tthis.code = code;\n\t}\n}\n\nexport interface FileInfo {\n\tname: string;\n\tpath: string;\n\tkind: FileKind;\n\tsize: number;\n\tmtimeMs: number;\n}\n\nexport interface TextLine {\n\ttext: string;\n\tterminated: boolean;\n}\n\nexport interface TextLineReader {\n\treadLine(context: Context): Promise<Result<TextLine | undefined, FileError>>;\n\tclose(context: Context): Promise<void>;\n}\n\n/** Positional reads from one opened regular file; all calls see the same file even if its path is renamed. */\nexport interface BinaryReader {\n\t/** Metadata of the opened file, not of whatever its path names now. */\n\tinfo(context: Context): Promise<Result<FileInfo, FileError>>;\n\t/** Up to `length` bytes at `offset`; fewer only at end of file. */\n\tread(offset: number, length: number, context: Context): Promise<Result<Uint8Array, FileError>>;\n\t/**\n\t * One pass over the file that locates lines `[startLine, endLine)` (`endLine` absent: to the end), 0-based, where line\n\t * `k` starts after the `k`-th newline byte. Decoded sizes are those of the text `new TextDecoder().decode(file)` would\n\t * produce for that range, so a byte-order mark at the start of the file is not counted.\n\t */\n\tscanLines(options: { startLine: number; endLine?: number }, context: Context): Promise<Result<LineScan, FileError>>;\n\tclose(context: Context): Promise<void>;\n}\n\n/** A file or directory to watch. It may be missing; creating it is a change. */\nexport interface WatchTarget {\n\tpath: string;\n\t/** Watch everything below a directory, not only its entries. Symbolic links below it are not followed. */\n\trecursive?: boolean;\n\t/** Entries below `path` that are neither watched nor reported: names starting with `.`, or these names. */\n\texclude?: { hidden?: boolean; names?: readonly string[] };\n}\n\n/**\n * What changed:\n * - `paths`: something at or below each path may have changed (a directory path covers its whole subtree). Calls may be\n * spurious; a change is never missed while the watcher is healthy.\n * - `overflow`: coverage was uncertain for a while (lost events, reconnect); rescan everything that is watched.\n * - `error`: the watcher stopped, for example because the watched tree grew past the environment's limit; no calls\n * follow.\n */\nexport type WatchChange = { paths: string[] } | { overflow: true } | { error: FileError };\n\nexport interface FileWatcher {\n\t/**\n\t * `native`: changes are reported within about two seconds. `polling`: the environment compares snapshots, because\n\t * the file system does not report changes reliably (network and FUSE file systems); a change undone between two\n\t * snapshots can be missed.\n\t */\n\treadonly mode: \"native\" | \"polling\";\n\t/** Stop watching; no `onChange` call starts after this resolves. Idempotent. */\n\tclose(context: Context): Promise<void>;\n}\n\n/** Where lines of a file are, as `BinaryReader.scanLines` found them. */\nexport interface LineScan {\n\t/** Newline bytes in the whole file; it has `newlines + 1` lines. */\n\tnewlines: number;\n\t/**\n\t * Byte range of the selected lines: from the start of the first to the end of the last, without the newline that\n\t * ends it. A selection past the last line is empty at the end of the file.\n\t */\n\tstart: number;\n\tend: number;\n\t/** Where the first selected line ends: its newline, or the end of the file. */\n\tfirstLineEnd: number;\n\t/** Where the last selected line starts. */\n\tlastLineStart: number;\n\t/** UTF-8 byte length of the decoded selection and of its first line. */\n\tselectedBytes: number;\n\tfirstLineBytes: number;\n}\n\n/** Pages of one directory's entries. */\nexport interface DirReader {\n\t/**\n\t * Up to `maxEntries` entries in the order the file system returns them, continuing where the previous call stopped.\n\t * `done` marks the end; it may come with the last entries or with an empty page. An entry that disappears before its\n\t * metadata is read is skipped, as are entries of unsupported kinds. After a failed or aborted call, close the reader.\n\t */\n\tnext(maxEntries: number, context: Context): Promise<Result<{ entries: FileInfo[]; done: boolean }, FileError>>;\n\tclose(context: Context): Promise<void>;\n}\n\n/** Portable filesystem capability. Operations return failures rather than throwing. */\nexport interface FileSystem {\n\t/**\n\t * The file namespace: equal ids see the same files at the same paths, whatever their `cwd`. Every local Node\n\t * environment shares one id; each container or remote host has its own.\n\t */\n\treadonly id: string;\n\tcwd: string;\n\tabsolutePath(path: string, context: Context): Promise<Result<string, FileError>>;\n\tjoinPath(parts: string[], context: Context): Promise<Result<string, FileError>>;\n\treadTextFile(path: string, context: Context): Promise<Result<string, FileError>>;\n\topenTextLineReader(path: string, context: Context): Promise<Result<TextLineReader, FileError>>;\n\treadTextLines(\n\t\tpath: string,\n\t\toptions: { maxLines?: number } | undefined,\n\t\tcontext: Context,\n\t): Promise<Result<string[], FileError>>;\n\treadBinaryFile(path: string, context: Context): Promise<Result<Uint8Array, FileError>>;\n\t/**\n\t * Open a regular file for bounded positional reads. A directory fails with `is_directory`, other non-regular files\n\t * with `invalid`. With `noFollow`, a symbolic link as the final path component fails with `invalid` instead of being\n\t * followed; earlier components are still resolved.\n\t */\n\topenBinaryReader(\n\t\tpath: string,\n\t\toptions: { noFollow?: boolean } | undefined,\n\t\tcontext: Context,\n\t): Promise<Result<BinaryReader, FileError>>;\n\twriteFile(path: string, content: string | Uint8Array, context: Context): Promise<Result<void, FileError>>;\n\tappendFile(path: string, content: string | Uint8Array, context: Context): Promise<Result<void, FileError>>;\n\t/** Truncate or extend a file to exactly `size` bytes. */\n\ttruncateFile(path: string, size: number, context: Context): Promise<Result<void, FileError>>;\n\t/** Flush file contents and metadata needed to retrieve them from an open file handle. */\n\tflushFile(path: string, context: Context): Promise<Result<void, FileError>>;\n\trenameFile(sourcePath: string, destinationPath: string, context: Context): Promise<Result<void, FileError>>;\n\tfileInfo(path: string, context: Context): Promise<Result<FileInfo, FileError>>;\n\tlistDir(path: string, context: Context): Promise<Result<FileInfo[], FileError>>;\n\topenDirReader(path: string, context: Context): Promise<Result<DirReader, FileError>>;\n\t/**\n\t * Report changes to files and directories, for hosts that load resources from the environment. When the returned\n\t * watcher exists, coverage is established: a host that watches before it loads cannot miss a change made during the\n\t * load. See `WatchChange` for what is reported and `FileWatcher.mode` for how reliably.\n\t */\n\twatch(\n\t\ttargets: readonly WatchTarget[],\n\t\tonChange: (change: WatchChange) => void,\n\t\tcontext: Context,\n\t): Promise<Result<FileWatcher, FileError>>;\n\tcanonicalPath(path: string, context: Context): Promise<Result<string, FileError>>;\n\texists(path: string, context: Context): Promise<Result<boolean, FileError>>;\n\tcreateDir(\n\t\tpath: string,\n\t\toptions: { recursive?: boolean } | undefined,\n\t\tcontext: Context,\n\t): Promise<Result<void, FileError>>;\n\tremove(\n\t\tpath: string,\n\t\toptions: { recursive?: boolean; force?: boolean } | undefined,\n\t\tcontext: Context,\n\t): Promise<Result<void, FileError>>;\n\tcreateTempDir(prefix: string | undefined, context: Context): Promise<Result<string, FileError>>;\n\tcreateTempFile(\n\t\toptions: { prefix?: string; suffix?: string } | undefined,\n\t\tcontext: Context,\n\t): Promise<Result<string, FileError>>;\n\tcleanup(context: Context): Promise<void>;\n}\n\n/** Spill the complete output to a temporary file once it exceeds either threshold. */\nexport interface ShellSpillOptions {\n\tafterBytes: number;\n\t/** Complete or partial lines. */\n\tafterLines: number;\n}\n\nexport interface ShellExecResult {\n\texitCode: number;\n\t/** Temporary file holding the complete raw output, when the spill thresholds were exceeded. */\n\tspillPath?: string;\n}\n\nexport interface ShellExecOptions {\n\tcwd?: string;\n\tenv?: Record<string, string>;\n\tinheritEnv?: boolean;\n\ttimeout?: number;\n\t/**\n\t * Every decoded chunk of stdout and stderr as it arrives, in arrival order, with the stream it came from: raw,\n\t * unbounded, and unthrottled. Each stream is decoded separately, so a character split across chunks survives.\n\t */\n\tonOutput?: (text: string, context: Context, info: ShellOutputInfo) => void;\n\tspill?: ShellSpillOptions;\n\t/**\n\t * The caller keeps only this tail of the output, so the environment may omit output outside it and report the omission\n\t * as `info.skipped`. Without it, every chunk is delivered.\n\t */\n\twindow?: ShellOutputWindow;\n}\n\n/**\n * The tail of the combined output a caller keeps, and how often it samples it. An environment that transfers output\n * over a slow link uses it to omit what the caller would drop anyway and to send no faster than the caller commits.\n */\nexport interface ShellOutputWindow {\n\t/** UTF-8 bytes of decoded text kept at the end of the output. */\n\tmaxBytes: number;\n\t/** Lines kept at the end of the output. */\n\tmaxLines: number;\n\t/** Minimum pause between the caller's samples of the output. */\n\tminIntervalMs: number;\n\t/** Each sample also pauses the caller in proportion to its size at this rate. */\n\tbytesPerSecond: number;\n}\n\n/**\n * Output an environment omitted, measured on the decoded text `onOutput` would have received: every U+FFFD counts as\n * three bytes, and no sanitizing is applied.\n */\nexport interface ShellOutputSkip {\n\t/** UTF-8 byte length of the omitted text. */\n\tbytes: number;\n\t/** Newlines (U+000A) in the omitted text. */\n\tnewlines: number;\n\t/** Whether the omitted text ends with a newline. */\n\tendsWithNewline: boolean;\n}\n\nexport interface ShellOutputInfo {\n\tstream: \"stdout\" | \"stderr\";\n\t/**\n\t * Output omitted immediately before this chunk, only with `window`. The chunk then holds all output after the\n\t * omission up to its end, and that is more than the window by at least one byte or one line: more than\n\t * `window.maxBytes` bytes or more than `window.maxLines` newlines. So the omitted text can never be in the kept tail.\n\t * The omission and such a chunk may span both streams in arrival order; `stream` then names the chunk's last stream.\n\t * Callers that need the streams apart do not pass `window`.\n\t */\n\tskipped?: ShellOutputSkip;\n}\n\nexport interface Shell {\n\t/**\n\t * Run a command. A string runs through the environment's shell. An array runs `command[0]` directly with the rest\n\t * as its arguments, without a shell, so they reach the program unparsed. Aborting the context or a timeout kills\n\t * only this command's processes.\n\t */\n\texec(\n\t\tcommand: string | readonly string[],\n\t\toptions: ShellExecOptions | undefined,\n\t\tcontext: Context,\n\t): Promise<Result<ShellExecResult, ExecutionError>>;\n\t/** Kill every command this environment still runs; for its owner's shutdown, never for one request. */\n\tcleanup(context: Context): Promise<void>;\n}\n\nexport interface ExecutionEnv extends FileSystem, Shell {}\n\nexport { rangeDecoder, StreamDecoder, startsWithBom } from \"./decode.ts\";\nexport { LineScanner } from \"./line-scan.ts\";\n"]}
@@ -0,0 +1,13 @@
1
+ import type { LineScan } from "./index.ts";
2
+ /**
3
+ * Computes a `LineScan` from a file's bytes fed in order, so an environment can scan any size in bounded memory. Decoded
4
+ * sizes use the same streaming WHATWG decoder as decoding the whole file at once.
5
+ */
6
+ export declare class LineScanner {
7
+ #private;
8
+ /** `startLine` and `endLine` must be non-negative integers with `endLine > startLine`; `endLine` absent: to the end. */
9
+ constructor(startLine: number, endLine?: number);
10
+ push(chunk: Uint8Array): void;
11
+ finish(): LineScan;
12
+ }
13
+ //# sourceMappingURL=line-scan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"line-scan.d.ts","sourceRoot":"","sources":["../../src/env/line-scan.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAU3C;;;GAGG;AACH,qBAAa,WAAW;;IAkBvB,wHAAwH;IACxH,YAAY,SAAS,EAAE,MAAM,EAAE,OAAO,SAA2B,EAShE;IAED,IAAI,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAS5B;IA8BD,MAAM,IAAI,QAAQ,CA0BjB;CAkCD"}
@@ -0,0 +1,147 @@
1
+ import { rangeDecoder, startsWithBom } from "./decode.js";
2
+ const NEWLINE = 0x0a;
3
+ const encoder = new TextEncoder();
4
+ /** UTF-8 byte length of decoded text. */
5
+ function decodedBytes(text) {
6
+ return text === "" ? 0 : encoder.encode(text).length;
7
+ }
8
+ /**
9
+ * Computes a `LineScan` from a file's bytes fed in order, so an environment can scan any size in bounded memory. Decoded
10
+ * sizes use the same streaming WHATWG decoder as decoding the whole file at once.
11
+ */
12
+ export class LineScanner {
13
+ #startLine;
14
+ #endLine;
15
+ #position = 0;
16
+ #newlines = 0;
17
+ #lineStart = 0;
18
+ #start;
19
+ #end;
20
+ #firstLineEnd;
21
+ #lastLineStart;
22
+ #selectedBytes = 0;
23
+ #firstLineBytes = 0;
24
+ #selection;
25
+ #firstLine;
26
+ /** The first bytes, held until it is known whether they are a byte-order mark. */
27
+ #head = [];
28
+ #bom = false;
29
+ /** `startLine` and `endLine` must be non-negative integers with `endLine > startLine`; `endLine` absent: to the end. */
30
+ constructor(startLine, endLine = Number.POSITIVE_INFINITY) {
31
+ if (!Number.isSafeInteger(startLine) || startLine < 0 || !(endLine > startLine)) {
32
+ throw new RangeError("Invalid line range");
33
+ }
34
+ if (endLine !== Number.POSITIVE_INFINITY && !Number.isSafeInteger(endLine))
35
+ throw new RangeError("Invalid line range");
36
+ this.#startLine = startLine;
37
+ this.#endLine = endLine;
38
+ if (startLine === 0)
39
+ this.#begin(0);
40
+ }
41
+ push(chunk) {
42
+ if (this.#head !== undefined) {
43
+ const take = Math.min(3 - this.#head.length, chunk.length);
44
+ this.#head.push(...chunk.subarray(0, take));
45
+ if (this.#head.length < 3)
46
+ return;
47
+ this.#releaseHead();
48
+ chunk = chunk.subarray(take);
49
+ }
50
+ this.#process(chunk);
51
+ }
52
+ #releaseHead() {
53
+ const head = Uint8Array.from(this.#head ?? []);
54
+ this.#head = undefined;
55
+ this.#bom = startsWithBom(head);
56
+ this.#process(head);
57
+ }
58
+ #process(chunk) {
59
+ const base = this.#position;
60
+ let from = 0;
61
+ for (let index = chunk.indexOf(NEWLINE); index !== -1; index = chunk.indexOf(NEWLINE, index + 1)) {
62
+ // The newline ends line `this.#newlines`. It belongs to the selection between selected lines only.
63
+ this.#feed(chunk, base, from, index);
64
+ const line = this.#newlines;
65
+ const position = base + index;
66
+ if (line === this.#startLine)
67
+ this.#endFirstLine(position);
68
+ if (line === this.#endLine - 1)
69
+ this.#endSelection(position);
70
+ this.#feed(chunk, base, index, index + 1);
71
+ from = index + 1;
72
+ this.#newlines++;
73
+ this.#lineStart = position + 1;
74
+ if (this.#newlines === this.#startLine)
75
+ this.#begin(this.#lineStart);
76
+ if (this.#newlines === this.#endLine - 1)
77
+ this.#lastLineStart = this.#lineStart;
78
+ }
79
+ this.#feed(chunk, base, from, chunk.length);
80
+ this.#position += chunk.length;
81
+ }
82
+ finish() {
83
+ if (this.#head !== undefined)
84
+ this.#releaseHead();
85
+ const size = this.#position;
86
+ if (this.#start === undefined) {
87
+ return {
88
+ newlines: this.#newlines,
89
+ start: size,
90
+ end: size,
91
+ firstLineEnd: size,
92
+ lastLineStart: size,
93
+ selectedBytes: 0,
94
+ firstLineBytes: 0,
95
+ };
96
+ }
97
+ if (this.#firstLineEnd === undefined)
98
+ this.#endFirstLine(size);
99
+ if (this.#end === undefined)
100
+ this.#endSelection(size);
101
+ return {
102
+ newlines: this.#newlines,
103
+ start: this.#start,
104
+ end: this.#end ?? size,
105
+ firstLineEnd: this.#firstLineEnd ?? size,
106
+ // A selection that reaches past the last line ends with the last line.
107
+ lastLineStart: this.#lastLineStart ?? this.#lineStart,
108
+ selectedBytes: this.#selectedBytes,
109
+ firstLineBytes: this.#firstLineBytes,
110
+ };
111
+ }
112
+ #begin(start) {
113
+ this.#start = start;
114
+ if (this.#startLine === this.#endLine - 1)
115
+ this.#lastLineStart = start;
116
+ this.#selection = rangeDecoder();
117
+ this.#firstLine = rangeDecoder();
118
+ }
119
+ #endFirstLine(position) {
120
+ this.#firstLineEnd = position;
121
+ if (this.#firstLine !== undefined)
122
+ this.#firstLineBytes += decodedBytes(this.#firstLine.decode());
123
+ this.#firstLine = undefined;
124
+ }
125
+ #endSelection(position) {
126
+ this.#end = position;
127
+ if (this.#selection !== undefined)
128
+ this.#selectedBytes += decodedBytes(this.#selection.decode());
129
+ this.#selection = undefined;
130
+ }
131
+ /** Feed `chunk[from, to)`, which starts at file offset `base`, to the decoders of the ranges still open. */
132
+ #feed(chunk, base, from, to) {
133
+ // Decoding the whole file drops a leading byte-order mark.
134
+ if (this.#bom && base + from < 3)
135
+ from = Math.min(to, 3 - base);
136
+ if (to <= from)
137
+ return;
138
+ const bytes = chunk.subarray(from, to);
139
+ if (this.#selection !== undefined) {
140
+ this.#selectedBytes += decodedBytes(this.#selection.decode(bytes, { stream: true }));
141
+ }
142
+ if (this.#firstLine !== undefined) {
143
+ this.#firstLineBytes += decodedBytes(this.#firstLine.decode(bytes, { stream: true }));
144
+ }
145
+ }
146
+ }
147
+ //# sourceMappingURL=line-scan.js.map