@smthrs/std 1.0.0-rc.5 → 1.0.0-rc.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -39
- package/dist/cjs/Bash.d.ts +10 -0
- package/dist/cjs/Bash.d.ts.map +1 -1
- package/dist/cjs/Bash.js +13 -12
- package/dist/cjs/Bash.js.map +3 -3
- package/dist/cjs/ExaWebSearch.js +1 -1
- package/dist/cjs/ExaWebSearch.js.map +1 -1
- package/dist/cjs/Fetch.d.ts +1 -1
- package/dist/cjs/Fetch.d.ts.map +1 -1
- package/dist/cjs/Fetch.js +1 -1
- package/dist/cjs/Fetch.js.map +2 -2
- package/dist/cjs/HttpPost.d.ts +1 -1
- package/dist/cjs/HttpPost.d.ts.map +1 -1
- package/dist/cjs/HttpPost.js +1 -1
- package/dist/cjs/HttpPost.js.map +2 -2
- package/dist/cjs/Lsp.d.ts +1 -1
- package/dist/cjs/Manifest.d.ts +8 -8
- package/dist/cjs/NodeLanguageServer.js +1 -1
- package/dist/cjs/NodeLanguageServer.js.map +1 -1
- package/dist/cjs/WebFetch.d.ts.map +1 -1
- package/dist/cjs/WebFetch.js +1 -1
- package/dist/cjs/WebFetch.js.map +2 -2
- package/dist/cjs/internal/Exec.d.ts +14 -7
- package/dist/cjs/internal/Exec.d.ts.map +1 -1
- package/dist/cjs/internal/Exec.js +66 -28
- package/dist/cjs/internal/Exec.js.map +2 -2
- package/dist/cjs/internal/Http.d.ts +1 -1
- package/dist/cjs/internal/Http.d.ts.map +1 -1
- package/dist/cjs/internal/Http.js +1 -1
- package/dist/cjs/internal/Http.js.map +2 -2
- package/dist/cjs/internal/HttpNetwork.d.ts +2 -2
- package/dist/cjs/internal/HttpNetwork.d.ts.map +1 -1
- package/dist/cjs/internal/HttpNetwork.js +3 -3
- package/dist/cjs/internal/HttpNetwork.js.map +1 -1
- package/dist/cjs/internal/Text.d.ts +11 -0
- package/dist/cjs/internal/Text.d.ts.map +1 -1
- package/dist/cjs/internal/Text.js +3 -0
- package/dist/cjs/internal/Text.js.map +2 -2
- package/dist/esm/Bash.d.ts +10 -0
- package/dist/esm/Bash.d.ts.map +1 -1
- package/dist/esm/Bash.js +22 -13
- package/dist/esm/Bash.js.map +1 -1
- package/dist/esm/ExaWebSearch.js +1 -1
- package/dist/esm/ExaWebSearch.js.map +1 -1
- package/dist/esm/Fetch.d.ts +1 -1
- package/dist/esm/Fetch.d.ts.map +1 -1
- package/dist/esm/Fetch.js +1 -1
- package/dist/esm/Fetch.js.map +1 -1
- package/dist/esm/HttpPost.d.ts +1 -1
- package/dist/esm/HttpPost.d.ts.map +1 -1
- package/dist/esm/HttpPost.js +1 -1
- package/dist/esm/HttpPost.js.map +1 -1
- package/dist/esm/Lsp.d.ts +1 -1
- package/dist/esm/Manifest.d.ts +8 -8
- package/dist/esm/NodeLanguageServer.js +1 -1
- package/dist/esm/NodeLanguageServer.js.map +1 -1
- package/dist/esm/WebFetch.d.ts.map +1 -1
- package/dist/esm/WebFetch.js +1 -1
- package/dist/esm/WebFetch.js.map +1 -1
- package/dist/esm/internal/Exec.d.ts +14 -7
- package/dist/esm/internal/Exec.d.ts.map +1 -1
- package/dist/esm/internal/Exec.js +87 -22
- package/dist/esm/internal/Exec.js.map +1 -1
- package/dist/esm/internal/Http.d.ts +1 -1
- package/dist/esm/internal/Http.d.ts.map +1 -1
- package/dist/esm/internal/Http.js +1 -1
- package/dist/esm/internal/Http.js.map +1 -1
- package/dist/esm/internal/HttpNetwork.d.ts +2 -2
- package/dist/esm/internal/HttpNetwork.d.ts.map +1 -1
- package/dist/esm/internal/HttpNetwork.js +3 -3
- package/dist/esm/internal/HttpNetwork.js.map +1 -1
- package/dist/esm/internal/Text.d.ts +11 -0
- package/dist/esm/internal/Text.d.ts.map +1 -1
- package/dist/esm/internal/Text.js +11 -0
- package/dist/esm/internal/Text.js.map +1 -1
- package/docs/api.md +1 -1
- package/docs/concepts/limits-and-disclosure.md +8 -5
- package/docs/guides/run-a-shell-command.md +16 -6
- package/docs/installation.md +1 -1
- package/docs/reference/flows.md +2 -2
- package/docs/troubleshooting.md +3 -3
- package/package.json +10 -10
- package/src/Bash.ts +25 -13
- package/src/ExaWebSearch.ts +1 -1
- package/src/Fetch.ts +1 -1
- package/src/HttpPost.ts +1 -1
- package/src/NodeLanguageServer.ts +1 -1
- package/src/WebFetch.ts +1 -1
- package/src/internal/Exec.ts +126 -44
- package/src/internal/Http.ts +1 -1
- package/src/internal/HttpNetwork.ts +3 -3
- package/src/internal/Text.ts +15 -0
package/docs/reference/flows.md
CHANGED
|
@@ -241,9 +241,9 @@ envelope `bash` declares for every call.
|
|
|
241
241
|
| Output | Type | Meaning |
|
|
242
242
|
| ------------------------------------------ | ------------------------------ | ---------------------------------------------------------------------------------- |
|
|
243
243
|
| `exitCode` | number | The command's exit code, including non-zero. |
|
|
244
|
-
| `stdout`, `stderr` | string | Captured streams
|
|
244
|
+
| `stdout`, `stderr` | string | Captured streams; start and end kept when truncated. |
|
|
245
245
|
| `stdoutTruncated`, `stderrTruncated` | boolean | Whether that stream is a fragment. |
|
|
246
|
-
| `stdoutDroppedBytes`, `stderrDroppedBytes` | number | Bytes omitted from the
|
|
246
|
+
| `stdoutDroppedBytes`, `stderrDroppedBytes` | number | Bytes omitted from the middle. |
|
|
247
247
|
| `invalidProbe` | `Probe.InvalidProbe`, optional | Reserved for independently confirmed launch failures; not inferred from exit code. |
|
|
248
248
|
|
|
249
249
|
`Bash.Input` and `Bash.Output` are also exported as TypeScript types alongside
|
package/docs/troubleshooting.md
CHANGED
|
@@ -260,11 +260,11 @@ runs. The server would have run workspace-written code on the host.
|
|
|
260
260
|
absolute path, not a launcher. For typescript-language-server, also pin
|
|
261
261
|
`initializationOptions: { tsserver: { path } }` to a host copy.
|
|
262
262
|
|
|
263
|
-
## A write of captured output replaced a file with a
|
|
263
|
+
## A write of captured output replaced a file with a fragment
|
|
264
264
|
|
|
265
265
|
**What happened.** A truncated `stdout`, `stderr`, or `tail` was written to a
|
|
266
|
-
file. Those fields are fragments: the
|
|
267
|
-
printed,
|
|
266
|
+
file. Those fields are fragments: `bash` keeps the start and the end of what the
|
|
267
|
+
process printed and `test` keeps the tail, never the whole of it.
|
|
268
268
|
|
|
269
269
|
**What to change.** Check `stdoutTruncated`, `stderrTruncated`, or
|
|
270
270
|
`tailTruncated` before writing captured output anywhere.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smthrs/std",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.0.0-rc.
|
|
4
|
+
"version": "1.0.0-rc.7",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"smthrs": {
|
|
7
7
|
"group": "agent"
|
|
@@ -401,21 +401,21 @@
|
|
|
401
401
|
"tag": "next"
|
|
402
402
|
},
|
|
403
403
|
"dependencies": {
|
|
404
|
-
"@smthrs/capability": "1.0.0-rc.
|
|
405
|
-
"@smthrs/control": "1.0.0-rc.
|
|
406
|
-
"@smthrs/core": "1.0.0-rc.
|
|
407
|
-
"@smthrs/kernel": "1.0.0-rc.
|
|
408
|
-
"@smthrs/model": "1.0.0-rc.
|
|
404
|
+
"@smthrs/capability": "1.0.0-rc.7",
|
|
405
|
+
"@smthrs/control": "1.0.0-rc.7",
|
|
406
|
+
"@smthrs/core": "1.0.0-rc.7",
|
|
407
|
+
"@smthrs/kernel": "1.0.0-rc.7",
|
|
408
|
+
"@smthrs/model": "1.0.0-rc.7"
|
|
409
409
|
},
|
|
410
410
|
"devDependencies": {
|
|
411
411
|
"@effect/language-service": "0.87.2",
|
|
412
|
-
"@effect/platform-node": "4.0.
|
|
413
|
-
"@effect/vitest": "4.0.
|
|
412
|
+
"@effect/platform-node": "4.0.4",
|
|
413
|
+
"@effect/vitest": "4.0.4",
|
|
414
414
|
"@eslint/js": "9.39.5",
|
|
415
415
|
"@types/node": "26.4.1",
|
|
416
416
|
"@vitest/coverage-v8": "5.0.0",
|
|
417
417
|
"dprint": "0.57.1",
|
|
418
|
-
"effect": "4.0.
|
|
418
|
+
"effect": "4.0.4",
|
|
419
419
|
"esbuild": "0.28.2",
|
|
420
420
|
"eslint": "9.39.5",
|
|
421
421
|
"eslint-import-resolver-typescript": "4.4.5",
|
|
@@ -428,7 +428,7 @@
|
|
|
428
428
|
"vitest": "5.0.0"
|
|
429
429
|
},
|
|
430
430
|
"peerDependencies": {
|
|
431
|
-
"effect": "4.0.
|
|
431
|
+
"effect": "^4.0.4"
|
|
432
432
|
},
|
|
433
433
|
"scripts": {
|
|
434
434
|
"lint": "eslint src --max-warnings=0 && dprint check",
|
package/src/Bash.ts
CHANGED
|
@@ -27,7 +27,7 @@ import { capability, envelope } from "./internal/Declaration.ts"
|
|
|
27
27
|
import * as Diagnostics from "./internal/Diagnostics.ts"
|
|
28
28
|
import { outsideEnvelope } from "./internal/EnvelopePrecheck.ts"
|
|
29
29
|
import * as Exec from "./internal/Exec.ts"
|
|
30
|
-
import {
|
|
30
|
+
import { elideMiddle as elide, MAX_SHELL_OUTPUT_BYTES } from "./internal/Text.ts"
|
|
31
31
|
import * as Probe from "./Probe.ts"
|
|
32
32
|
import * as StdError from "./StdError.ts"
|
|
33
33
|
import * as TreeFingerprint from "./TreeFingerprint.ts"
|
|
@@ -173,10 +173,12 @@ export type Input = typeof Input.Type
|
|
|
173
173
|
export const Output = Schema.Struct({
|
|
174
174
|
exitCode: Schema.Number.annotate({ description: "Command exit code, including non-zero codes" }),
|
|
175
175
|
stdout: Schema.String.annotate({
|
|
176
|
-
description:
|
|
176
|
+
description:
|
|
177
|
+
"Captured stdout; when stdoutTruncated is true, only its start and end, with one line naming the bytes omitted between them"
|
|
177
178
|
}),
|
|
178
179
|
stderr: Schema.String.annotate({
|
|
179
|
-
description:
|
|
180
|
+
description:
|
|
181
|
+
"Captured stderr; when stderrTruncated is true, only its start and end, with one line naming the bytes omitted between them"
|
|
180
182
|
}),
|
|
181
183
|
stdoutTruncated: Schema.Boolean.annotate({
|
|
182
184
|
description:
|
|
@@ -186,8 +188,8 @@ export const Output = Schema.Struct({
|
|
|
186
188
|
description:
|
|
187
189
|
"Whether stderr exceeded the capture limit, leaving stderr a fragment that must not be written to a file"
|
|
188
190
|
}),
|
|
189
|
-
stdoutDroppedBytes: Schema.Number.annotate({ description: "UTF-8 bytes omitted from the
|
|
190
|
-
stderrDroppedBytes: Schema.Number.annotate({ description: "UTF-8 bytes omitted from the
|
|
191
|
+
stdoutDroppedBytes: Schema.Number.annotate({ description: "UTF-8 bytes omitted from the middle of stdout" }),
|
|
192
|
+
stderrDroppedBytes: Schema.Number.annotate({ description: "UTF-8 bytes omitted from the middle of stderr" }),
|
|
191
193
|
invalidProbe: Schema.optional(
|
|
192
194
|
Probe.InvalidProbe.annotate({
|
|
193
195
|
description: "Reserved for a confirmed launch failure; an exit code alone does not establish one"
|
|
@@ -209,6 +211,17 @@ export const Output = Schema.Struct({
|
|
|
209
211
|
*/
|
|
210
212
|
export type Output = typeof Output.Type
|
|
211
213
|
|
|
214
|
+
/**
|
|
215
|
+
* The text of a stream cut to its start and end: the start, one line naming
|
|
216
|
+
* the bytes omitted between them, and the end. A host that captures output
|
|
217
|
+
* itself, such as a terminal behind {@link inTerminal}, cuts with this so every
|
|
218
|
+
* {@link Output} reads the same.
|
|
219
|
+
*
|
|
220
|
+
* @category rendering
|
|
221
|
+
* @since 1.0.0-rc.5
|
|
222
|
+
*/
|
|
223
|
+
export const elideMiddle: (start: string, droppedBytes: number, end: string) => string = elide
|
|
224
|
+
|
|
212
225
|
/**
|
|
213
226
|
* Static conservative effect envelope for the bash flow.
|
|
214
227
|
*
|
|
@@ -500,6 +513,7 @@ export const run = Effect.fn("Bash.run")(function*(
|
|
|
500
513
|
...(spawned.env === undefined ? {} : { env: spawned.env }),
|
|
501
514
|
timeoutMs: input.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
502
515
|
maxCaptureBytes: MAX_SHELL_OUTPUT_BYTES,
|
|
516
|
+
overflow: "ends",
|
|
503
517
|
...(spawned.args === undefined ? {} : { args: spawned.args }),
|
|
504
518
|
...(spawned.stdin === undefined ? {} : { stdin: spawned.stdin })
|
|
505
519
|
}).pipe(Effect.mapError((error) => Exec.toStdError(spawned.quoted, error)))
|
|
@@ -507,18 +521,16 @@ export const run = Effect.fn("Bash.run")(function*(
|
|
|
507
521
|
// A command on this host may have changed files a language server holds
|
|
508
522
|
// open; a container's files are not the ones it was sent.
|
|
509
523
|
if (!contained) yield* Diagnostics.refresh
|
|
510
|
-
const stdout = truncateBytes(result.stdout, MAX_SHELL_OUTPUT_BYTES, { keep: "tail" })
|
|
511
|
-
const stderr = truncateBytes(result.stderr, MAX_SHELL_OUTPUT_BYTES, { keep: "tail" })
|
|
512
524
|
// Exit codes alone cannot establish whether the shell refused to launch a
|
|
513
525
|
// command or a launched runner chose that status after executing tests.
|
|
514
526
|
return {
|
|
515
527
|
exitCode: result.exitCode,
|
|
516
|
-
stdout: stdout
|
|
517
|
-
stderr: stderr
|
|
518
|
-
stdoutTruncated: result.stdoutDroppedBytes > 0
|
|
519
|
-
stderrTruncated: result.stderrDroppedBytes > 0
|
|
520
|
-
stdoutDroppedBytes: result.stdoutDroppedBytes
|
|
521
|
-
stderrDroppedBytes: result.stderrDroppedBytes
|
|
528
|
+
stdout: result.stdout,
|
|
529
|
+
stderr: result.stderr,
|
|
530
|
+
stdoutTruncated: result.stdoutDroppedBytes > 0,
|
|
531
|
+
stderrTruncated: result.stderrDroppedBytes > 0,
|
|
532
|
+
stdoutDroppedBytes: result.stdoutDroppedBytes,
|
|
533
|
+
stderrDroppedBytes: result.stderrDroppedBytes,
|
|
522
534
|
...(mutated === undefined ? {} : { mutated })
|
|
523
535
|
}
|
|
524
536
|
})
|
package/src/ExaWebSearch.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import * as Credential from "@smthrs/control/Credential"
|
|
12
12
|
import * as HttpClient from "@smthrs/kernel/HttpClient"
|
|
13
13
|
import { Clock, Effect, Layer, Redacted, Schema } from "effect"
|
|
14
|
-
import * as HttpClientRequest from "effect/
|
|
14
|
+
import * as HttpClientRequest from "effect/http/HttpClientRequest"
|
|
15
15
|
import { header, readBounded } from "./internal/Http.ts"
|
|
16
16
|
import * as StdError from "./StdError.ts"
|
|
17
17
|
import * as WebSearch from "./WebSearch.ts"
|
package/src/Fetch.ts
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import * as Effect from "effect/Effect"
|
|
8
|
+
import * as HttpClientRequest from "effect/http/HttpClientRequest"
|
|
8
9
|
import * as Schema from "effect/Schema"
|
|
9
|
-
import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"
|
|
10
10
|
import { capability, envelope } from "./internal/Declaration.ts"
|
|
11
11
|
import { execute, Response, Timeout } from "./internal/Http.ts"
|
|
12
12
|
|
package/src/HttpPost.ts
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import * as Effect from "effect/Effect"
|
|
8
|
+
import * as HttpClientRequest from "effect/http/HttpClientRequest"
|
|
8
9
|
import * as Schema from "effect/Schema"
|
|
9
|
-
import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"
|
|
10
10
|
import { capability, envelope } from "./internal/Declaration.ts"
|
|
11
11
|
import { execute, Response, Timeout } from "./internal/Http.ts"
|
|
12
12
|
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import * as ChildProcessEnvironment from "@smthrs/kernel/ChildProcessEnvironment"
|
|
13
13
|
import * as ChildProcessSpawner from "@smthrs/kernel/ChildProcessSpawner"
|
|
14
14
|
import { Clock, Deferred, Effect, Fiber, Layer, Option, Queue, type Scope, Semaphore, Stream } from "effect"
|
|
15
|
-
import * as ChildProcess from "effect/
|
|
15
|
+
import * as ChildProcess from "effect/process/ChildProcess"
|
|
16
16
|
import { createHash } from "node:crypto"
|
|
17
17
|
import * as NodeFs from "node:fs"
|
|
18
18
|
import * as NodePath from "node:path"
|
package/src/WebFetch.ts
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
import type { GrantStore } from "@smthrs/kernel/GrantStore"
|
|
13
13
|
import * as HttpClient from "@smthrs/kernel/HttpClient"
|
|
14
14
|
import * as Effect from "effect/Effect"
|
|
15
|
+
import * as HttpClientRequest from "effect/http/HttpClientRequest"
|
|
15
16
|
import * as Schema from "effect/Schema"
|
|
16
|
-
import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"
|
|
17
17
|
import { capability, envelope } from "./internal/Declaration.ts"
|
|
18
18
|
import { toMarkdown, toText } from "./internal/Html.ts"
|
|
19
19
|
import { capOutput, header, MAX_RESPONSE_BYTES, readBounded, Timeout, withDeadline } from "./internal/Http.ts"
|
package/src/internal/Exec.ts
CHANGED
|
@@ -17,10 +17,11 @@
|
|
|
17
17
|
import * as ChildProcessEnvironment from "@smthrs/kernel/ChildProcessEnvironment"
|
|
18
18
|
import * as ChildProcessSpawner from "@smthrs/kernel/ChildProcessSpawner"
|
|
19
19
|
import * as Effect from "effect/Effect"
|
|
20
|
+
import * as ChildProcess from "effect/process/ChildProcess"
|
|
20
21
|
import * as Schema from "effect/Schema"
|
|
21
22
|
import * as Stream from "effect/Stream"
|
|
22
|
-
import * as ChildProcess from "effect/unstable/process/ChildProcess"
|
|
23
23
|
import * as StdError from "../StdError.ts"
|
|
24
|
+
import { elideMiddle } from "./Text.ts"
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* Failure codes a buffered execution can report, matching the codes the
|
|
@@ -66,9 +67,9 @@ export interface ExecResult {
|
|
|
66
67
|
readonly stdout: string
|
|
67
68
|
readonly stderr: string
|
|
68
69
|
readonly exitCode: number
|
|
69
|
-
/** UTF-8 bytes
|
|
70
|
+
/** UTF-8 bytes of stdout dropped to stay inside the capture bound; see `overflow`. */
|
|
70
71
|
readonly stdoutDroppedBytes: number
|
|
71
|
-
/** UTF-8 bytes
|
|
72
|
+
/** UTF-8 bytes of stderr dropped to stay inside the capture bound; see `overflow`. */
|
|
72
73
|
readonly stderrDroppedBytes: number
|
|
73
74
|
}
|
|
74
75
|
|
|
@@ -97,18 +98,26 @@ export interface ExecOptions {
|
|
|
97
98
|
*/
|
|
98
99
|
readonly stdin?: string | undefined
|
|
99
100
|
/**
|
|
100
|
-
* Bytes retained per captured stream. A stream that prints more keeps
|
|
101
|
-
*
|
|
102
|
-
* bounded amount of memory rather than the whole of what it
|
|
101
|
+
* Bytes retained per captured stream. A stream that prints more keeps what
|
|
102
|
+
* `overflow` names and reports what it dropped, so a command printing
|
|
103
|
+
* gigabytes costs a bounded amount of memory rather than the whole of what it
|
|
104
|
+
* printed.
|
|
103
105
|
*
|
|
104
106
|
* Absent means unbounded, which is what a caller wants when it needs every
|
|
105
107
|
* byte a tool produced (`rg --files`, `git worktree list`).
|
|
106
108
|
*/
|
|
107
109
|
readonly maxCaptureBytes?: number | undefined
|
|
108
|
-
/**
|
|
109
|
-
|
|
110
|
+
/**
|
|
111
|
+
* What a stream past the bound keeps. `tail`, the default, keeps its last
|
|
112
|
+
* bytes. `ends` keeps its first and last halves and puts one line naming the
|
|
113
|
+
* dropped bytes where the middle was. `refuse` fails if either stream exceeds
|
|
114
|
+
* the bound.
|
|
115
|
+
*/
|
|
116
|
+
readonly overflow?: Overflow | undefined
|
|
110
117
|
}
|
|
111
118
|
|
|
119
|
+
type Overflow = "tail" | "ends" | "refuse"
|
|
120
|
+
|
|
112
121
|
/** One captured stream: the text kept, and the bytes dropped to keep it bounded. */
|
|
113
122
|
interface Capture {
|
|
114
123
|
readonly text: string
|
|
@@ -121,70 +130,143 @@ interface Tail {
|
|
|
121
130
|
dropped: number
|
|
122
131
|
}
|
|
123
132
|
|
|
133
|
+
/** A stream's first bytes and its last ones: what `overflow: "ends"` keeps. */
|
|
134
|
+
interface Ends {
|
|
135
|
+
readonly head: Array<Uint8Array>
|
|
136
|
+
headBytes: number
|
|
137
|
+
readonly tail: Tail
|
|
138
|
+
}
|
|
139
|
+
|
|
124
140
|
const decoder = new TextDecoder("utf-8")
|
|
125
141
|
|
|
142
|
+
const emptyTail = (): Tail => ({ chunks: [], bytes: 0, dropped: 0 })
|
|
143
|
+
|
|
144
|
+
/** A UTF-8 continuation byte is `10xxxxxx`; every other byte starts a character. */
|
|
145
|
+
const isContinuation = (byte: number): boolean => (byte & 0b1100_0000) === 0b1000_0000
|
|
146
|
+
|
|
147
|
+
const join = (chunks: ReadonlyArray<Uint8Array>, size: number): Uint8Array => {
|
|
148
|
+
const bytes = new Uint8Array(size)
|
|
149
|
+
let offset = 0
|
|
150
|
+
for (const chunk of chunks) {
|
|
151
|
+
bytes.set(chunk, offset)
|
|
152
|
+
offset += chunk.byteLength
|
|
153
|
+
}
|
|
154
|
+
return bytes
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Appends one chunk and drops from the front whatever no longer fits. */
|
|
158
|
+
const keepTail = (state: Tail, chunk: Uint8Array, maxBytes: number): void => {
|
|
159
|
+
if (chunk.byteLength === 0) return
|
|
160
|
+
state.chunks.push(chunk)
|
|
161
|
+
state.bytes += chunk.byteLength
|
|
162
|
+
while (state.bytes > maxBytes && state.chunks.length > 0) {
|
|
163
|
+
const excess = state.bytes - maxBytes
|
|
164
|
+
const first = state.chunks[0]!
|
|
165
|
+
if (first.byteLength <= excess) {
|
|
166
|
+
state.chunks.shift()
|
|
167
|
+
state.bytes -= first.byteLength
|
|
168
|
+
state.dropped += first.byteLength
|
|
169
|
+
} else {
|
|
170
|
+
state.chunks[0] = first.subarray(excess)
|
|
171
|
+
state.bytes -= excess
|
|
172
|
+
state.dropped += excess
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
126
177
|
/**
|
|
127
178
|
* Joins the retained chunks and decodes them, skipping the partial code point a
|
|
128
179
|
* dropped head can leave behind.
|
|
129
180
|
*
|
|
130
|
-
* A
|
|
131
|
-
* bytes
|
|
132
|
-
*
|
|
133
|
-
*
|
|
181
|
+
* A code point is at most four bytes, so at most three leading continuation
|
|
182
|
+
* bytes can precede the first whole character. Skipping them is counted as
|
|
183
|
+
* dropped rather than decoded to a replacement character the process never
|
|
184
|
+
* printed.
|
|
134
185
|
*/
|
|
135
186
|
const decodeTail = (state: Tail): Capture => {
|
|
136
|
-
const bytes =
|
|
137
|
-
let offset = 0
|
|
138
|
-
for (const chunk of state.chunks) {
|
|
139
|
-
bytes.set(chunk, offset)
|
|
140
|
-
offset += chunk.byteLength
|
|
141
|
-
}
|
|
187
|
+
const bytes = join(state.chunks, state.bytes)
|
|
142
188
|
if (state.dropped === 0) return { text: decoder.decode(bytes), droppedBytes: 0 }
|
|
143
189
|
let start = 0
|
|
144
|
-
while (start < bytes.byteLength && start < 3 && (bytes[start]!
|
|
190
|
+
while (start < bytes.byteLength && start < 3 && isContinuation(bytes[start]!)) start++
|
|
145
191
|
return { text: decoder.decode(bytes.subarray(start)), droppedBytes: state.dropped + start }
|
|
146
192
|
}
|
|
147
193
|
|
|
194
|
+
/**
|
|
195
|
+
* The length of a cut head without the character the cut left unfinished.
|
|
196
|
+
*
|
|
197
|
+
* Its last character starts at the last byte that is not a continuation byte,
|
|
198
|
+
* and that byte says how many bytes the character needs. One that needs more
|
|
199
|
+
* than the head holds is dropped, for the same reason {@link decodeTail} skips
|
|
200
|
+
* a partial code point.
|
|
201
|
+
*/
|
|
202
|
+
const wholeLength = (bytes: Uint8Array): number => {
|
|
203
|
+
for (let back = 1; back <= Math.min(3, bytes.byteLength); back++) {
|
|
204
|
+
const byte = bytes[bytes.byteLength - back]!
|
|
205
|
+
if (isContinuation(byte)) continue
|
|
206
|
+
const width = byte >= 0b1111_0000 ? 4 : byte >= 0b1110_0000 ? 3 : byte >= 0b1100_0000 ? 2 : 1
|
|
207
|
+
return width > back ? bytes.byteLength - back : bytes.byteLength
|
|
208
|
+
}
|
|
209
|
+
return bytes.byteLength
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Decodes a stream kept by its ends. A stream that fit is decoded whole;
|
|
214
|
+
* otherwise each end is cut on a character boundary and one line naming every
|
|
215
|
+
* byte between them stands where the middle was.
|
|
216
|
+
*/
|
|
217
|
+
const decodeEnds = (state: Ends): Capture => {
|
|
218
|
+
if (state.tail.dropped === 0) {
|
|
219
|
+
const whole = join([...state.head, ...state.tail.chunks], state.headBytes + state.tail.bytes)
|
|
220
|
+
return { text: decoder.decode(whole), droppedBytes: 0 }
|
|
221
|
+
}
|
|
222
|
+
const head = join(state.head, state.headBytes)
|
|
223
|
+
const kept = wholeLength(head)
|
|
224
|
+
const tail = decodeTail(state.tail)
|
|
225
|
+
const droppedBytes = head.byteLength - kept + tail.droppedBytes
|
|
226
|
+
return { text: elideMiddle(decoder.decode(head.subarray(0, kept)), droppedBytes, tail.text), droppedBytes }
|
|
227
|
+
}
|
|
228
|
+
|
|
148
229
|
const boundedTail = <E>(
|
|
149
230
|
stream: Stream.Stream<Uint8Array, E>,
|
|
150
231
|
maxBytes: number,
|
|
151
|
-
overflow:
|
|
232
|
+
overflow: Overflow | undefined
|
|
152
233
|
): Effect.Effect<Capture, E> =>
|
|
153
|
-
Stream.runFold(
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
if (chunk.byteLength === 0) return state
|
|
158
|
-
state.chunks.push(chunk)
|
|
159
|
-
state.bytes += chunk.byteLength
|
|
160
|
-
while (state.bytes > maxBytes && state.chunks.length > 0) {
|
|
161
|
-
const excess = state.bytes - maxBytes
|
|
162
|
-
const first = state.chunks[0]!
|
|
163
|
-
if (first.byteLength <= excess) {
|
|
164
|
-
state.chunks.shift()
|
|
165
|
-
state.bytes -= first.byteLength
|
|
166
|
-
state.dropped += first.byteLength
|
|
167
|
-
} else {
|
|
168
|
-
state.chunks[0] = first.subarray(excess)
|
|
169
|
-
state.bytes -= excess
|
|
170
|
-
state.dropped += excess
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
return state
|
|
174
|
-
}
|
|
175
|
-
).pipe(Effect.map((state) =>
|
|
234
|
+
Stream.runFold(stream, emptyTail, (state, chunk) => {
|
|
235
|
+
keepTail(state, chunk, maxBytes)
|
|
236
|
+
return state
|
|
237
|
+
}).pipe(Effect.map((state) =>
|
|
176
238
|
overflow === "refuse" && state.dropped > 0
|
|
177
239
|
? { text: "", droppedBytes: state.dropped }
|
|
178
240
|
: decodeTail(state)
|
|
179
241
|
))
|
|
180
242
|
|
|
243
|
+
/**
|
|
244
|
+
* Keeps the first half of the bound and the last half. The head fills first,
|
|
245
|
+
* and every byte after it passes through the tail, which drops from its front:
|
|
246
|
+
* what falls out of the tail is the middle.
|
|
247
|
+
*/
|
|
248
|
+
const boundedEnds = <E>(stream: Stream.Stream<Uint8Array, E>, maxBytes: number): Effect.Effect<Capture, E> => {
|
|
249
|
+
const headBytes = Math.floor(maxBytes / 2)
|
|
250
|
+
return Stream.runFold(stream, (): Ends => ({ head: [], headBytes: 0, tail: emptyTail() }), (state, chunk) => {
|
|
251
|
+
const taken = Math.min(headBytes - state.headBytes, chunk.byteLength)
|
|
252
|
+
if (taken > 0) {
|
|
253
|
+
state.head.push(chunk.subarray(0, taken))
|
|
254
|
+
state.headBytes += taken
|
|
255
|
+
}
|
|
256
|
+
keepTail(state.tail, chunk.subarray(taken), maxBytes - headBytes)
|
|
257
|
+
return state
|
|
258
|
+
}).pipe(Effect.map(decodeEnds))
|
|
259
|
+
}
|
|
260
|
+
|
|
181
261
|
const capture = <E>(
|
|
182
262
|
stream: Stream.Stream<Uint8Array, E>,
|
|
183
263
|
maxBytes: number | undefined,
|
|
184
|
-
overflow:
|
|
264
|
+
overflow: Overflow | undefined
|
|
185
265
|
): Effect.Effect<Capture, E> =>
|
|
186
266
|
maxBytes === undefined
|
|
187
267
|
? Effect.map(Stream.mkString(Stream.decodeText(stream)), (text) => ({ text, droppedBytes: 0 }))
|
|
268
|
+
: overflow === "ends"
|
|
269
|
+
? boundedEnds(stream, maxBytes)
|
|
188
270
|
: boundedTail(stream, maxBytes, overflow)
|
|
189
271
|
|
|
190
272
|
const unbounded = (
|
package/src/internal/Http.ts
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
import type { GrantStore } from "@smthrs/kernel/GrantStore"
|
|
8
8
|
import * as HttpClient from "@smthrs/kernel/HttpClient"
|
|
9
9
|
import * as Effect from "effect/Effect"
|
|
10
|
+
import * as HttpClientRequest from "effect/http/HttpClientRequest"
|
|
10
11
|
import * as Schema from "effect/Schema"
|
|
11
12
|
import * as Stream from "effect/Stream"
|
|
12
|
-
import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"
|
|
13
13
|
import * as StdError from "../StdError.ts"
|
|
14
14
|
import { guarded, refusal } from "./HttpNetwork.ts"
|
|
15
15
|
import { MAX_OUTPUT_BYTES, notice, truncateBytes } from "./Text.ts"
|
|
@@ -13,9 +13,9 @@ import {
|
|
|
13
13
|
supportsDestinationPinning
|
|
14
14
|
} from "@smthrs/kernel/HttpClient"
|
|
15
15
|
import { Context, Effect, Option } from "effect"
|
|
16
|
-
import * as FetchHttpClient from "effect/
|
|
17
|
-
import * as HttpClient from "effect/
|
|
18
|
-
import * as HttpClientError from "effect/
|
|
16
|
+
import * as FetchHttpClient from "effect/http/FetchHttpClient"
|
|
17
|
+
import * as HttpClient from "effect/http/HttpClient"
|
|
18
|
+
import * as HttpClientError from "effect/http/HttpClientError"
|
|
19
19
|
import { lookup } from "node:dns/promises"
|
|
20
20
|
import { BlockList, isIP } from "node:net"
|
|
21
21
|
import * as StdError from "../StdError.ts"
|
package/src/internal/Text.ts
CHANGED
|
@@ -124,6 +124,21 @@ export const truncateBytes = (
|
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Joins the start and the end of a stream cut in the middle around one line
|
|
129
|
+
* naming the bytes cut between them.
|
|
130
|
+
*
|
|
131
|
+
* The line stands alone, so a start that stops mid-line is ended before it.
|
|
132
|
+
* Only the line's own newlines are added; `droppedBytes` counts printed bytes.
|
|
133
|
+
*
|
|
134
|
+
* @category rendering
|
|
135
|
+
* @since 1.0.0-rc.5
|
|
136
|
+
*/
|
|
137
|
+
export const elideMiddle = (start: string, droppedBytes: number, end: string): string =>
|
|
138
|
+
`${start}${start === "" || start.endsWith("\n") ? "" : "\n"}[${droppedBytes} ${
|
|
139
|
+
droppedBytes === 1 ? "byte" : "bytes"
|
|
140
|
+
} omitted]\n${end}`
|
|
141
|
+
|
|
127
142
|
/**
|
|
128
143
|
* Splits on LF without counting a terminal LF as a line.
|
|
129
144
|
* Carriage returns remain part of each line, including the CR in CRLF, so
|