@effect-uai/core 0.10.0 → 0.11.0
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/dist/{Image-BZmKfIdq.d.mts → Image-C4vuBmu0.d.mts} +11 -11
- package/dist/{Image-BZmKfIdq.d.mts.map → Image-C4vuBmu0.d.mts.map} +1 -1
- package/dist/{Items-BnIduEV2.d.mts → Items-BWNu6VGp.d.mts} +116 -90
- package/dist/Items-BWNu6VGp.d.mts.map +1 -0
- package/dist/{Tool-2lC_XMth.d.mts → Tool-CXFHCQrT.d.mts} +42 -9
- package/dist/Tool-CXFHCQrT.d.mts.map +1 -0
- package/dist/{ToolEvent-cWZ2BnbS.d.mts → ToolEvent-nVijCrvm.d.mts} +2 -2
- package/dist/{ToolEvent-cWZ2BnbS.d.mts.map → ToolEvent-nVijCrvm.d.mts.map} +1 -1
- package/dist/{ToolResult-CL4hK_9z.d.mts → ToolResult-CQqKkcDa.d.mts} +2 -2
- package/dist/{ToolResult-CL4hK_9z.d.mts.map → ToolResult-CQqKkcDa.d.mts.map} +1 -1
- package/dist/{Toolkit-Dr9l_hNy.d.mts → Toolkit-BYPwwu-j.d.mts} +6 -6
- package/dist/Toolkit-BYPwwu-j.d.mts.map +1 -0
- package/dist/browser/Browser.mjs +4 -4
- package/dist/browser/Browser.mjs.map +1 -1
- package/dist/browser/BrowserTool.d.mts +34 -33
- package/dist/browser/BrowserTool.d.mts.map +1 -1
- package/dist/{dist-DWgRbDN3.mjs → dist-C0m0QnUP.mjs} +71 -30
- package/dist/dist-C0m0QnUP.mjs.map +1 -0
- package/dist/{dist-B04dlFHg.mjs → dist-CcRV3-A5.mjs} +8 -8
- package/dist/{dist-B04dlFHg.mjs.map → dist-CcRV3-A5.mjs.map} +1 -1
- package/dist/domain/Citation.d.mts +60 -0
- package/dist/domain/Citation.d.mts.map +1 -0
- package/dist/domain/Citation.mjs +7 -0
- package/dist/domain/Citation.mjs.map +1 -0
- package/dist/domain/Image.d.mts +1 -1
- package/dist/domain/Items.d.mts +1 -1
- package/dist/domain/Items.mjs +13 -3
- package/dist/domain/Items.mjs.map +1 -1
- package/dist/domain/Research.d.mts +37 -0
- package/dist/domain/Research.d.mts.map +1 -0
- package/dist/domain/Research.mjs +7 -0
- package/dist/domain/Research.mjs.map +1 -0
- package/dist/domain/Turn.d.mts +91 -9
- package/dist/domain/Turn.d.mts.map +1 -1
- package/dist/domain/Turn.mjs +8 -1
- package/dist/domain/Turn.mjs.map +1 -1
- package/dist/domain/Turn.test.mjs +1 -1
- package/dist/embedding-model/Embedding.d.mts +1 -1
- package/dist/embedding-model/EmbeddingModel.mjs +2 -2
- package/dist/embedding-model/EmbeddingModel.mjs.map +1 -1
- package/dist/embedding-model/EmbeddingModel.test.mjs +1 -1
- package/dist/index.d.mts +12 -8
- package/dist/index.mjs +5 -1
- package/dist/job/Job.d.mts +80 -0
- package/dist/job/Job.d.mts.map +1 -0
- package/dist/job/Job.mjs +49 -0
- package/dist/job/Job.mjs.map +1 -0
- package/dist/language-model/LanguageModel.d.mts +2 -2
- package/dist/language-model/LanguageModel.mjs +2 -2
- package/dist/language-model/LanguageModel.mjs.map +1 -1
- package/dist/language-model/LanguageModel.test.mjs +1 -1
- package/dist/loop/Loop.test.mjs +1 -1
- package/dist/music-generator/MusicGenerator.mjs +4 -4
- package/dist/music-generator/MusicGenerator.mjs.map +1 -1
- package/dist/music-generator/MusicGenerator.test.mjs +1 -1
- package/dist/observability/Metrics.d.mts +1 -1
- package/dist/observability/Metrics.test.mjs +2 -2
- package/dist/observability/Telemetry.test.mjs +2 -2
- package/dist/research/DeepResearch.d.mts +71 -0
- package/dist/research/DeepResearch.d.mts.map +1 -0
- package/dist/research/DeepResearch.mjs +61 -0
- package/dist/research/DeepResearch.mjs.map +1 -0
- package/dist/retry/Retry.test.mjs +1 -1
- package/dist/sandbox/Sandbox.mjs +18 -18
- package/dist/sandbox/Sandbox.mjs.map +1 -1
- package/dist/speech-synthesizer/SpeechSynthesizer.mjs +8 -8
- package/dist/speech-synthesizer/SpeechSynthesizer.mjs.map +1 -1
- package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs +1 -1
- package/dist/streaming/JSONL.d.mts +3 -1
- package/dist/streaming/JSONL.d.mts.map +1 -1
- package/dist/streaming/JSONL.mjs +5 -11
- package/dist/streaming/JSONL.mjs.map +1 -1
- package/dist/streaming/JSONL.test.mjs +10 -1
- package/dist/streaming/JSONL.test.mjs.map +1 -1
- package/dist/streaming/SSE.d.mts +5 -3
- package/dist/streaming/SSE.d.mts.map +1 -1
- package/dist/streaming/SSE.mjs +28 -38
- package/dist/streaming/SSE.mjs.map +1 -1
- package/dist/streaming/SSE.test.mjs +54 -4
- package/dist/streaming/SSE.test.mjs.map +1 -1
- package/dist/structured-format/StructuredFormat.test.mjs +1 -1
- package/dist/testing/MockProvider.d.mts +1 -1
- package/dist/tool/Approval.d.mts +3 -3
- package/dist/tool/Approval.test.mjs +1 -1
- package/dist/tool/HistoryCheck.d.mts +2 -2
- package/dist/tool/Tool.d.mts +2 -2
- package/dist/tool/Tool.mjs +27 -7
- package/dist/tool/Tool.mjs.map +1 -1
- package/dist/tool/Tool.test.mjs +127 -3
- package/dist/tool/Tool.test.mjs.map +1 -1
- package/dist/tool/ToolEvent.d.mts +1 -1
- package/dist/tool/ToolResult.d.mts +1 -1
- package/dist/tool/Toolkit.d.mts +1 -1
- package/dist/tool/Toolkit.mjs.map +1 -1
- package/dist/tool/Toolkit.test.mjs +46 -2
- package/dist/tool/Toolkit.test.mjs.map +1 -1
- package/dist/transcriber/Transcriber.mjs +3 -3
- package/dist/transcriber/Transcriber.mjs.map +1 -1
- package/dist/transcriber/Transcriber.test.mjs +1 -1
- package/dist/web-read/WebRead.mjs +1 -1
- package/dist/web-read/WebRead.mjs.map +1 -1
- package/dist/web-read/WebReadTool.d.mts +1 -1
- package/dist/web-search/WebSearch.mjs +1 -1
- package/dist/web-search/WebSearch.mjs.map +1 -1
- package/dist/web-search/WebSearchTool.d.mts +1 -1
- package/package.json +19 -3
- package/src/browser/Browser.ts +4 -4
- package/src/domain/Citation.ts +64 -0
- package/src/domain/Items.ts +14 -4
- package/src/domain/Research.ts +42 -0
- package/src/domain/Turn.ts +26 -0
- package/src/embedding-model/EmbeddingModel.ts +2 -2
- package/src/index.ts +4 -0
- package/src/job/Job.ts +125 -0
- package/src/language-model/LanguageModel.ts +2 -2
- package/src/music-generator/MusicGenerator.ts +4 -4
- package/src/research/DeepResearch.ts +137 -0
- package/src/sandbox/Sandbox.ts +18 -18
- package/src/speech-synthesizer/SpeechSynthesizer.ts +8 -8
- package/src/streaming/JSONL.test.ts +19 -0
- package/src/streaming/JSONL.ts +6 -38
- package/src/streaming/SSE.test.ts +86 -3
- package/src/streaming/SSE.ts +36 -80
- package/src/tool/Tool.test.ts +151 -1
- package/src/tool/Tool.ts +120 -22
- package/src/tool/Toolkit.test.ts +60 -0
- package/src/tool/Toolkit.ts +2 -2
- package/src/transcriber/Transcriber.ts +3 -3
- package/src/web-read/WebRead.ts +1 -1
- package/src/web-search/WebSearch.ts +1 -1
- package/dist/Items-BnIduEV2.d.mts.map +0 -1
- package/dist/Tool-2lC_XMth.d.mts.map +0 -1
- package/dist/Toolkit-Dr9l_hNy.d.mts.map +0 -1
- package/dist/dist-DWgRbDN3.mjs.map +0 -1
package/dist/sandbox/Sandbox.mjs
CHANGED
|
@@ -117,20 +117,20 @@ var SandboxPortExposure = class extends Context.Service()("@betalyra/effect-uai/
|
|
|
117
117
|
* Provision a sandbox bound to the caller's scope. Drop the
|
|
118
118
|
* `Scope.Scope` requirement with `Effect.scoped`.
|
|
119
119
|
*/
|
|
120
|
-
const create = (request) => Effect.flatMap(Sandbox
|
|
120
|
+
const create = (request) => Effect.flatMap(Sandbox, (s) => s.create(request));
|
|
121
121
|
/**
|
|
122
122
|
* Re-acquire an existing sandbox by id. Same scope semantics as
|
|
123
123
|
* `create`, but the finalizer detaches rather than destroying.
|
|
124
124
|
*/
|
|
125
|
-
const attach = (id) => Effect.flatMap(Sandbox
|
|
125
|
+
const attach = (id) => Effect.flatMap(Sandbox, (s) => s.attach(id));
|
|
126
126
|
/** Enumerate sandboxes for the configured account / project. */
|
|
127
|
-
const list = Effect.flatMap(Sandbox
|
|
127
|
+
const list = Effect.flatMap(Sandbox, (s) => s.list);
|
|
128
128
|
/**
|
|
129
129
|
* Escape hatch for destroying a sandbox from outside its owning
|
|
130
130
|
* scope. Most callers should let the scope finalizer handle this
|
|
131
131
|
* automatically.
|
|
132
132
|
*/
|
|
133
|
-
const destroy = (id) => Effect.flatMap(Sandbox
|
|
133
|
+
const destroy = (id) => Effect.flatMap(Sandbox, (s) => s.destroy(id));
|
|
134
134
|
/**
|
|
135
135
|
* Create a snapshot of a live sandbox. Requires
|
|
136
136
|
* {@link SandboxSnapshots} on the R channel — adapters whose
|
|
@@ -138,40 +138,40 @@ const destroy = (id) => Effect.flatMap(Sandbox.asEffect(), (s) => s.destroy(id))
|
|
|
138
138
|
* marker and calls fail at `Effect.provide` with a type error.
|
|
139
139
|
*/
|
|
140
140
|
const snapshot = (from, name) => Effect.gen(function* () {
|
|
141
|
-
const s = yield* Sandbox
|
|
142
|
-
yield* SandboxSnapshots
|
|
141
|
+
const s = yield* Sandbox;
|
|
142
|
+
yield* SandboxSnapshots;
|
|
143
143
|
return yield* s.snapshots.create(from, name);
|
|
144
144
|
});
|
|
145
145
|
/**
|
|
146
146
|
* Create a named persistent volume. Requires {@link SandboxVolumes}.
|
|
147
147
|
*/
|
|
148
148
|
const createVolume = (name, options) => Effect.gen(function* () {
|
|
149
|
-
const s = yield* Sandbox
|
|
150
|
-
yield* SandboxVolumes
|
|
149
|
+
const s = yield* Sandbox;
|
|
150
|
+
yield* SandboxVolumes;
|
|
151
151
|
return yield* s.volumes.create(name, options);
|
|
152
152
|
});
|
|
153
153
|
/** Destroy a named volume. Requires {@link SandboxVolumes}. */
|
|
154
154
|
const destroyVolume = (id) => Effect.gen(function* () {
|
|
155
|
-
const s = yield* Sandbox
|
|
156
|
-
yield* SandboxVolumes
|
|
155
|
+
const s = yield* Sandbox;
|
|
156
|
+
yield* SandboxVolumes;
|
|
157
157
|
return yield* s.volumes.destroy(id);
|
|
158
158
|
});
|
|
159
159
|
/** Enumerate persistent volumes. Requires {@link SandboxVolumes}. */
|
|
160
160
|
const listVolumes = Effect.gen(function* () {
|
|
161
|
-
const s = yield* Sandbox
|
|
162
|
-
yield* SandboxVolumes
|
|
161
|
+
const s = yield* Sandbox;
|
|
162
|
+
yield* SandboxVolumes;
|
|
163
163
|
return yield* s.volumes.list;
|
|
164
164
|
});
|
|
165
165
|
/** Destroy a snapshot. Requires {@link SandboxSnapshots}. */
|
|
166
166
|
const destroySnapshot = (id) => Effect.gen(function* () {
|
|
167
|
-
const s = yield* Sandbox
|
|
168
|
-
yield* SandboxSnapshots
|
|
167
|
+
const s = yield* Sandbox;
|
|
168
|
+
yield* SandboxSnapshots;
|
|
169
169
|
return yield* s.snapshots.destroy(id);
|
|
170
170
|
});
|
|
171
171
|
/** Enumerate snapshots. Requires {@link SandboxSnapshots}. */
|
|
172
172
|
const listSnapshots = Effect.gen(function* () {
|
|
173
|
-
const s = yield* Sandbox
|
|
174
|
-
yield* SandboxSnapshots
|
|
173
|
+
const s = yield* Sandbox;
|
|
174
|
+
yield* SandboxSnapshots;
|
|
175
175
|
return yield* s.snapshots.list;
|
|
176
176
|
});
|
|
177
177
|
/**
|
|
@@ -182,8 +182,8 @@ const listSnapshots = Effect.gen(function* () {
|
|
|
182
182
|
* fail at `Effect.provide` with a type error.
|
|
183
183
|
*/
|
|
184
184
|
const exposePort = (instance, port) => Effect.gen(function* () {
|
|
185
|
-
const s = yield* Sandbox
|
|
186
|
-
yield* SandboxPortExposure
|
|
185
|
+
const s = yield* Sandbox;
|
|
186
|
+
yield* SandboxPortExposure;
|
|
187
187
|
return yield* s.ports.expose(instance, port);
|
|
188
188
|
});
|
|
189
189
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Sandbox.mjs","names":[],"sources":["../../src/sandbox/Sandbox.ts"],"sourcesContent":["import { Brand, Context, Data, type Duration, Effect, Redacted, Scope, Stream } from \"effect\"\nimport type * as SandboxError from \"./SandboxError.js\"\n\n// ---------------------------------------------------------------------------\n// Identity\n// ---------------------------------------------------------------------------\n\n/**\n * Sandbox identifier. Branded `string` so providers don't accidentally\n * accept a raw user-supplied id. Each provider mints these per its own\n * naming scheme (uuid for E2B/Vercel, user-supplied name for\n * Microsandbox/Modal, …); the core code treats them opaquely.\n *\n * Mint via the value-level `SandboxId(...)` smart constructor instead\n * of `as SandboxId` casts.\n */\nexport type SandboxId = Brand.Branded<string, \"SandboxId\">\nexport const SandboxId = Brand.nominal<SandboxId>()\n\n/**\n * Reference returned by `list`. Carries just enough to call `attach` —\n * provider-specific metadata (createdAt, region, tags, …) lives on the\n * narrowed per-provider ref.\n */\nexport type SandboxRef = {\n readonly id: SandboxId\n readonly name?: string\n}\n\nexport type VolumeId = Brand.Branded<string, \"VolumeId\">\nexport const VolumeId = Brand.nominal<VolumeId>()\n\nexport type SnapshotId = Brand.Branded<string, \"SnapshotId\">\nexport const SnapshotId = Brand.nominal<SnapshotId>()\n\n// ---------------------------------------------------------------------------\n// Image / runtime selection\n// ---------------------------------------------------------------------------\n\n/**\n * What runtime / filesystem to boot the sandbox from.\n *\n * - `Default` — provider picks its house image (Vercel: Amazon Linux\n * 2023; Microsandbox: requires user image but a builder default may\n * apply, …). Use when you don't care what's pre-installed.\n * - `Registry` — an OCI image reference. Supported by every provider\n * except those with a fixed base (Vercel ignores this and the\n * request-schema rejects at decode).\n * - `Snapshot` — restore from a captured state. Gated by\n * {@link SandboxSnapshots} at the call site — the marker is provided\n * by adapters whose provider supports snapshot-as-image.\n * - `Dockerfile` — provider builds a custom image from a Dockerfile.\n * Gated by {@link SandboxCustomImage}.\n */\nexport type ImageRef = Data.TaggedEnum<{\n Default: {}\n Registry: { readonly ref: string }\n Snapshot: { readonly id: SnapshotId }\n Dockerfile: { readonly contents: string }\n}>\n\nexport const ImageRef = Data.taggedEnum<ImageRef>()\n\n// ---------------------------------------------------------------------------\n// Network policy + secrets\n// ---------------------------------------------------------------------------\n\n/**\n * Outbound network policy for a sandbox.\n *\n * - `Open` — no egress restriction. The provider's defaults apply\n * (e.g. Microsandbox's \"public-only\" still blocks private ranges).\n * - `Blocked` — fully airgapped, no outbound connectivity at all.\n * - `Allowlist` — only the listed hosts / CIDRs may be reached.\n *\n * Providers vary in what they honour: hostname-allowlist requires\n * {@link SandboxHostnameAllowlist}; CIDR-only providers ignore `hosts`\n * and the adapter rejects at decode if `hosts` is non-empty.\n */\nexport type NetworkPolicy = Data.TaggedEnum<{\n Open: {}\n Blocked: {}\n Allowlist: {\n readonly hosts?: ReadonlyArray<string>\n readonly cidrs?: ReadonlyArray<string>\n }\n}>\n\nexport const NetworkPolicy = Data.taggedEnum<NetworkPolicy>()\n\n/**\n * A secret bound to a sandbox at create time. The code inside the\n * sandbox sees only a placeholder (`$MSB_<NAME>` for Microsandbox,\n * `{name}` for Vercel header-injection, etc.); the real `value` lives\n * on the provider's proxy and is substituted on outbound HTTPS to\n * `hosts`.\n *\n * Requires the {@link SandboxSecretInjection} capability marker —\n * provider Layers without proxy-layer secret rewriting do not ship the\n * marker and consumer code fails at `Effect.provide` with a type\n * error.\n *\n * `header` is the request header to inject (default\n * `\"Authorization: Bearer <value>\"`). Providers that do not let\n * callers pick the header (Microsandbox's `secretEnv` shorthand)\n * silently ignore this field; providers that require an explicit\n * header (Vercel's `transform.headers`) fail at decode if it's absent.\n */\nexport type BoundSecret = {\n readonly name: string\n readonly value: Redacted.Redacted<string>\n readonly hosts: ReadonlyArray<string>\n readonly header?: string\n}\n\n// ---------------------------------------------------------------------------\n// Volumes (capability-gated on `SandboxVolumes`)\n// ---------------------------------------------------------------------------\n\nexport type VolumeMount = {\n readonly id: VolumeId\n readonly mountPath: string\n readonly readonly?: boolean\n}\n\n// ---------------------------------------------------------------------------\n// Create / exec request shapes\n// ---------------------------------------------------------------------------\n\n/**\n * Cross-provider create request. Provider-specific knobs (region,\n * cpus, memory, template id, …) live on each provider's narrowed\n * request type which extends this.\n *\n * Fields a provider can't honour fail at **decode time** (per the\n * per-provider request schema), not silently at runtime. The\n * ComputeSDK anti-pattern of \"drop unsupported fields\" is the\n * footgun we're avoiding.\n */\nexport type CommonCreateRequest = {\n /** Image / snapshot to boot from. Omit for the provider's default. */\n readonly image?: ImageRef\n /**\n * Idle / hard timeout. Accepts a number (millis), a string like\n * `\"30 seconds\"`, or a `Duration`. Providers default differently.\n */\n readonly timeout?: Duration.Input\n /**\n * Env vars exposed to the sandbox. NOT for secrets you want hidden\n * from the running code — use {@link BoundSecret} instead.\n */\n readonly env?: Readonly<Record<string, string>>\n /** Outbound network policy. */\n readonly network?: NetworkPolicy\n /**\n * Bound secrets — code inside the sandbox sees a placeholder; the\n * proxy substitutes the real value on outbound HTTPS to listed hosts.\n * Capability-gated on {@link SandboxSecretInjection}.\n */\n readonly secrets?: ReadonlyArray<BoundSecret>\n /**\n * Persistent volumes to mount. Volumes are created via\n * `Sandbox.volumes.create` and survive sandbox lifetime.\n * Capability-gated on {@link SandboxVolumes}.\n */\n readonly volumes?: ReadonlyArray<VolumeMount>\n}\n\n/**\n * Cross-provider exec request. Background / long-running processes\n * use `spawn` instead of `exec`; the semantics differ enough that\n * overloading `exec` with a `detached` flag was rejected during\n * design.\n */\nexport type CommonExecRequest = {\n /**\n * Argv-style command. Pass a string for \"run this through the\n * shell\" semantics (provider-dependent — most use `sh -c`); pass\n * an array for direct argv with no shell parsing.\n */\n readonly cmd: string | ReadonlyArray<string>\n readonly cwd?: string\n readonly env?: Readonly<Record<string, string>>\n readonly stdin?: string | Uint8Array\n /**\n * Per-exec timeout. Accepts a number (millis), a string like\n * `\"5 seconds\"`, or a `Duration`.\n */\n readonly timeout?: Duration.Input\n}\n\n/**\n * Spawn request — extends {@link CommonExecRequest} with the option\n * to provide stdin as a `Stream<Uint8Array>`. The provider takes one\n * chunk at a time and sends EOF when the stream completes. Useful\n * for piping LLM output into a sandboxed process, or feeding stdin\n * from another stream source. `exec` does not accept streams.\n */\nexport type CommonSpawnRequest = Omit<CommonExecRequest, \"stdin\"> & {\n readonly stdin?: string | Uint8Array | Stream.Stream<Uint8Array, never, never>\n}\n\nexport type ExecResult = {\n readonly exitCode: number\n readonly stdout: string\n readonly stderr: string\n readonly durationMs: number\n}\n\n/**\n * Streaming exec output. `Complete` is terminal — the stream\n * completes after emitting it.\n */\nexport type ExecEvent = Data.TaggedEnum<{\n Stdout: { readonly chunk: Uint8Array }\n Stderr: { readonly chunk: Uint8Array }\n Complete: { readonly exitCode: number; readonly durationMs: number }\n}>\n\nexport const ExecEvent = Data.taggedEnum<ExecEvent>()\n\n// ---------------------------------------------------------------------------\n// Filesystem\n// ---------------------------------------------------------------------------\n\nexport type FileEntryKind = \"file\" | \"directory\" | \"symlink\" | \"other\"\n\nexport type FileEntry = {\n readonly path: string\n readonly kind: FileEntryKind\n}\n\nexport type SandboxFilesystem = {\n readonly read: (path: string) => Effect.Effect<Uint8Array, SandboxError.SandboxError>\n readonly write: (\n path: string,\n contents: Uint8Array | string,\n ) => Effect.Effect<void, SandboxError.SandboxError>\n readonly remove: (path: string) => Effect.Effect<void, SandboxError.SandboxError>\n readonly mkdir: (path: string) => Effect.Effect<void, SandboxError.SandboxError>\n readonly list: (\n path: string,\n ) => Effect.Effect<ReadonlyArray<FileEntry>, SandboxError.SandboxError>\n readonly exists: (path: string) => Effect.Effect<boolean, SandboxError.SandboxError>\n}\n\n// ---------------------------------------------------------------------------\n// Process handle (returned by `spawn`)\n// ---------------------------------------------------------------------------\n\n/**\n * Long-running process handle. Returned by `spawn` in a `Scope.Scope`\n * — the process is killed on scope close. To outlive the calling\n * scope, do `Effect.forkScoped` against a wider scope or use\n * `attach`-style reconnection per the provider's escape hatch.\n */\nexport type ProcessHandle = {\n readonly pid: number\n readonly events: Stream.Stream<ExecEvent, SandboxError.SandboxError>\n readonly kill: Effect.Effect<void, SandboxError.SandboxError>\n readonly exit: Effect.Effect<{ readonly exitCode: number }, SandboxError.SandboxError>\n}\n\n// ---------------------------------------------------------------------------\n// SandboxInstance — the live handle returned from `create` / `attach`\n// ---------------------------------------------------------------------------\n\n/**\n * Live sandbox handle. Methods are plain `Effect` / `Stream` values\n * — no nullary thunks (`destroy()`, `pause()`) — to match Effect's\n * `Queue.shutdown` / `Fiber.interrupt` style. Destruction is the\n * scope finalizer, not a method here; for the rare \"kill from\n * another fiber\" case use the service-level escape hatch\n * `Sandbox.destroy(id)`.\n */\nexport type SandboxInstance = {\n readonly id: SandboxId\n readonly exec: (\n request: CommonExecRequest,\n ) => Effect.Effect<ExecResult, SandboxError.SandboxError>\n readonly execStream: (\n request: CommonExecRequest,\n ) => Stream.Stream<ExecEvent, SandboxError.SandboxError>\n /**\n * Spawn a background / long-running process. The returned handle is\n * scoped — the process is killed on scope close. Use this for\n * \"dev server / watcher / tail -f\" workloads. For one-shot\n * commands prefer `exec`.\n */\n readonly spawn: (\n request: CommonSpawnRequest,\n ) => Effect.Effect<ProcessHandle, SandboxError.SandboxError, Scope.Scope>\n readonly files: SandboxFilesystem\n}\n\n// ---------------------------------------------------------------------------\n// Snapshot / volume / port sub-services (capability-gated)\n// ---------------------------------------------------------------------------\n\nexport type SandboxSnapshotsApi = {\n readonly create: (\n from: SandboxInstance,\n name?: string,\n ) => Effect.Effect<SnapshotId, SandboxError.SandboxError>\n readonly destroy: (id: SnapshotId) => Effect.Effect<void, SandboxError.SandboxError>\n readonly list: Effect.Effect<\n ReadonlyArray<{ readonly id: SnapshotId; readonly name?: string }>,\n SandboxError.SandboxError\n >\n}\n\nexport type SandboxVolumesApi = {\n readonly create: (\n name: string,\n options?: { readonly quotaBytes?: number },\n ) => Effect.Effect<VolumeId, SandboxError.SandboxError>\n readonly destroy: (id: VolumeId) => Effect.Effect<void, SandboxError.SandboxError>\n readonly list: Effect.Effect<\n ReadonlyArray<{ readonly id: VolumeId; readonly name: string }>,\n SandboxError.SandboxError\n >\n}\n\nexport type SandboxPortsApi = {\n /**\n * Expose an internal sandbox port as a publicly reachable URL.\n * Gated by {@link SandboxPortExposure} on the free helper; adapters\n * that don't ship the marker leave this sub-API out of the public\n * surface entirely (callers can't construct an Effect against it).\n */\n readonly expose: (\n instance: SandboxInstance,\n port: number,\n ) => Effect.Effect<{ readonly url: string }, SandboxError.SandboxError>\n}\n\n// ---------------------------------------------------------------------------\n// Service definition\n// ---------------------------------------------------------------------------\n\n/**\n * Cross-provider sandbox service.\n *\n * Lifetime model: `create` returns a sandbox handle in `Scope.Scope`.\n * The scope finalizer destroys the sandbox. Use `Effect.scoped` to\n * bound a sandbox's lifetime to a single Effect, or manage the scope\n * explicitly via `Scope.make` for sandboxes that span many calls.\n * This is why there's no `destroy()` method on `SandboxInstance` —\n * the scope is the lifecycle handle.\n *\n * `snapshots` and `volumes` sub-APIs are present on the service but\n * are still capability-gated: provider Layers that don't support\n * them ship implementations that always fail with\n * `SandboxUnsupported`. The free helpers ({@link snapshot},\n * {@link createVolume}) additionally require the corresponding\n * capability marker on R, so misuses surface as type errors at\n * `Effect.provide`, not as runtime failures.\n */\nexport type SandboxService = {\n /**\n * Provision a new sandbox. The returned handle's lifetime is\n * bound to the caller's scope; the sandbox is destroyed on scope\n * close. To survive the scope, use `attach`-style reconnection or\n * promote into a wider scope.\n */\n readonly create: (\n request: CommonCreateRequest,\n ) => Effect.Effect<SandboxInstance, SandboxError.SandboxError, Scope.Scope>\n\n /**\n * Re-acquire an existing sandbox by id. The returned handle is\n * scope-bound but the finalizer is \"detach\" (no destroy) —\n * appropriate for paused / detached / persistent sandboxes that\n * outlive any single agent run.\n */\n readonly attach: (\n id: SandboxId,\n ) => Effect.Effect<SandboxInstance, SandboxError.SandboxError, Scope.Scope>\n\n /**\n * Enumerate sandboxes visible to the configured account / project.\n */\n readonly list: Effect.Effect<ReadonlyArray<SandboxRef>, SandboxError.SandboxError>\n\n /**\n * Escape hatch for destroying a sandbox from outside its owning\n * scope. Most callers should let the scope finalizer handle this.\n */\n readonly destroy: (id: SandboxId) => Effect.Effect<void, SandboxError.SandboxError>\n\n /** Snapshot management. Capability-gated on {@link SandboxSnapshots}. */\n readonly snapshots: SandboxSnapshotsApi\n\n /** Volume management. Capability-gated on {@link SandboxVolumes}. */\n readonly volumes: SandboxVolumesApi\n\n /** Port exposure. Capability-gated on {@link SandboxPortExposure}. */\n readonly ports: SandboxPortsApi\n}\n\nexport class Sandbox extends Context.Service<Sandbox, SandboxService>()(\n \"@betalyra/effect-uai/Sandbox\",\n) {}\n\n// ---------------------------------------------------------------------------\n// Capability markers\n// ---------------------------------------------------------------------------\n\n/**\n * Capability marker — provided by adapter Layers whose provider\n * supports a hostname-level egress allowlist (matching against host\n * names / wildcards, not just CIDRs). Vercel, Cloudflare, Deno,\n * Microsandbox, Runloop, Anthropic srt ship this marker; Modal and\n * Daytona (CIDR-only) do not.\n *\n * Phantom — the value is `void`; adapters register with\n * `Layer.succeed(SandboxHostnameAllowlist, undefined)`.\n */\nexport class SandboxHostnameAllowlist extends Context.Service<SandboxHostnameAllowlist, void>()(\n \"@betalyra/effect-uai/capability/SandboxHostnameAllowlist\",\n) {}\n\n/**\n * Capability marker — adapter's provider supports proxy-layer\n * secret rewriting (header injection on outbound HTTPS to bound\n * hosts; the running code never sees the real value). Deno,\n * Cloudflare, E2B, Vercel (firewall transform), Microsandbox,\n * Runloop (Agent Gateway) ship this. Modal, Daytona, CodeSandbox\n * do not.\n */\nexport class SandboxSecretInjection extends Context.Service<SandboxSecretInjection, void>()(\n \"@betalyra/effect-uai/capability/SandboxSecretInjection\",\n) {}\n\n/**\n * Capability marker — adapter supports snapshot-as-new-sandbox\n * (capture state, restore as a derived sandbox with a fresh id).\n * Distinct from {@link SandboxPauseResume}, which preserves the\n * same id and resumes in place.\n */\nexport class SandboxSnapshots extends Context.Service<SandboxSnapshots, void>()(\n \"@betalyra/effect-uai/capability/SandboxSnapshots\",\n) {}\n\n/**\n * Capability marker — adapter supports in-place pause and resume on\n * the same sandbox id (E2B, CodeSandbox). Memory + processes are\n * preserved across the pause boundary. Most providers without this\n * marker offer {@link SandboxSnapshots} instead.\n */\nexport class SandboxPauseResume extends Context.Service<SandboxPauseResume, void>()(\n \"@betalyra/effect-uai/capability/SandboxPauseResume\",\n) {}\n\n/**\n * Capability marker — adapter manages named persistent volumes\n * outside any single sandbox lifecycle (Modal Volume, Daytona,\n * Microsandbox, BoxLite). Providers without this marker leave\n * `Sandbox.volumes.*` returning `SandboxUnsupported`; the free\n * helpers ({@link createVolume}, {@link destroyVolume}) additionally\n * require this marker on R so misuses become a type error.\n */\nexport class SandboxVolumes extends Context.Service<SandboxVolumes, void>()(\n \"@betalyra/effect-uai/capability/SandboxVolumes\",\n) {}\n\n/**\n * Capability marker — adapter supports a stateful kernel session\n * (Jupyter-style: persistent Python / JS context that returns rich\n * outputs across many `runCode` calls). Cloudflare Code Interpreter\n * and E2B ship this; everyone else expects you to write your own\n * script and `exec` it.\n *\n * The actual `runCode` helper lives separately (not on\n * `SandboxInstance`) because the return shape is different — rich\n * outputs with images/tables, not buffered stdout strings.\n */\nexport class SandboxKernelSession extends Context.Service<SandboxKernelSession, void>()(\n \"@betalyra/effect-uai/capability/SandboxKernelSession\",\n) {}\n\n/**\n * Capability marker — adapter exposes an interactive PTY session\n * (Runloop, Cloudflare terminal). Distinct from `execStream` which\n * is byte-oriented and one-shot.\n */\nexport class SandboxPty extends Context.Service<SandboxPty, void>()(\n \"@betalyra/effect-uai/capability/SandboxPty\",\n) {}\n\n/**\n * Capability marker — adapter accepts a user-supplied Dockerfile (or\n * OCI image build) instead of just a registry reference. Vercel does\n * not ship this (fixed AL2023). Deno does not (snapshots only).\n * Most others do.\n */\nexport class SandboxCustomImage extends Context.Service<SandboxCustomImage, void>()(\n \"@betalyra/effect-uai/capability/SandboxCustomImage\",\n) {}\n\n/**\n * Capability marker — adapter can expose an internal port as a\n * publicly reachable URL. Most cloud providers ship this; local\n * providers (Microsandbox, BoxLite) typically only support\n * host-side port forwarding (handled by ignoring this marker and\n * documenting via the per-provider request type).\n */\nexport class SandboxPortExposure extends Context.Service<SandboxPortExposure, void>()(\n \"@betalyra/effect-uai/capability/SandboxPortExposure\",\n) {}\n\n// ---------------------------------------------------------------------------\n// Free helpers — accept Sandbox via Context, optionally require markers.\n// ---------------------------------------------------------------------------\n\n/**\n * Provision a sandbox bound to the caller's scope. Drop the\n * `Scope.Scope` requirement with `Effect.scoped`.\n */\nexport const create = (\n request: CommonCreateRequest,\n): Effect.Effect<SandboxInstance, SandboxError.SandboxError, Sandbox | Scope.Scope> =>\n Effect.flatMap(Sandbox.asEffect(), (s) => s.create(request))\n\n/**\n * Re-acquire an existing sandbox by id. Same scope semantics as\n * `create`, but the finalizer detaches rather than destroying.\n */\nexport const attach = (\n id: SandboxId,\n): Effect.Effect<SandboxInstance, SandboxError.SandboxError, Sandbox | Scope.Scope> =>\n Effect.flatMap(Sandbox.asEffect(), (s) => s.attach(id))\n\n/** Enumerate sandboxes for the configured account / project. */\nexport const list: Effect.Effect<\n ReadonlyArray<SandboxRef>,\n SandboxError.SandboxError,\n Sandbox\n> = Effect.flatMap(Sandbox.asEffect(), (s) => s.list)\n\n/**\n * Escape hatch for destroying a sandbox from outside its owning\n * scope. Most callers should let the scope finalizer handle this\n * automatically.\n */\nexport const destroy = (id: SandboxId): Effect.Effect<void, SandboxError.SandboxError, Sandbox> =>\n Effect.flatMap(Sandbox.asEffect(), (s) => s.destroy(id))\n\n/**\n * Create a snapshot of a live sandbox. Requires\n * {@link SandboxSnapshots} on the R channel — adapters whose\n * provider doesn't support snapshot-as-new-sandbox don't ship the\n * marker and calls fail at `Effect.provide` with a type error.\n */\nexport const snapshot = (\n from: SandboxInstance,\n name?: string,\n): Effect.Effect<SnapshotId, SandboxError.SandboxError, Sandbox | SandboxSnapshots> =>\n Effect.gen(function* () {\n const s = yield* Sandbox.asEffect()\n yield* SandboxSnapshots.asEffect()\n return yield* s.snapshots.create(from, name)\n })\n\n/**\n * Create a named persistent volume. Requires {@link SandboxVolumes}.\n */\nexport const createVolume = (\n name: string,\n options?: { readonly quotaBytes?: number },\n): Effect.Effect<VolumeId, SandboxError.SandboxError, Sandbox | SandboxVolumes> =>\n Effect.gen(function* () {\n const s = yield* Sandbox.asEffect()\n yield* SandboxVolumes.asEffect()\n return yield* s.volumes.create(name, options)\n })\n\n/** Destroy a named volume. Requires {@link SandboxVolumes}. */\nexport const destroyVolume = (\n id: VolumeId,\n): Effect.Effect<void, SandboxError.SandboxError, Sandbox | SandboxVolumes> =>\n Effect.gen(function* () {\n const s = yield* Sandbox.asEffect()\n yield* SandboxVolumes.asEffect()\n return yield* s.volumes.destroy(id)\n })\n\n/** Enumerate persistent volumes. Requires {@link SandboxVolumes}. */\nexport const listVolumes: Effect.Effect<\n ReadonlyArray<{ readonly id: VolumeId; readonly name: string }>,\n SandboxError.SandboxError,\n Sandbox | SandboxVolumes\n> = Effect.gen(function* () {\n const s = yield* Sandbox.asEffect()\n yield* SandboxVolumes.asEffect()\n return yield* s.volumes.list\n})\n\n/** Destroy a snapshot. Requires {@link SandboxSnapshots}. */\nexport const destroySnapshot = (\n id: SnapshotId,\n): Effect.Effect<void, SandboxError.SandboxError, Sandbox | SandboxSnapshots> =>\n Effect.gen(function* () {\n const s = yield* Sandbox.asEffect()\n yield* SandboxSnapshots.asEffect()\n return yield* s.snapshots.destroy(id)\n })\n\n/** Enumerate snapshots. Requires {@link SandboxSnapshots}. */\nexport const listSnapshots: Effect.Effect<\n ReadonlyArray<{ readonly id: SnapshotId; readonly name?: string }>,\n SandboxError.SandboxError,\n Sandbox | SandboxSnapshots\n> = Effect.gen(function* () {\n const s = yield* Sandbox.asEffect()\n yield* SandboxSnapshots.asEffect()\n return yield* s.snapshots.list\n})\n\n/**\n * Expose an internal port of a live sandbox as a publicly reachable\n * URL. Requires {@link SandboxPortExposure} — adapters whose provider\n * doesn't support runtime port exposure (e.g. Microsandbox, which\n * forwards at create time only) don't ship the marker, and calls\n * fail at `Effect.provide` with a type error.\n */\nexport const exposePort = (\n instance: SandboxInstance,\n port: number,\n): Effect.Effect<\n { readonly url: string },\n SandboxError.SandboxError,\n Sandbox | SandboxPortExposure\n> =>\n Effect.gen(function* () {\n const s = yield* Sandbox.asEffect()\n yield* SandboxPortExposure.asEffect()\n return yield* s.ports.expose(instance, port)\n })\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiBA,MAAa,YAAY,MAAM,SAAoB;AAanD,MAAa,WAAW,MAAM,SAAmB;AAGjD,MAAa,aAAa,MAAM,SAAqB;AA4BrD,MAAa,WAAW,KAAK,YAAsB;AA2BnD,MAAa,gBAAgB,KAAK,YAA2B;AAmI7D,MAAa,YAAY,KAAK,YAAuB;AAqLrD,IAAa,UAAb,cAA6B,QAAQ,SAAkC,CACrE,+BACD,CAAC;;;;;;;;;;;AAgBF,IAAa,2BAAb,cAA8C,QAAQ,SAAyC,CAC7F,2DACD,CAAC;;;;;;;;;AAUF,IAAa,yBAAb,cAA4C,QAAQ,SAAuC,CACzF,yDACD,CAAC;;;;;;;AAQF,IAAa,mBAAb,cAAsC,QAAQ,SAAiC,CAC7E,mDACD,CAAC;;;;;;;AAQF,IAAa,qBAAb,cAAwC,QAAQ,SAAmC,CACjF,qDACD,CAAC;;;;;;;;;AAUF,IAAa,iBAAb,cAAoC,QAAQ,SAA+B,CACzE,iDACD,CAAC;;;;;;;;;;;;AAaF,IAAa,uBAAb,cAA0C,QAAQ,SAAqC,CACrF,uDACD,CAAC;;;;;;AAOF,IAAa,aAAb,cAAgC,QAAQ,SAA2B,CACjE,6CACD,CAAC;;;;;;;AAQF,IAAa,qBAAb,cAAwC,QAAQ,SAAmC,CACjF,qDACD,CAAC;;;;;;;;AASF,IAAa,sBAAb,cAAyC,QAAQ,SAAoC,CACnF,sDACD,CAAC;;;;;AAUF,MAAa,UACX,YAEA,OAAO,QAAQ,QAAQ,UAAU,GAAG,MAAM,EAAE,OAAO,QAAQ,CAAC;;;;;AAM9D,MAAa,UACX,OAEA,OAAO,QAAQ,QAAQ,UAAU,GAAG,MAAM,EAAE,OAAO,GAAG,CAAC;;AAGzD,MAAa,OAIT,OAAO,QAAQ,QAAQ,UAAU,GAAG,MAAM,EAAE,KAAK;;;;;;AAOrD,MAAa,WAAW,OACtB,OAAO,QAAQ,QAAQ,UAAU,GAAG,MAAM,EAAE,QAAQ,GAAG,CAAC;;;;;;;AAQ1D,MAAa,YACX,MACA,SAEA,OAAO,IAAI,aAAa;CACtB,MAAM,IAAI,OAAO,QAAQ,UAAU;AACnC,QAAO,iBAAiB,UAAU;AAClC,QAAO,OAAO,EAAE,UAAU,OAAO,MAAM,KAAK;EAC5C;;;;AAKJ,MAAa,gBACX,MACA,YAEA,OAAO,IAAI,aAAa;CACtB,MAAM,IAAI,OAAO,QAAQ,UAAU;AACnC,QAAO,eAAe,UAAU;AAChC,QAAO,OAAO,EAAE,QAAQ,OAAO,MAAM,QAAQ;EAC7C;;AAGJ,MAAa,iBACX,OAEA,OAAO,IAAI,aAAa;CACtB,MAAM,IAAI,OAAO,QAAQ,UAAU;AACnC,QAAO,eAAe,UAAU;AAChC,QAAO,OAAO,EAAE,QAAQ,QAAQ,GAAG;EACnC;;AAGJ,MAAa,cAIT,OAAO,IAAI,aAAa;CAC1B,MAAM,IAAI,OAAO,QAAQ,UAAU;AACnC,QAAO,eAAe,UAAU;AAChC,QAAO,OAAO,EAAE,QAAQ;EACxB;;AAGF,MAAa,mBACX,OAEA,OAAO,IAAI,aAAa;CACtB,MAAM,IAAI,OAAO,QAAQ,UAAU;AACnC,QAAO,iBAAiB,UAAU;AAClC,QAAO,OAAO,EAAE,UAAU,QAAQ,GAAG;EACrC;;AAGJ,MAAa,gBAIT,OAAO,IAAI,aAAa;CAC1B,MAAM,IAAI,OAAO,QAAQ,UAAU;AACnC,QAAO,iBAAiB,UAAU;AAClC,QAAO,OAAO,EAAE,UAAU;EAC1B;;;;;;;;AASF,MAAa,cACX,UACA,SAMA,OAAO,IAAI,aAAa;CACtB,MAAM,IAAI,OAAO,QAAQ,UAAU;AACnC,QAAO,oBAAoB,UAAU;AACrC,QAAO,OAAO,EAAE,MAAM,OAAO,UAAU,KAAK;EAC5C"}
|
|
1
|
+
{"version":3,"file":"Sandbox.mjs","names":[],"sources":["../../src/sandbox/Sandbox.ts"],"sourcesContent":["import { Brand, Context, Data, type Duration, Effect, Redacted, Scope, Stream } from \"effect\"\nimport type * as SandboxError from \"./SandboxError.js\"\n\n// ---------------------------------------------------------------------------\n// Identity\n// ---------------------------------------------------------------------------\n\n/**\n * Sandbox identifier. Branded `string` so providers don't accidentally\n * accept a raw user-supplied id. Each provider mints these per its own\n * naming scheme (uuid for E2B/Vercel, user-supplied name for\n * Microsandbox/Modal, …); the core code treats them opaquely.\n *\n * Mint via the value-level `SandboxId(...)` smart constructor instead\n * of `as SandboxId` casts.\n */\nexport type SandboxId = Brand.Branded<string, \"SandboxId\">\nexport const SandboxId = Brand.nominal<SandboxId>()\n\n/**\n * Reference returned by `list`. Carries just enough to call `attach` —\n * provider-specific metadata (createdAt, region, tags, …) lives on the\n * narrowed per-provider ref.\n */\nexport type SandboxRef = {\n readonly id: SandboxId\n readonly name?: string\n}\n\nexport type VolumeId = Brand.Branded<string, \"VolumeId\">\nexport const VolumeId = Brand.nominal<VolumeId>()\n\nexport type SnapshotId = Brand.Branded<string, \"SnapshotId\">\nexport const SnapshotId = Brand.nominal<SnapshotId>()\n\n// ---------------------------------------------------------------------------\n// Image / runtime selection\n// ---------------------------------------------------------------------------\n\n/**\n * What runtime / filesystem to boot the sandbox from.\n *\n * - `Default` — provider picks its house image (Vercel: Amazon Linux\n * 2023; Microsandbox: requires user image but a builder default may\n * apply, …). Use when you don't care what's pre-installed.\n * - `Registry` — an OCI image reference. Supported by every provider\n * except those with a fixed base (Vercel ignores this and the\n * request-schema rejects at decode).\n * - `Snapshot` — restore from a captured state. Gated by\n * {@link SandboxSnapshots} at the call site — the marker is provided\n * by adapters whose provider supports snapshot-as-image.\n * - `Dockerfile` — provider builds a custom image from a Dockerfile.\n * Gated by {@link SandboxCustomImage}.\n */\nexport type ImageRef = Data.TaggedEnum<{\n Default: {}\n Registry: { readonly ref: string }\n Snapshot: { readonly id: SnapshotId }\n Dockerfile: { readonly contents: string }\n}>\n\nexport const ImageRef = Data.taggedEnum<ImageRef>()\n\n// ---------------------------------------------------------------------------\n// Network policy + secrets\n// ---------------------------------------------------------------------------\n\n/**\n * Outbound network policy for a sandbox.\n *\n * - `Open` — no egress restriction. The provider's defaults apply\n * (e.g. Microsandbox's \"public-only\" still blocks private ranges).\n * - `Blocked` — fully airgapped, no outbound connectivity at all.\n * - `Allowlist` — only the listed hosts / CIDRs may be reached.\n *\n * Providers vary in what they honour: hostname-allowlist requires\n * {@link SandboxHostnameAllowlist}; CIDR-only providers ignore `hosts`\n * and the adapter rejects at decode if `hosts` is non-empty.\n */\nexport type NetworkPolicy = Data.TaggedEnum<{\n Open: {}\n Blocked: {}\n Allowlist: {\n readonly hosts?: ReadonlyArray<string>\n readonly cidrs?: ReadonlyArray<string>\n }\n}>\n\nexport const NetworkPolicy = Data.taggedEnum<NetworkPolicy>()\n\n/**\n * A secret bound to a sandbox at create time. The code inside the\n * sandbox sees only a placeholder (`$MSB_<NAME>` for Microsandbox,\n * `{name}` for Vercel header-injection, etc.); the real `value` lives\n * on the provider's proxy and is substituted on outbound HTTPS to\n * `hosts`.\n *\n * Requires the {@link SandboxSecretInjection} capability marker —\n * provider Layers without proxy-layer secret rewriting do not ship the\n * marker and consumer code fails at `Effect.provide` with a type\n * error.\n *\n * `header` is the request header to inject (default\n * `\"Authorization: Bearer <value>\"`). Providers that do not let\n * callers pick the header (Microsandbox's `secretEnv` shorthand)\n * silently ignore this field; providers that require an explicit\n * header (Vercel's `transform.headers`) fail at decode if it's absent.\n */\nexport type BoundSecret = {\n readonly name: string\n readonly value: Redacted.Redacted<string>\n readonly hosts: ReadonlyArray<string>\n readonly header?: string\n}\n\n// ---------------------------------------------------------------------------\n// Volumes (capability-gated on `SandboxVolumes`)\n// ---------------------------------------------------------------------------\n\nexport type VolumeMount = {\n readonly id: VolumeId\n readonly mountPath: string\n readonly readonly?: boolean\n}\n\n// ---------------------------------------------------------------------------\n// Create / exec request shapes\n// ---------------------------------------------------------------------------\n\n/**\n * Cross-provider create request. Provider-specific knobs (region,\n * cpus, memory, template id, …) live on each provider's narrowed\n * request type which extends this.\n *\n * Fields a provider can't honour fail at **decode time** (per the\n * per-provider request schema), not silently at runtime. The\n * ComputeSDK anti-pattern of \"drop unsupported fields\" is the\n * footgun we're avoiding.\n */\nexport type CommonCreateRequest = {\n /** Image / snapshot to boot from. Omit for the provider's default. */\n readonly image?: ImageRef\n /**\n * Idle / hard timeout. Accepts a number (millis), a string like\n * `\"30 seconds\"`, or a `Duration`. Providers default differently.\n */\n readonly timeout?: Duration.Input\n /**\n * Env vars exposed to the sandbox. NOT for secrets you want hidden\n * from the running code — use {@link BoundSecret} instead.\n */\n readonly env?: Readonly<Record<string, string>>\n /** Outbound network policy. */\n readonly network?: NetworkPolicy\n /**\n * Bound secrets — code inside the sandbox sees a placeholder; the\n * proxy substitutes the real value on outbound HTTPS to listed hosts.\n * Capability-gated on {@link SandboxSecretInjection}.\n */\n readonly secrets?: ReadonlyArray<BoundSecret>\n /**\n * Persistent volumes to mount. Volumes are created via\n * `Sandbox.volumes.create` and survive sandbox lifetime.\n * Capability-gated on {@link SandboxVolumes}.\n */\n readonly volumes?: ReadonlyArray<VolumeMount>\n}\n\n/**\n * Cross-provider exec request. Background / long-running processes\n * use `spawn` instead of `exec`; the semantics differ enough that\n * overloading `exec` with a `detached` flag was rejected during\n * design.\n */\nexport type CommonExecRequest = {\n /**\n * Argv-style command. Pass a string for \"run this through the\n * shell\" semantics (provider-dependent — most use `sh -c`); pass\n * an array for direct argv with no shell parsing.\n */\n readonly cmd: string | ReadonlyArray<string>\n readonly cwd?: string\n readonly env?: Readonly<Record<string, string>>\n readonly stdin?: string | Uint8Array\n /**\n * Per-exec timeout. Accepts a number (millis), a string like\n * `\"5 seconds\"`, or a `Duration`.\n */\n readonly timeout?: Duration.Input\n}\n\n/**\n * Spawn request — extends {@link CommonExecRequest} with the option\n * to provide stdin as a `Stream<Uint8Array>`. The provider takes one\n * chunk at a time and sends EOF when the stream completes. Useful\n * for piping LLM output into a sandboxed process, or feeding stdin\n * from another stream source. `exec` does not accept streams.\n */\nexport type CommonSpawnRequest = Omit<CommonExecRequest, \"stdin\"> & {\n readonly stdin?: string | Uint8Array | Stream.Stream<Uint8Array, never, never>\n}\n\nexport type ExecResult = {\n readonly exitCode: number\n readonly stdout: string\n readonly stderr: string\n readonly durationMs: number\n}\n\n/**\n * Streaming exec output. `Complete` is terminal — the stream\n * completes after emitting it.\n */\nexport type ExecEvent = Data.TaggedEnum<{\n Stdout: { readonly chunk: Uint8Array }\n Stderr: { readonly chunk: Uint8Array }\n Complete: { readonly exitCode: number; readonly durationMs: number }\n}>\n\nexport const ExecEvent = Data.taggedEnum<ExecEvent>()\n\n// ---------------------------------------------------------------------------\n// Filesystem\n// ---------------------------------------------------------------------------\n\nexport type FileEntryKind = \"file\" | \"directory\" | \"symlink\" | \"other\"\n\nexport type FileEntry = {\n readonly path: string\n readonly kind: FileEntryKind\n}\n\nexport type SandboxFilesystem = {\n readonly read: (path: string) => Effect.Effect<Uint8Array, SandboxError.SandboxError>\n readonly write: (\n path: string,\n contents: Uint8Array | string,\n ) => Effect.Effect<void, SandboxError.SandboxError>\n readonly remove: (path: string) => Effect.Effect<void, SandboxError.SandboxError>\n readonly mkdir: (path: string) => Effect.Effect<void, SandboxError.SandboxError>\n readonly list: (\n path: string,\n ) => Effect.Effect<ReadonlyArray<FileEntry>, SandboxError.SandboxError>\n readonly exists: (path: string) => Effect.Effect<boolean, SandboxError.SandboxError>\n}\n\n// ---------------------------------------------------------------------------\n// Process handle (returned by `spawn`)\n// ---------------------------------------------------------------------------\n\n/**\n * Long-running process handle. Returned by `spawn` in a `Scope.Scope`\n * — the process is killed on scope close. To outlive the calling\n * scope, do `Effect.forkScoped` against a wider scope or use\n * `attach`-style reconnection per the provider's escape hatch.\n */\nexport type ProcessHandle = {\n readonly pid: number\n readonly events: Stream.Stream<ExecEvent, SandboxError.SandboxError>\n readonly kill: Effect.Effect<void, SandboxError.SandboxError>\n readonly exit: Effect.Effect<{ readonly exitCode: number }, SandboxError.SandboxError>\n}\n\n// ---------------------------------------------------------------------------\n// SandboxInstance — the live handle returned from `create` / `attach`\n// ---------------------------------------------------------------------------\n\n/**\n * Live sandbox handle. Methods are plain `Effect` / `Stream` values\n * — no nullary thunks (`destroy()`, `pause()`) — to match Effect's\n * `Queue.shutdown` / `Fiber.interrupt` style. Destruction is the\n * scope finalizer, not a method here; for the rare \"kill from\n * another fiber\" case use the service-level escape hatch\n * `Sandbox.destroy(id)`.\n */\nexport type SandboxInstance = {\n readonly id: SandboxId\n readonly exec: (\n request: CommonExecRequest,\n ) => Effect.Effect<ExecResult, SandboxError.SandboxError>\n readonly execStream: (\n request: CommonExecRequest,\n ) => Stream.Stream<ExecEvent, SandboxError.SandboxError>\n /**\n * Spawn a background / long-running process. The returned handle is\n * scoped — the process is killed on scope close. Use this for\n * \"dev server / watcher / tail -f\" workloads. For one-shot\n * commands prefer `exec`.\n */\n readonly spawn: (\n request: CommonSpawnRequest,\n ) => Effect.Effect<ProcessHandle, SandboxError.SandboxError, Scope.Scope>\n readonly files: SandboxFilesystem\n}\n\n// ---------------------------------------------------------------------------\n// Snapshot / volume / port sub-services (capability-gated)\n// ---------------------------------------------------------------------------\n\nexport type SandboxSnapshotsApi = {\n readonly create: (\n from: SandboxInstance,\n name?: string,\n ) => Effect.Effect<SnapshotId, SandboxError.SandboxError>\n readonly destroy: (id: SnapshotId) => Effect.Effect<void, SandboxError.SandboxError>\n readonly list: Effect.Effect<\n ReadonlyArray<{ readonly id: SnapshotId; readonly name?: string }>,\n SandboxError.SandboxError\n >\n}\n\nexport type SandboxVolumesApi = {\n readonly create: (\n name: string,\n options?: { readonly quotaBytes?: number },\n ) => Effect.Effect<VolumeId, SandboxError.SandboxError>\n readonly destroy: (id: VolumeId) => Effect.Effect<void, SandboxError.SandboxError>\n readonly list: Effect.Effect<\n ReadonlyArray<{ readonly id: VolumeId; readonly name: string }>,\n SandboxError.SandboxError\n >\n}\n\nexport type SandboxPortsApi = {\n /**\n * Expose an internal sandbox port as a publicly reachable URL.\n * Gated by {@link SandboxPortExposure} on the free helper; adapters\n * that don't ship the marker leave this sub-API out of the public\n * surface entirely (callers can't construct an Effect against it).\n */\n readonly expose: (\n instance: SandboxInstance,\n port: number,\n ) => Effect.Effect<{ readonly url: string }, SandboxError.SandboxError>\n}\n\n// ---------------------------------------------------------------------------\n// Service definition\n// ---------------------------------------------------------------------------\n\n/**\n * Cross-provider sandbox service.\n *\n * Lifetime model: `create` returns a sandbox handle in `Scope.Scope`.\n * The scope finalizer destroys the sandbox. Use `Effect.scoped` to\n * bound a sandbox's lifetime to a single Effect, or manage the scope\n * explicitly via `Scope.make` for sandboxes that span many calls.\n * This is why there's no `destroy()` method on `SandboxInstance` —\n * the scope is the lifecycle handle.\n *\n * `snapshots` and `volumes` sub-APIs are present on the service but\n * are still capability-gated: provider Layers that don't support\n * them ship implementations that always fail with\n * `SandboxUnsupported`. The free helpers ({@link snapshot},\n * {@link createVolume}) additionally require the corresponding\n * capability marker on R, so misuses surface as type errors at\n * `Effect.provide`, not as runtime failures.\n */\nexport type SandboxService = {\n /**\n * Provision a new sandbox. The returned handle's lifetime is\n * bound to the caller's scope; the sandbox is destroyed on scope\n * close. To survive the scope, use `attach`-style reconnection or\n * promote into a wider scope.\n */\n readonly create: (\n request: CommonCreateRequest,\n ) => Effect.Effect<SandboxInstance, SandboxError.SandboxError, Scope.Scope>\n\n /**\n * Re-acquire an existing sandbox by id. The returned handle is\n * scope-bound but the finalizer is \"detach\" (no destroy) —\n * appropriate for paused / detached / persistent sandboxes that\n * outlive any single agent run.\n */\n readonly attach: (\n id: SandboxId,\n ) => Effect.Effect<SandboxInstance, SandboxError.SandboxError, Scope.Scope>\n\n /**\n * Enumerate sandboxes visible to the configured account / project.\n */\n readonly list: Effect.Effect<ReadonlyArray<SandboxRef>, SandboxError.SandboxError>\n\n /**\n * Escape hatch for destroying a sandbox from outside its owning\n * scope. Most callers should let the scope finalizer handle this.\n */\n readonly destroy: (id: SandboxId) => Effect.Effect<void, SandboxError.SandboxError>\n\n /** Snapshot management. Capability-gated on {@link SandboxSnapshots}. */\n readonly snapshots: SandboxSnapshotsApi\n\n /** Volume management. Capability-gated on {@link SandboxVolumes}. */\n readonly volumes: SandboxVolumesApi\n\n /** Port exposure. Capability-gated on {@link SandboxPortExposure}. */\n readonly ports: SandboxPortsApi\n}\n\nexport class Sandbox extends Context.Service<Sandbox, SandboxService>()(\n \"@betalyra/effect-uai/Sandbox\",\n) {}\n\n// ---------------------------------------------------------------------------\n// Capability markers\n// ---------------------------------------------------------------------------\n\n/**\n * Capability marker — provided by adapter Layers whose provider\n * supports a hostname-level egress allowlist (matching against host\n * names / wildcards, not just CIDRs). Vercel, Cloudflare, Deno,\n * Microsandbox, Runloop, Anthropic srt ship this marker; Modal and\n * Daytona (CIDR-only) do not.\n *\n * Phantom — the value is `void`; adapters register with\n * `Layer.succeed(SandboxHostnameAllowlist, undefined)`.\n */\nexport class SandboxHostnameAllowlist extends Context.Service<SandboxHostnameAllowlist, void>()(\n \"@betalyra/effect-uai/capability/SandboxHostnameAllowlist\",\n) {}\n\n/**\n * Capability marker — adapter's provider supports proxy-layer\n * secret rewriting (header injection on outbound HTTPS to bound\n * hosts; the running code never sees the real value). Deno,\n * Cloudflare, E2B, Vercel (firewall transform), Microsandbox,\n * Runloop (Agent Gateway) ship this. Modal, Daytona, CodeSandbox\n * do not.\n */\nexport class SandboxSecretInjection extends Context.Service<SandboxSecretInjection, void>()(\n \"@betalyra/effect-uai/capability/SandboxSecretInjection\",\n) {}\n\n/**\n * Capability marker — adapter supports snapshot-as-new-sandbox\n * (capture state, restore as a derived sandbox with a fresh id).\n * Distinct from {@link SandboxPauseResume}, which preserves the\n * same id and resumes in place.\n */\nexport class SandboxSnapshots extends Context.Service<SandboxSnapshots, void>()(\n \"@betalyra/effect-uai/capability/SandboxSnapshots\",\n) {}\n\n/**\n * Capability marker — adapter supports in-place pause and resume on\n * the same sandbox id (E2B, CodeSandbox). Memory + processes are\n * preserved across the pause boundary. Most providers without this\n * marker offer {@link SandboxSnapshots} instead.\n */\nexport class SandboxPauseResume extends Context.Service<SandboxPauseResume, void>()(\n \"@betalyra/effect-uai/capability/SandboxPauseResume\",\n) {}\n\n/**\n * Capability marker — adapter manages named persistent volumes\n * outside any single sandbox lifecycle (Modal Volume, Daytona,\n * Microsandbox, BoxLite). Providers without this marker leave\n * `Sandbox.volumes.*` returning `SandboxUnsupported`; the free\n * helpers ({@link createVolume}, {@link destroyVolume}) additionally\n * require this marker on R so misuses become a type error.\n */\nexport class SandboxVolumes extends Context.Service<SandboxVolumes, void>()(\n \"@betalyra/effect-uai/capability/SandboxVolumes\",\n) {}\n\n/**\n * Capability marker — adapter supports a stateful kernel session\n * (Jupyter-style: persistent Python / JS context that returns rich\n * outputs across many `runCode` calls). Cloudflare Code Interpreter\n * and E2B ship this; everyone else expects you to write your own\n * script and `exec` it.\n *\n * The actual `runCode` helper lives separately (not on\n * `SandboxInstance`) because the return shape is different — rich\n * outputs with images/tables, not buffered stdout strings.\n */\nexport class SandboxKernelSession extends Context.Service<SandboxKernelSession, void>()(\n \"@betalyra/effect-uai/capability/SandboxKernelSession\",\n) {}\n\n/**\n * Capability marker — adapter exposes an interactive PTY session\n * (Runloop, Cloudflare terminal). Distinct from `execStream` which\n * is byte-oriented and one-shot.\n */\nexport class SandboxPty extends Context.Service<SandboxPty, void>()(\n \"@betalyra/effect-uai/capability/SandboxPty\",\n) {}\n\n/**\n * Capability marker — adapter accepts a user-supplied Dockerfile (or\n * OCI image build) instead of just a registry reference. Vercel does\n * not ship this (fixed AL2023). Deno does not (snapshots only).\n * Most others do.\n */\nexport class SandboxCustomImage extends Context.Service<SandboxCustomImage, void>()(\n \"@betalyra/effect-uai/capability/SandboxCustomImage\",\n) {}\n\n/**\n * Capability marker — adapter can expose an internal port as a\n * publicly reachable URL. Most cloud providers ship this; local\n * providers (Microsandbox, BoxLite) typically only support\n * host-side port forwarding (handled by ignoring this marker and\n * documenting via the per-provider request type).\n */\nexport class SandboxPortExposure extends Context.Service<SandboxPortExposure, void>()(\n \"@betalyra/effect-uai/capability/SandboxPortExposure\",\n) {}\n\n// ---------------------------------------------------------------------------\n// Free helpers — accept Sandbox via Context, optionally require markers.\n// ---------------------------------------------------------------------------\n\n/**\n * Provision a sandbox bound to the caller's scope. Drop the\n * `Scope.Scope` requirement with `Effect.scoped`.\n */\nexport const create = (\n request: CommonCreateRequest,\n): Effect.Effect<SandboxInstance, SandboxError.SandboxError, Sandbox | Scope.Scope> =>\n Effect.flatMap(Sandbox, (s) => s.create(request))\n\n/**\n * Re-acquire an existing sandbox by id. Same scope semantics as\n * `create`, but the finalizer detaches rather than destroying.\n */\nexport const attach = (\n id: SandboxId,\n): Effect.Effect<SandboxInstance, SandboxError.SandboxError, Sandbox | Scope.Scope> =>\n Effect.flatMap(Sandbox, (s) => s.attach(id))\n\n/** Enumerate sandboxes for the configured account / project. */\nexport const list: Effect.Effect<\n ReadonlyArray<SandboxRef>,\n SandboxError.SandboxError,\n Sandbox\n> = Effect.flatMap(Sandbox, (s) => s.list)\n\n/**\n * Escape hatch for destroying a sandbox from outside its owning\n * scope. Most callers should let the scope finalizer handle this\n * automatically.\n */\nexport const destroy = (id: SandboxId): Effect.Effect<void, SandboxError.SandboxError, Sandbox> =>\n Effect.flatMap(Sandbox, (s) => s.destroy(id))\n\n/**\n * Create a snapshot of a live sandbox. Requires\n * {@link SandboxSnapshots} on the R channel — adapters whose\n * provider doesn't support snapshot-as-new-sandbox don't ship the\n * marker and calls fail at `Effect.provide` with a type error.\n */\nexport const snapshot = (\n from: SandboxInstance,\n name?: string,\n): Effect.Effect<SnapshotId, SandboxError.SandboxError, Sandbox | SandboxSnapshots> =>\n Effect.gen(function* () {\n const s = yield* Sandbox\n yield* SandboxSnapshots\n return yield* s.snapshots.create(from, name)\n })\n\n/**\n * Create a named persistent volume. Requires {@link SandboxVolumes}.\n */\nexport const createVolume = (\n name: string,\n options?: { readonly quotaBytes?: number },\n): Effect.Effect<VolumeId, SandboxError.SandboxError, Sandbox | SandboxVolumes> =>\n Effect.gen(function* () {\n const s = yield* Sandbox\n yield* SandboxVolumes\n return yield* s.volumes.create(name, options)\n })\n\n/** Destroy a named volume. Requires {@link SandboxVolumes}. */\nexport const destroyVolume = (\n id: VolumeId,\n): Effect.Effect<void, SandboxError.SandboxError, Sandbox | SandboxVolumes> =>\n Effect.gen(function* () {\n const s = yield* Sandbox\n yield* SandboxVolumes\n return yield* s.volumes.destroy(id)\n })\n\n/** Enumerate persistent volumes. Requires {@link SandboxVolumes}. */\nexport const listVolumes: Effect.Effect<\n ReadonlyArray<{ readonly id: VolumeId; readonly name: string }>,\n SandboxError.SandboxError,\n Sandbox | SandboxVolumes\n> = Effect.gen(function* () {\n const s = yield* Sandbox\n yield* SandboxVolumes\n return yield* s.volumes.list\n})\n\n/** Destroy a snapshot. Requires {@link SandboxSnapshots}. */\nexport const destroySnapshot = (\n id: SnapshotId,\n): Effect.Effect<void, SandboxError.SandboxError, Sandbox | SandboxSnapshots> =>\n Effect.gen(function* () {\n const s = yield* Sandbox\n yield* SandboxSnapshots\n return yield* s.snapshots.destroy(id)\n })\n\n/** Enumerate snapshots. Requires {@link SandboxSnapshots}. */\nexport const listSnapshots: Effect.Effect<\n ReadonlyArray<{ readonly id: SnapshotId; readonly name?: string }>,\n SandboxError.SandboxError,\n Sandbox | SandboxSnapshots\n> = Effect.gen(function* () {\n const s = yield* Sandbox\n yield* SandboxSnapshots\n return yield* s.snapshots.list\n})\n\n/**\n * Expose an internal port of a live sandbox as a publicly reachable\n * URL. Requires {@link SandboxPortExposure} — adapters whose provider\n * doesn't support runtime port exposure (e.g. Microsandbox, which\n * forwards at create time only) don't ship the marker, and calls\n * fail at `Effect.provide` with a type error.\n */\nexport const exposePort = (\n instance: SandboxInstance,\n port: number,\n): Effect.Effect<\n { readonly url: string },\n SandboxError.SandboxError,\n Sandbox | SandboxPortExposure\n> =>\n Effect.gen(function* () {\n const s = yield* Sandbox\n yield* SandboxPortExposure\n return yield* s.ports.expose(instance, port)\n })\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiBA,MAAa,YAAY,MAAM,SAAoB;AAanD,MAAa,WAAW,MAAM,SAAmB;AAGjD,MAAa,aAAa,MAAM,SAAqB;AA4BrD,MAAa,WAAW,KAAK,YAAsB;AA2BnD,MAAa,gBAAgB,KAAK,YAA2B;AAmI7D,MAAa,YAAY,KAAK,YAAuB;AAqLrD,IAAa,UAAb,cAA6B,QAAQ,SAAkC,CACrE,+BACD,CAAC;;;;;;;;;;;AAgBF,IAAa,2BAAb,cAA8C,QAAQ,SAAyC,CAC7F,2DACD,CAAC;;;;;;;;;AAUF,IAAa,yBAAb,cAA4C,QAAQ,SAAuC,CACzF,yDACD,CAAC;;;;;;;AAQF,IAAa,mBAAb,cAAsC,QAAQ,SAAiC,CAC7E,mDACD,CAAC;;;;;;;AAQF,IAAa,qBAAb,cAAwC,QAAQ,SAAmC,CACjF,qDACD,CAAC;;;;;;;;;AAUF,IAAa,iBAAb,cAAoC,QAAQ,SAA+B,CACzE,iDACD,CAAC;;;;;;;;;;;;AAaF,IAAa,uBAAb,cAA0C,QAAQ,SAAqC,CACrF,uDACD,CAAC;;;;;;AAOF,IAAa,aAAb,cAAgC,QAAQ,SAA2B,CACjE,6CACD,CAAC;;;;;;;AAQF,IAAa,qBAAb,cAAwC,QAAQ,SAAmC,CACjF,qDACD,CAAC;;;;;;;;AASF,IAAa,sBAAb,cAAyC,QAAQ,SAAoC,CACnF,sDACD,CAAC;;;;;AAUF,MAAa,UACX,YAEA,OAAO,QAAQ,UAAU,MAAM,EAAE,OAAO,QAAQ,CAAC;;;;;AAMnD,MAAa,UACX,OAEA,OAAO,QAAQ,UAAU,MAAM,EAAE,OAAO,GAAG,CAAC;;AAG9C,MAAa,OAIT,OAAO,QAAQ,UAAU,MAAM,EAAE,KAAK;;;;;;AAO1C,MAAa,WAAW,OACtB,OAAO,QAAQ,UAAU,MAAM,EAAE,QAAQ,GAAG,CAAC;;;;;;;AAQ/C,MAAa,YACX,MACA,SAEA,OAAO,IAAI,aAAa;CACtB,MAAM,IAAI,OAAO;AACjB,QAAO;AACP,QAAO,OAAO,EAAE,UAAU,OAAO,MAAM,KAAK;EAC5C;;;;AAKJ,MAAa,gBACX,MACA,YAEA,OAAO,IAAI,aAAa;CACtB,MAAM,IAAI,OAAO;AACjB,QAAO;AACP,QAAO,OAAO,EAAE,QAAQ,OAAO,MAAM,QAAQ;EAC7C;;AAGJ,MAAa,iBACX,OAEA,OAAO,IAAI,aAAa;CACtB,MAAM,IAAI,OAAO;AACjB,QAAO;AACP,QAAO,OAAO,EAAE,QAAQ,QAAQ,GAAG;EACnC;;AAGJ,MAAa,cAIT,OAAO,IAAI,aAAa;CAC1B,MAAM,IAAI,OAAO;AACjB,QAAO;AACP,QAAO,OAAO,EAAE,QAAQ;EACxB;;AAGF,MAAa,mBACX,OAEA,OAAO,IAAI,aAAa;CACtB,MAAM,IAAI,OAAO;AACjB,QAAO;AACP,QAAO,OAAO,EAAE,UAAU,QAAQ,GAAG;EACrC;;AAGJ,MAAa,gBAIT,OAAO,IAAI,aAAa;CAC1B,MAAM,IAAI,OAAO;AACjB,QAAO;AACP,QAAO,OAAO,EAAE,UAAU;EAC1B;;;;;;;;AASF,MAAa,cACX,UACA,SAMA,OAAO,IAAI,aAAa;CACtB,MAAM,IAAI,OAAO;AACjB,QAAO;AACP,QAAO,OAAO,EAAE,MAAM,OAAO,UAAU,KAAK;EAC5C"}
|
|
@@ -34,9 +34,9 @@ var TtsIncrementalText = class extends Context.Service()("@betalyra/effect-uai/c
|
|
|
34
34
|
*/
|
|
35
35
|
var MultiSpeakerTts = class extends Context.Service()("@betalyra/effect-uai/capability/MultiSpeakerTts") {};
|
|
36
36
|
/** One-shot synthesis. */
|
|
37
|
-
const synthesize = (request) => Effect.flatMap(SpeechSynthesizer
|
|
37
|
+
const synthesize = (request) => Effect.flatMap(SpeechSynthesizer, (s) => s.synthesize(request));
|
|
38
38
|
/** Full text in, audio chunks out. */
|
|
39
|
-
const streamSynthesis = (request) => Stream.unwrap(Effect.map(SpeechSynthesizer
|
|
39
|
+
const streamSynthesis = (request) => Stream.unwrap(Effect.map(SpeechSynthesizer, (s) => s.streamSynthesis(request)));
|
|
40
40
|
/**
|
|
41
41
|
* Incremental synthesis. Dual-arity: pipeable (data-last) and direct
|
|
42
42
|
* (data-first). Requires `TtsIncrementalText` in R — providers without
|
|
@@ -51,8 +51,8 @@ const streamSynthesis = (request) => Stream.unwrap(Effect.map(SpeechSynthesizer.
|
|
|
51
51
|
* ```
|
|
52
52
|
*/
|
|
53
53
|
const streamSynthesisFrom = Function.dual(2, (textIn, request) => Stream.unwrap(Effect.gen(function* () {
|
|
54
|
-
const s = yield* SpeechSynthesizer
|
|
55
|
-
yield* TtsIncrementalText
|
|
54
|
+
const s = yield* SpeechSynthesizer;
|
|
55
|
+
yield* TtsIncrementalText;
|
|
56
56
|
return s.streamSynthesisFrom(textIn, request);
|
|
57
57
|
})));
|
|
58
58
|
/**
|
|
@@ -60,16 +60,16 @@ const streamSynthesisFrom = Function.dual(2, (textIn, request) => Stream.unwrap(
|
|
|
60
60
|
* providers without dialogue support are a compile-time error.
|
|
61
61
|
*/
|
|
62
62
|
const synthesizeDialogue = (request) => Effect.gen(function* () {
|
|
63
|
-
const s = yield* SpeechSynthesizer
|
|
64
|
-
yield* MultiSpeakerTts
|
|
63
|
+
const s = yield* SpeechSynthesizer;
|
|
64
|
+
yield* MultiSpeakerTts;
|
|
65
65
|
return yield* s.synthesizeDialogue(request);
|
|
66
66
|
});
|
|
67
67
|
/**
|
|
68
68
|
* Chunked-streaming variant. Same R requirement as `synthesizeDialogue`.
|
|
69
69
|
*/
|
|
70
70
|
const streamSynthesizeDialogue = (request) => Stream.unwrap(Effect.gen(function* () {
|
|
71
|
-
const s = yield* SpeechSynthesizer
|
|
72
|
-
yield* MultiSpeakerTts
|
|
71
|
+
const s = yield* SpeechSynthesizer;
|
|
72
|
+
yield* MultiSpeakerTts;
|
|
73
73
|
return s.streamSynthesizeDialogue(request);
|
|
74
74
|
}));
|
|
75
75
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SpeechSynthesizer.mjs","names":[],"sources":["../../src/speech-synthesizer/SpeechSynthesizer.ts"],"sourcesContent":["import { Context, Effect, Function, Stream } from \"effect\"\nimport * as AiError from \"../domain/AiError.js\"\nimport type { AudioBlob, AudioChunk, AudioFormat } from \"../domain/Audio.js\"\n\n/**\n * Per-phrase pronunciation override. `pronunciation` is always IPA, the\n * universal phonetic interchange format. Adapters translate to the\n * provider's wire form (inline `/ipa/` token, SSML `<phoneme>`, or a\n * structured field), converting IPA to X-SAMPA internally where the wire\n * requires it (Google, Azure).\n *\n * Pronunciations are load-bearing (the configured word is mispronounced\n * if dropped), so a provider with no stateless IPA path fails the call\n * with `AiError.Unsupported` rather than silently degrading.\n */\nexport type CustomPronunciation = {\n readonly phrase: string\n /** IPA notation, e.g. `\"ænˈθrɒpɪk\"`. */\n readonly pronunciation: string\n}\n\n/**\n * Cross-provider synthesis request. Provider-specific extensions\n * (ElevenLabs `stability` / `similarity_boost`, Cartesia `emotion`,\n * MiniMax `vol` / `pitch`, Azure SSML style tags) live on each\n * provider's typed request which extends this and narrows `model` and\n * `voiceId`.\n */\nexport type CommonSynthesizeRequest = {\n readonly text: string\n /** Model identifier. Each provider narrows. */\n readonly model: string\n /**\n * Voice identifier. Per-provider request types narrow this to a\n * typed literal union of stock voices + `(string & {})` escape for\n * custom cloned voice IDs. Providers without custom-voice support\n * (OpenAI, Deepgram Aura, AWS Polly) narrow to the stock-only union.\n */\n readonly voiceId: string\n readonly outputFormat?: AudioFormat\n readonly speed?: number\n readonly languageCode?: string\n /**\n * IPA phoneme overrides for specific phrases. Adapters render them to\n * the provider's wire form (inline token, SSML `<phoneme>`, structured\n * field). Load-bearing: a provider with no stateless IPA path fails\n * the call with `AiError.Unsupported` rather than mispronouncing.\n */\n readonly pronunciations?: ReadonlyArray<CustomPronunciation>\n}\n\n/**\n * Incremental-synthesis request — text arrives as `Stream<string>`.\n * Gated by the `TtsIncrementalText` capability marker; only providers\n * that ship the marker can be used.\n *\n * Multi-context features (Cartesia `context_id`, ElevenLabs `multi-\n * stream-input`) are NOT exposed here — one logical utterance per\n * call. Provider extensions can expose `forkContext` for that.\n */\nexport type CommonStreamSynthesizeRequest = Omit<CommonSynthesizeRequest, \"text\">\n\n/**\n * One turn in a multi-speaker dialogue. Just a voice and its line; the\n * only in-tree dialogue endpoint (ElevenLabs `/v1/text-to-dialogue`)\n * accepts `{ voice_id, text }` per turn. Per-turn styling knobs\n * (Hume Octave-2 `styleDescription` / `speed`) are provider-specific\n * and belong on a provider-typed turn extension, not here.\n */\nexport type DialogueTurn = {\n readonly voiceId: string\n readonly text: string\n}\n\n/**\n * Cross-provider dialogue request. Same return type as `synthesize` —\n * one continuous `AudioBlob`. Per-turn timing metadata is not exposed\n * in the common shape (only Hume returns it natively).\n */\nexport type CommonSynthesizeDialogueRequest = {\n readonly model: string\n readonly turns: ReadonlyArray<DialogueTurn>\n readonly outputFormat?: AudioFormat\n readonly languageCode?: string\n readonly pronunciations?: ReadonlyArray<CustomPronunciation>\n}\n\nexport type SpeechSynthesizerService = {\n /** One-shot. Full text in, full audio bytes out. Universally supported. */\n readonly synthesize: (\n request: CommonSynthesizeRequest,\n ) => Effect.Effect<AudioBlob, AiError.AiError>\n /**\n * Full text in, audio chunks streamed out (chunked HTTP). Universally\n * supported across providers that offer any streaming TTS at all.\n */\n readonly streamSynthesis: (\n request: CommonSynthesizeRequest,\n ) => Stream.Stream<AudioChunk, AiError.AiError>\n /**\n * Incremental text in (as a Stream), audio chunks streamed out. The\n * underlying WS connection is acquired on first pull and released\n * when the output stream is finalized via `Stream.scoped`.\n *\n * Gated by the `TtsIncrementalText` capability marker on the top-\n * level helper — providers without WS-style incremental input don't\n * ship the marker, so calls fail at `Effect.provide` with a type\n * error.\n */\n readonly streamSynthesisFrom: <E, R>(\n textIn: Stream.Stream<string, E, R>,\n request: CommonStreamSynthesizeRequest,\n ) => Stream.Stream<AudioChunk, AiError.AiError | E, R>\n /**\n * One-shot multi-speaker dialogue. Turn array in, single `AudioBlob`\n * out. Providers without native dialogue support return\n * `AiError.Unsupported` and do NOT ship the `MultiSpeakerTts` marker\n * — calls via the top-level helper become a compile-time error.\n */\n readonly synthesizeDialogue: (\n request: CommonSynthesizeDialogueRequest,\n ) => Effect.Effect<AudioBlob, AiError.AiError>\n /**\n * Chunked-streaming variant of `synthesizeDialogue`. Same input,\n * audio chunks emitted as the wire delivers them. Providers that\n * synthesize the whole dialogue server-side may fall back to a\n * single-chunk wrap of the sync result.\n *\n * Gated by `MultiSpeakerTts` at the top-level helper.\n */\n readonly streamSynthesizeDialogue: (\n request: CommonSynthesizeDialogueRequest,\n ) => Stream.Stream<AudioChunk, AiError.AiError>\n}\n\nexport class SpeechSynthesizer extends Context.Service<\n SpeechSynthesizer,\n SpeechSynthesizerService\n>()(\"@betalyra/effect-uai/SpeechSynthesizer\") {}\n\n/**\n * Capability marker — provided by provider layers whose\n * `streamSynthesisFrom` is wired up at the wire level. OpenAI, Azure\n * (wire), and AWS Polly non-Generative do not ship it. Calling\n * `streamSynthesisFrom` while only one of those Layers is in scope\n * fails at `Effect.provide` with a type error.\n *\n * Phantom — the value is `void`; providers register with\n * `Layer.succeed(TtsIncrementalText, undefined)`.\n */\nexport class TtsIncrementalText extends Context.Service<TtsIncrementalText, void>()(\n \"@betalyra/effect-uai/capability/TtsIncrementalText\",\n) {}\n\n/**\n * Capability marker for multi-speaker dialogue. Shipped by provider\n * Layers whose `synthesizeDialogue` is wired to a native multi-speaker\n * endpoint (ElevenLabs `/v1/text-to-dialogue`, Hume `/v0/tts` with\n * `utterances[]`, Google Cloud TTS Gemini TTS `multiSpeakerMarkup`).\n * Other providers leave the marker unregistered so the top-level\n * `synthesizeDialogue` / `streamSynthesizeDialogue` helpers fail to\n * satisfy R against those Layers — compile-time error.\n */\nexport class MultiSpeakerTts extends Context.Service<MultiSpeakerTts, void>()(\n \"@betalyra/effect-uai/capability/MultiSpeakerTts\",\n) {}\n\n/** One-shot synthesis. */\nexport const synthesize = (\n request: CommonSynthesizeRequest,\n): Effect.Effect<AudioBlob, AiError.AiError, SpeechSynthesizer> =>\n Effect.flatMap(SpeechSynthesizer.asEffect(), (s) => s.synthesize(request))\n\n/** Full text in, audio chunks out. */\nexport const streamSynthesis = (\n request: CommonSynthesizeRequest,\n): Stream.Stream<AudioChunk, AiError.AiError, SpeechSynthesizer> =>\n Stream.unwrap(Effect.map(SpeechSynthesizer.asEffect(), (s) => s.streamSynthesis(request)))\n\n/**\n * Incremental synthesis. Dual-arity: pipeable (data-last) and direct\n * (data-first). Requires `TtsIncrementalText` in R — providers without\n * incremental-text-in support are a type error at provide time.\n *\n * @example\n * ```ts\n * const audio = LanguageModel.streamTurn(turnReq).pipe(\n * Stream.filterMap(Turn.toTextDelta),\n * SpeechSynthesizer.streamSynthesisFrom(synthReq),\n * )\n * ```\n */\nexport const streamSynthesisFrom: {\n (\n request: CommonStreamSynthesizeRequest,\n ): <E, R>(\n textIn: Stream.Stream<string, E, R>,\n ) => Stream.Stream<AudioChunk, AiError.AiError | E, R | SpeechSynthesizer | TtsIncrementalText>\n <E, R>(\n textIn: Stream.Stream<string, E, R>,\n request: CommonStreamSynthesizeRequest,\n ): Stream.Stream<AudioChunk, AiError.AiError | E, R | SpeechSynthesizer | TtsIncrementalText>\n} = Function.dual(\n 2,\n <E, R>(textIn: Stream.Stream<string, E, R>, request: CommonStreamSynthesizeRequest) =>\n Stream.unwrap(\n Effect.gen(function* () {\n const s = yield* SpeechSynthesizer.asEffect()\n yield* TtsIncrementalText.asEffect()\n return s.streamSynthesisFrom(textIn, request)\n }),\n ),\n)\n\n/**\n * One-shot multi-speaker dialogue. Requires `MultiSpeakerTts` in R —\n * providers without dialogue support are a compile-time error.\n */\nexport const synthesizeDialogue = (\n request: CommonSynthesizeDialogueRequest,\n): Effect.Effect<AudioBlob, AiError.AiError, SpeechSynthesizer | MultiSpeakerTts> =>\n Effect.gen(function* () {\n const s = yield* SpeechSynthesizer.asEffect()\n yield* MultiSpeakerTts.asEffect()\n return yield* s.synthesizeDialogue(request)\n })\n\n/**\n * Chunked-streaming variant. Same R requirement as `synthesizeDialogue`.\n */\nexport const streamSynthesizeDialogue = (\n request: CommonSynthesizeDialogueRequest,\n): Stream.Stream<AudioChunk, AiError.AiError, SpeechSynthesizer | MultiSpeakerTts> =>\n Stream.unwrap(\n Effect.gen(function* () {\n const s = yield* SpeechSynthesizer.asEffect()\n yield* MultiSpeakerTts.asEffect()\n return s.streamSynthesizeDialogue(request)\n }),\n )\n"],"mappings":";;;;;;;;;;;;;AAuIA,IAAa,oBAAb,cAAuC,QAAQ,SAG5C,CAAC,yCAAyC,CAAC;;;;;;;;;;;AAY9C,IAAa,qBAAb,cAAwC,QAAQ,SAAmC,CACjF,qDACD,CAAC;;;;;;;;;;AAWF,IAAa,kBAAb,cAAqC,QAAQ,SAAgC,CAC3E,kDACD,CAAC;;AAGF,MAAa,cACX,YAEA,OAAO,QAAQ,kBAAkB,UAAU,GAAG,MAAM,EAAE,WAAW,QAAQ,CAAC;;AAG5E,MAAa,mBACX,YAEA,OAAO,OAAO,OAAO,IAAI,kBAAkB,UAAU,GAAG,MAAM,EAAE,gBAAgB,QAAQ,CAAC,CAAC;;;;;;;;;;;;;;AAe5F,MAAa,sBAUT,SAAS,KACX,IACO,QAAqC,YAC1C,OAAO,OACL,OAAO,IAAI,aAAa;CACtB,MAAM,IAAI,OAAO,kBAAkB,UAAU;AAC7C,QAAO,mBAAmB,UAAU;AACpC,QAAO,EAAE,oBAAoB,QAAQ,QAAQ;EAC7C,CACH,CACJ;;;;;AAMD,MAAa,sBACX,YAEA,OAAO,IAAI,aAAa;CACtB,MAAM,IAAI,OAAO,kBAAkB,UAAU;AAC7C,QAAO,gBAAgB,UAAU;AACjC,QAAO,OAAO,EAAE,mBAAmB,QAAQ;EAC3C;;;;AAKJ,MAAa,4BACX,YAEA,OAAO,OACL,OAAO,IAAI,aAAa;CACtB,MAAM,IAAI,OAAO,kBAAkB,UAAU;AAC7C,QAAO,gBAAgB,UAAU;AACjC,QAAO,EAAE,yBAAyB,QAAQ;EAC1C,CACH"}
|
|
1
|
+
{"version":3,"file":"SpeechSynthesizer.mjs","names":[],"sources":["../../src/speech-synthesizer/SpeechSynthesizer.ts"],"sourcesContent":["import { Context, Effect, Function, Stream } from \"effect\"\nimport * as AiError from \"../domain/AiError.js\"\nimport type { AudioBlob, AudioChunk, AudioFormat } from \"../domain/Audio.js\"\n\n/**\n * Per-phrase pronunciation override. `pronunciation` is always IPA, the\n * universal phonetic interchange format. Adapters translate to the\n * provider's wire form (inline `/ipa/` token, SSML `<phoneme>`, or a\n * structured field), converting IPA to X-SAMPA internally where the wire\n * requires it (Google, Azure).\n *\n * Pronunciations are load-bearing (the configured word is mispronounced\n * if dropped), so a provider with no stateless IPA path fails the call\n * with `AiError.Unsupported` rather than silently degrading.\n */\nexport type CustomPronunciation = {\n readonly phrase: string\n /** IPA notation, e.g. `\"ænˈθrɒpɪk\"`. */\n readonly pronunciation: string\n}\n\n/**\n * Cross-provider synthesis request. Provider-specific extensions\n * (ElevenLabs `stability` / `similarity_boost`, Cartesia `emotion`,\n * MiniMax `vol` / `pitch`, Azure SSML style tags) live on each\n * provider's typed request which extends this and narrows `model` and\n * `voiceId`.\n */\nexport type CommonSynthesizeRequest = {\n readonly text: string\n /** Model identifier. Each provider narrows. */\n readonly model: string\n /**\n * Voice identifier. Per-provider request types narrow this to a\n * typed literal union of stock voices + `(string & {})` escape for\n * custom cloned voice IDs. Providers without custom-voice support\n * (OpenAI, Deepgram Aura, AWS Polly) narrow to the stock-only union.\n */\n readonly voiceId: string\n readonly outputFormat?: AudioFormat\n readonly speed?: number\n readonly languageCode?: string\n /**\n * IPA phoneme overrides for specific phrases. Adapters render them to\n * the provider's wire form (inline token, SSML `<phoneme>`, structured\n * field). Load-bearing: a provider with no stateless IPA path fails\n * the call with `AiError.Unsupported` rather than mispronouncing.\n */\n readonly pronunciations?: ReadonlyArray<CustomPronunciation>\n}\n\n/**\n * Incremental-synthesis request — text arrives as `Stream<string>`.\n * Gated by the `TtsIncrementalText` capability marker; only providers\n * that ship the marker can be used.\n *\n * Multi-context features (Cartesia `context_id`, ElevenLabs `multi-\n * stream-input`) are NOT exposed here — one logical utterance per\n * call. Provider extensions can expose `forkContext` for that.\n */\nexport type CommonStreamSynthesizeRequest = Omit<CommonSynthesizeRequest, \"text\">\n\n/**\n * One turn in a multi-speaker dialogue. Just a voice and its line; the\n * only in-tree dialogue endpoint (ElevenLabs `/v1/text-to-dialogue`)\n * accepts `{ voice_id, text }` per turn. Per-turn styling knobs\n * (Hume Octave-2 `styleDescription` / `speed`) are provider-specific\n * and belong on a provider-typed turn extension, not here.\n */\nexport type DialogueTurn = {\n readonly voiceId: string\n readonly text: string\n}\n\n/**\n * Cross-provider dialogue request. Same return type as `synthesize` —\n * one continuous `AudioBlob`. Per-turn timing metadata is not exposed\n * in the common shape (only Hume returns it natively).\n */\nexport type CommonSynthesizeDialogueRequest = {\n readonly model: string\n readonly turns: ReadonlyArray<DialogueTurn>\n readonly outputFormat?: AudioFormat\n readonly languageCode?: string\n readonly pronunciations?: ReadonlyArray<CustomPronunciation>\n}\n\nexport type SpeechSynthesizerService = {\n /** One-shot. Full text in, full audio bytes out. Universally supported. */\n readonly synthesize: (\n request: CommonSynthesizeRequest,\n ) => Effect.Effect<AudioBlob, AiError.AiError>\n /**\n * Full text in, audio chunks streamed out (chunked HTTP). Universally\n * supported across providers that offer any streaming TTS at all.\n */\n readonly streamSynthesis: (\n request: CommonSynthesizeRequest,\n ) => Stream.Stream<AudioChunk, AiError.AiError>\n /**\n * Incremental text in (as a Stream), audio chunks streamed out. The\n * underlying WS connection is acquired on first pull and released\n * when the output stream is finalized via `Stream.scoped`.\n *\n * Gated by the `TtsIncrementalText` capability marker on the top-\n * level helper — providers without WS-style incremental input don't\n * ship the marker, so calls fail at `Effect.provide` with a type\n * error.\n */\n readonly streamSynthesisFrom: <E, R>(\n textIn: Stream.Stream<string, E, R>,\n request: CommonStreamSynthesizeRequest,\n ) => Stream.Stream<AudioChunk, AiError.AiError | E, R>\n /**\n * One-shot multi-speaker dialogue. Turn array in, single `AudioBlob`\n * out. Providers without native dialogue support return\n * `AiError.Unsupported` and do NOT ship the `MultiSpeakerTts` marker\n * — calls via the top-level helper become a compile-time error.\n */\n readonly synthesizeDialogue: (\n request: CommonSynthesizeDialogueRequest,\n ) => Effect.Effect<AudioBlob, AiError.AiError>\n /**\n * Chunked-streaming variant of `synthesizeDialogue`. Same input,\n * audio chunks emitted as the wire delivers them. Providers that\n * synthesize the whole dialogue server-side may fall back to a\n * single-chunk wrap of the sync result.\n *\n * Gated by `MultiSpeakerTts` at the top-level helper.\n */\n readonly streamSynthesizeDialogue: (\n request: CommonSynthesizeDialogueRequest,\n ) => Stream.Stream<AudioChunk, AiError.AiError>\n}\n\nexport class SpeechSynthesizer extends Context.Service<\n SpeechSynthesizer,\n SpeechSynthesizerService\n>()(\"@betalyra/effect-uai/SpeechSynthesizer\") {}\n\n/**\n * Capability marker — provided by provider layers whose\n * `streamSynthesisFrom` is wired up at the wire level. OpenAI, Azure\n * (wire), and AWS Polly non-Generative do not ship it. Calling\n * `streamSynthesisFrom` while only one of those Layers is in scope\n * fails at `Effect.provide` with a type error.\n *\n * Phantom — the value is `void`; providers register with\n * `Layer.succeed(TtsIncrementalText, undefined)`.\n */\nexport class TtsIncrementalText extends Context.Service<TtsIncrementalText, void>()(\n \"@betalyra/effect-uai/capability/TtsIncrementalText\",\n) {}\n\n/**\n * Capability marker for multi-speaker dialogue. Shipped by provider\n * Layers whose `synthesizeDialogue` is wired to a native multi-speaker\n * endpoint (ElevenLabs `/v1/text-to-dialogue`, Hume `/v0/tts` with\n * `utterances[]`, Google Cloud TTS Gemini TTS `multiSpeakerMarkup`).\n * Other providers leave the marker unregistered so the top-level\n * `synthesizeDialogue` / `streamSynthesizeDialogue` helpers fail to\n * satisfy R against those Layers — compile-time error.\n */\nexport class MultiSpeakerTts extends Context.Service<MultiSpeakerTts, void>()(\n \"@betalyra/effect-uai/capability/MultiSpeakerTts\",\n) {}\n\n/** One-shot synthesis. */\nexport const synthesize = (\n request: CommonSynthesizeRequest,\n): Effect.Effect<AudioBlob, AiError.AiError, SpeechSynthesizer> =>\n Effect.flatMap(SpeechSynthesizer, (s) => s.synthesize(request))\n\n/** Full text in, audio chunks out. */\nexport const streamSynthesis = (\n request: CommonSynthesizeRequest,\n): Stream.Stream<AudioChunk, AiError.AiError, SpeechSynthesizer> =>\n Stream.unwrap(Effect.map(SpeechSynthesizer, (s) => s.streamSynthesis(request)))\n\n/**\n * Incremental synthesis. Dual-arity: pipeable (data-last) and direct\n * (data-first). Requires `TtsIncrementalText` in R — providers without\n * incremental-text-in support are a type error at provide time.\n *\n * @example\n * ```ts\n * const audio = LanguageModel.streamTurn(turnReq).pipe(\n * Stream.filterMap(Turn.toTextDelta),\n * SpeechSynthesizer.streamSynthesisFrom(synthReq),\n * )\n * ```\n */\nexport const streamSynthesisFrom: {\n (\n request: CommonStreamSynthesizeRequest,\n ): <E, R>(\n textIn: Stream.Stream<string, E, R>,\n ) => Stream.Stream<AudioChunk, AiError.AiError | E, R | SpeechSynthesizer | TtsIncrementalText>\n <E, R>(\n textIn: Stream.Stream<string, E, R>,\n request: CommonStreamSynthesizeRequest,\n ): Stream.Stream<AudioChunk, AiError.AiError | E, R | SpeechSynthesizer | TtsIncrementalText>\n} = Function.dual(\n 2,\n <E, R>(textIn: Stream.Stream<string, E, R>, request: CommonStreamSynthesizeRequest) =>\n Stream.unwrap(\n Effect.gen(function* () {\n const s = yield* SpeechSynthesizer\n yield* TtsIncrementalText\n return s.streamSynthesisFrom(textIn, request)\n }),\n ),\n)\n\n/**\n * One-shot multi-speaker dialogue. Requires `MultiSpeakerTts` in R —\n * providers without dialogue support are a compile-time error.\n */\nexport const synthesizeDialogue = (\n request: CommonSynthesizeDialogueRequest,\n): Effect.Effect<AudioBlob, AiError.AiError, SpeechSynthesizer | MultiSpeakerTts> =>\n Effect.gen(function* () {\n const s = yield* SpeechSynthesizer\n yield* MultiSpeakerTts\n return yield* s.synthesizeDialogue(request)\n })\n\n/**\n * Chunked-streaming variant. Same R requirement as `synthesizeDialogue`.\n */\nexport const streamSynthesizeDialogue = (\n request: CommonSynthesizeDialogueRequest,\n): Stream.Stream<AudioChunk, AiError.AiError, SpeechSynthesizer | MultiSpeakerTts> =>\n Stream.unwrap(\n Effect.gen(function* () {\n const s = yield* SpeechSynthesizer\n yield* MultiSpeakerTts\n return s.streamSynthesizeDialogue(request)\n }),\n )\n"],"mappings":";;;;;;;;;;;;;AAuIA,IAAa,oBAAb,cAAuC,QAAQ,SAG5C,CAAC,yCAAyC,CAAC;;;;;;;;;;;AAY9C,IAAa,qBAAb,cAAwC,QAAQ,SAAmC,CACjF,qDACD,CAAC;;;;;;;;;;AAWF,IAAa,kBAAb,cAAqC,QAAQ,SAAgC,CAC3E,kDACD,CAAC;;AAGF,MAAa,cACX,YAEA,OAAO,QAAQ,oBAAoB,MAAM,EAAE,WAAW,QAAQ,CAAC;;AAGjE,MAAa,mBACX,YAEA,OAAO,OAAO,OAAO,IAAI,oBAAoB,MAAM,EAAE,gBAAgB,QAAQ,CAAC,CAAC;;;;;;;;;;;;;;AAejF,MAAa,sBAUT,SAAS,KACX,IACO,QAAqC,YAC1C,OAAO,OACL,OAAO,IAAI,aAAa;CACtB,MAAM,IAAI,OAAO;AACjB,QAAO;AACP,QAAO,EAAE,oBAAoB,QAAQ,QAAQ;EAC7C,CACH,CACJ;;;;;AAMD,MAAa,sBACX,YAEA,OAAO,IAAI,aAAa;CACtB,MAAM,IAAI,OAAO;AACjB,QAAO;AACP,QAAO,OAAO,EAAE,mBAAmB,QAAQ;EAC3C;;;;AAKJ,MAAa,4BACX,YAEA,OAAO,OACL,OAAO,IAAI,aAAa;CACtB,MAAM,IAAI,OAAO;AACjB,QAAO;AACP,QAAO,EAAE,yBAAyB,QAAQ;EAC1C,CACH"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { streamSynthesis, streamSynthesisFrom, streamSynthesizeDialogue, synthesize, synthesizeDialogue } from "./SpeechSynthesizer.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { c as it, n as globalExpect, o as describe, t as import_dist } from "../dist-CcRV3-A5.mjs";
|
|
3
3
|
import { layer, layerWithoutIncremental, layerWithoutMultiSpeaker } from "../testing/MockSpeechSynthesizer.mjs";
|
|
4
4
|
import { Duration, Effect, Stream } from "effect";
|
|
5
5
|
//#region src/speech-synthesizer/SpeechSynthesizer.test.ts
|
|
@@ -15,7 +15,9 @@ declare class JsonParseError extends JsonParseError_base<{
|
|
|
15
15
|
}> {}
|
|
16
16
|
/**
|
|
17
17
|
* Decode a `Stream<Uint8Array>` into a `Stream<string>` of newline-delimited
|
|
18
|
-
* lines. Empty lines are skipped. Buffers across chunk boundaries
|
|
18
|
+
* lines. Empty lines are skipped. Buffers across chunk boundaries; a final
|
|
19
|
+
* line without a trailing newline is emitted at clean stream end, while a
|
|
20
|
+
* buffered partial line is discarded if the stream fails.
|
|
19
21
|
*/
|
|
20
22
|
declare const fromBytes: <E, R>(self: Stream.Stream<Uint8Array, E, R>) => Stream.Stream<string, E, R>;
|
|
21
23
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"JSONL.d.mts","names":[],"sources":["../../src/streaming/JSONL.ts"],"mappings":";;;;;;;;
|
|
1
|
+
{"version":3,"file":"JSONL.d.mts","names":[],"sources":["../../src/streaming/JSONL.ts"],"mappings":";;;;;;;;cAA8D,mBAAA;;;cAEjD,cAAA,SAAuB,mBAAA;EAAA,SACzB,IAAA;EAAA,SACA,KAAA;AAAA;;;;;AAJmD;;cAiBjD,SAAA,SACX,IAAA,EAAM,MAAA,CAAO,MAAA,CAAO,UAAA,EAAY,CAAA,EAAG,CAAA,MAClC,MAAA,CAAO,MAAA,SAAe,CAAA,EAAG,CAAA;;;;;;cAYf,KAAA,SAAe,MAAA,EAAQ,MAAA,CAAO,KAAA,CAAM,CAAA,EAAG,CAAA,aAEpC,IAAA,EAAM,MAAA,CAAO,MAAA,SAAe,CAAA,EAAG,CAAA,MAAK,MAAA,CAAO,MAAA,CAAO,CAAA,EAAG,cAAA,GAAiB,CAAA,EAAG,CAAA;;;;;;;cAgB5E,SAAA,GAAa,GAAA,aAAc,MAAA,CAAO,MAAA;;;;;cASlC,OAAA,SACJ,MAAA,EAAQ,MAAA,CAAO,KAAA,CAAM,CAAA,EAAG,CAAA,aACxB,IAAA,EAAM,MAAA,CAAO,MAAA,CAAO,CAAA,EAAG,CAAA,EAAG,CAAA,MAAK,MAAA,CAAO,MAAA,CAAO,UAAA,EAAY,CAAA,EAAG,CAAA"}
|
package/dist/streaming/JSONL.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as __exportAll } from "../chunk-uyGKjUfl.mjs";
|
|
2
|
-
import { Data, Effect, Schema, Stream } from "effect";
|
|
2
|
+
import { Channel, Data, Effect, Schema, Stream } from "effect";
|
|
3
3
|
//#region src/streaming/JSONL.ts
|
|
4
4
|
var JSONL_exports = /* @__PURE__ */ __exportAll({
|
|
5
5
|
JsonParseError: () => JsonParseError,
|
|
@@ -9,19 +9,13 @@ var JSONL_exports = /* @__PURE__ */ __exportAll({
|
|
|
9
9
|
toBytes: () => toBytes
|
|
10
10
|
});
|
|
11
11
|
var JsonParseError = class extends Data.TaggedError("JsonParseError") {};
|
|
12
|
-
const decodeText = (self) => self.pipe(Stream.mapAccum(() => new TextDecoder("utf-8"), (decoder, chunk) => [decoder, [decoder.decode(chunk, { stream: true })]], { onHalt: (decoder) => {
|
|
13
|
-
const tail = decoder.decode();
|
|
14
|
-
return tail.length > 0 ? [tail] : [];
|
|
15
|
-
} }));
|
|
16
|
-
const splitOn = (separator) => (self) => self.pipe(Stream.mapAccum(() => "", (buffer, chunk) => {
|
|
17
|
-
const parts = (buffer + chunk).split(separator);
|
|
18
|
-
return [parts[parts.length - 1] ?? "", parts.slice(0, -1)];
|
|
19
|
-
}, { onHalt: (tail) => tail.length > 0 ? [tail] : [] }));
|
|
20
12
|
/**
|
|
21
13
|
* Decode a `Stream<Uint8Array>` into a `Stream<string>` of newline-delimited
|
|
22
|
-
* lines. Empty lines are skipped. Buffers across chunk boundaries
|
|
14
|
+
* lines. Empty lines are skipped. Buffers across chunk boundaries; a final
|
|
15
|
+
* line without a trailing newline is emitted at clean stream end, while a
|
|
16
|
+
* buffered partial line is discarded if the stream fails.
|
|
23
17
|
*/
|
|
24
|
-
const fromBytes = (self) => self.pipe(decodeText, Stream.
|
|
18
|
+
const fromBytes = (self) => self.pipe(Stream.decodeText, Stream.pipeThroughChannel(Channel.splitLines()), Stream.filter((line) => line.length > 0));
|
|
25
19
|
/**
|
|
26
20
|
* Validate each JSONL line against a Schema. JSON parse errors and Schema
|
|
27
21
|
* decode errors both surface as a `JsonParseError` so callers can `catchTag`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"JSONL.mjs","names":[],"sources":["../../src/streaming/JSONL.ts"],"sourcesContent":["import { Data, Effect, Schema, Stream } from \"effect\"\n\nexport class JsonParseError extends Data.TaggedError(\"JsonParseError\")<{\n readonly line: string\n readonly cause: unknown\n}> {}\n\n// ---------------------------------------------------------------------------\n//
|
|
1
|
+
{"version":3,"file":"JSONL.mjs","names":[],"sources":["../../src/streaming/JSONL.ts"],"sourcesContent":["import { Channel, Data, Effect, Schema, Stream } from \"effect\"\n\nexport class JsonParseError extends Data.TaggedError(\"JsonParseError\")<{\n readonly line: string\n readonly cause: unknown\n}> {}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Decode a `Stream<Uint8Array>` into a `Stream<string>` of newline-delimited\n * lines. Empty lines are skipped. Buffers across chunk boundaries; a final\n * line without a trailing newline is emitted at clean stream end, while a\n * buffered partial line is discarded if the stream fails.\n */\nexport const fromBytes = <E, R>(\n self: Stream.Stream<Uint8Array, E, R>,\n): Stream.Stream<string, E, R> =>\n self.pipe(\n Stream.decodeText,\n Stream.pipeThroughChannel(Channel.splitLines()),\n Stream.filter((line) => line.length > 0),\n )\n\n/**\n * Validate each JSONL line against a Schema. JSON parse errors and Schema\n * decode errors both surface as a `JsonParseError` so callers can `catchTag`\n * uniformly.\n */\nexport const parse = <A, I>(schema: Schema.Codec<A, I>) => {\n const decode = Schema.decodeUnknownEffect(Schema.fromJsonString(schema))\n return <E, R>(self: Stream.Stream<string, E, R>): Stream.Stream<A, JsonParseError | E, R> =>\n self.pipe(\n Stream.mapEffect((line) =>\n decode(line).pipe(Effect.mapError((cause) => new JsonParseError({ line, cause }))),\n ),\n )\n}\n\nconst decodeUnknownFromJson = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Unknown))\n\n/**\n * Best-effort parse of a single JSON frame. Returns the parsed value or\n * `undefined` on malformed input. Realtime WS adapters use this to skip\n * non-JSON or partially-received frames silently rather than fail the\n * entire session over one bad frame.\n */\nexport const parseSafe = (raw: string): Effect.Effect<unknown> =>\n decodeUnknownFromJson(raw).pipe(Effect.orElseSucceed(() => undefined))\n\nconst encoder = new TextEncoder()\n\n/**\n * Serialize a stream of values to JSONL bytes. Encodes each value via\n * `Schema.encodeUnknownSync`. Each line ends with `\\n`.\n */\nexport const toBytes =\n <A, I>(schema: Schema.Codec<A, I>) =>\n <E, R>(self: Stream.Stream<A, E, R>): Stream.Stream<Uint8Array, E, R> =>\n self.pipe(\n Stream.map((value) => {\n const encoded = Schema.encodeUnknownSync(schema)(value)\n return encoder.encode(JSON.stringify(encoded) + \"\\n\")\n }),\n )\n"],"mappings":";;;;;;;;;;AAEA,IAAa,iBAAb,cAAoC,KAAK,YAAY,iBAAiB,CAGnE;;;;;;;AAYH,MAAa,aACX,SAEA,KAAK,KACH,OAAO,YACP,OAAO,mBAAmB,QAAQ,YAAY,CAAC,EAC/C,OAAO,QAAQ,SAAS,KAAK,SAAS,EAAE,CACzC;;;;;;AAOH,MAAa,SAAe,WAA+B;CACzD,MAAM,SAAS,OAAO,oBAAoB,OAAO,eAAe,OAAO,CAAC;AACxE,SAAc,SACZ,KAAK,KACH,OAAO,WAAW,SAChB,OAAO,KAAK,CAAC,KAAK,OAAO,UAAU,UAAU,IAAI,eAAe;EAAE;EAAM;EAAO,CAAC,CAAC,CAAC,CACnF,CACF;;AAGL,MAAM,wBAAwB,OAAO,oBAAoB,OAAO,eAAe,OAAO,QAAQ,CAAC;;;;;;;AAQ/F,MAAa,aAAa,QACxB,sBAAsB,IAAI,CAAC,KAAK,OAAO,oBAAoB,KAAA,EAAU,CAAC;AAExE,MAAM,UAAU,IAAI,aAAa;;;;;AAMjC,MAAa,WACJ,YACA,SACL,KAAK,KACH,OAAO,KAAK,UAAU;CACpB,MAAM,UAAU,OAAO,kBAAkB,OAAO,CAAC,MAAM;AACvD,QAAO,QAAQ,OAAO,KAAK,UAAU,QAAQ,GAAG,KAAK;EACrD,CACH"}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { fromBytes, parse, toBytes } from "./JSONL.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { c as it, n as globalExpect, o as describe } from "../dist-CcRV3-A5.mjs";
|
|
3
3
|
import { Effect, Result, Schema, Stream } from "effect";
|
|
4
4
|
//#region src/streaming/JSONL.test.ts
|
|
5
5
|
const enc = new TextEncoder();
|
|
6
6
|
const bytesOf = (...chunks) => Stream.fromIterable(chunks.map((c) => enc.encode(c)));
|
|
7
7
|
const collect = (s) => Effect.runPromise(Stream.runCollect(s));
|
|
8
|
+
const runUntilEnd = (s) => Effect.runPromise(s.pipe(Stream.result, Stream.runCollect, Effect.map((results) => ({
|
|
9
|
+
seen: results.filter(Result.isSuccess).map((r) => r.success),
|
|
10
|
+
failures: results.filter(Result.isFailure).map((r) => r.failure)
|
|
11
|
+
}))));
|
|
8
12
|
const collectResult = (s) => Effect.runPromise(Effect.result(Stream.runCollect(s)));
|
|
9
13
|
const Patch = Schema.Struct({
|
|
10
14
|
op: Schema.String,
|
|
@@ -27,6 +31,11 @@ describe("JSONL.fromBytes", () => {
|
|
|
27
31
|
it("ignores blank lines", async () => {
|
|
28
32
|
globalExpect(await collect(fromBytes(bytesOf("a\n\n\nb\n")))).toEqual(["a", "b"]);
|
|
29
33
|
});
|
|
34
|
+
it("discards a buffered partial line when the upstream fails mid-stream", async () => {
|
|
35
|
+
const { seen, failures } = await runUntilEnd(fromBytes(Stream.concat(bytesOf("{\"n\":1}\n{\"n\":2"), Stream.fail("boom"))));
|
|
36
|
+
globalExpect(seen).toEqual(["{\"n\":1}"]);
|
|
37
|
+
globalExpect(failures).toEqual(["boom"]);
|
|
38
|
+
});
|
|
30
39
|
});
|
|
31
40
|
describe("JSONL.parse", () => {
|
|
32
41
|
it("decodes well-formed JSON lines through a Schema", async () => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"JSONL.test.mjs","names":["JSONL.fromBytes","JSONL.parse","JSONL.toBytes"],"sources":["../../src/streaming/JSONL.test.ts"],"sourcesContent":["import { Effect, Result, Schema, Stream } from \"effect\"\nimport { describe, expect, it } from \"vitest\"\nimport * as JSONL from \"./JSONL.js\"\n\nconst enc = new TextEncoder()\nconst bytesOf = (...chunks: ReadonlyArray<string>) =>\n Stream.fromIterable(chunks.map((c) => enc.encode(c)))\n\nconst collect = <A, E>(s: Stream.Stream<A, E>) => Effect.runPromise(Stream.runCollect(s))\n\nconst collectResult = <A, E>(s: Stream.Stream<A, E>) =>\n Effect.runPromise(Effect.result(Stream.runCollect(s)))\n\nconst Patch = Schema.Struct({ op: Schema.String, value: Schema.Number })\n\ndescribe(\"JSONL.fromBytes\", () => {\n it(\"emits one string per line\", async () => {\n const out = await collect(JSONL.fromBytes(bytesOf(\"a\\nb\\nc\\n\")))\n expect(out).toEqual([\"a\", \"b\", \"c\"])\n })\n\n it(\"buffers lines across chunk boundaries\", async () => {\n const out = await collect(JSONL.fromBytes(bytesOf(\"ab\", \"c\\nde\", \"f\\n\")))\n expect(out).toEqual([\"abc\", \"def\"])\n })\n\n it(\"flushes a trailing line without a final newline\", async () => {\n const out = await collect(JSONL.fromBytes(bytesOf(\"a\\nb\")))\n expect(out).toEqual([\"a\", \"b\"])\n })\n\n it(\"ignores blank lines\", async () => {\n const out = await collect(JSONL.fromBytes(bytesOf(\"a\\n\\n\\nb\\n\")))\n expect(out).toEqual([\"a\", \"b\"])\n })\n})\n\ndescribe(\"JSONL.parse\", () => {\n it(\"decodes well-formed JSON lines through a Schema\", async () => {\n const out = await collect(\n bytesOf(`{\"op\":\"add\",\"value\":1}\\n{\"op\":\"sub\",\"value\":2}\\n`).pipe(\n JSONL.fromBytes,\n JSONL.parse(Patch),\n ),\n )\n expect(out).toEqual([\n { op: \"add\", value: 1 },\n { op: \"sub\", value: 2 },\n ])\n })\n\n it(\"fails with JsonParseError on malformed JSON\", async () => {\n const result = await collectResult(\n bytesOf(`{\"op\":\"add\",\"value\":1}\\nNOT_JSON\\n`).pipe(JSONL.fromBytes, JSONL.parse(Patch)),\n )\n expect(Result.isFailure(result)).toBe(true)\n if (Result.isFailure(result)) {\n expect(result.failure._tag).toBe(\"JsonParseError\")\n expect(result.failure.line).toBe(\"NOT_JSON\")\n }\n })\n\n it(\"fails with JsonParseError on schema mismatch\", async () => {\n const result = await collectResult(\n bytesOf(`{\"op\":\"add\",\"value\":\"not a number\"}\\n`).pipe(JSONL.fromBytes, JSONL.parse(Patch)),\n )\n expect(Result.isFailure(result)).toBe(true)\n if (Result.isFailure(result)) {\n expect(result.failure._tag).toBe(\"JsonParseError\")\n }\n })\n})\n\ndescribe(\"JSONL round-trip\", () => {\n it(\"toBytes then fromBytes/parse recovers the values\", async () => {\n const values = [\n { op: \"a\", value: 1 },\n { op: \"b\", value: 2 },\n ]\n const out = await collect(\n Stream.fromIterable(values).pipe(JSONL.toBytes(Patch), JSONL.fromBytes, JSONL.parse(Patch)),\n )\n expect(out).toEqual(values)\n })\n})\n"],"mappings":";;;;AAIA,MAAM,MAAM,IAAI,aAAa;AAC7B,MAAM,WAAW,GAAG,WAClB,OAAO,aAAa,OAAO,KAAK,MAAM,IAAI,OAAO,EAAE,CAAC,CAAC;AAEvD,MAAM,WAAiB,MAA2B,OAAO,WAAW,OAAO,WAAW,EAAE,CAAC;AAEzF,MAAM,iBAAuB,MAC3B,OAAO,WAAW,OAAO,OAAO,OAAO,WAAW,EAAE,CAAC,CAAC;AAExD,MAAM,QAAQ,OAAO,OAAO;CAAE,IAAI,OAAO;CAAQ,OAAO,OAAO;CAAQ,CAAC;AAExE,SAAS,yBAAyB;AAChC,IAAG,6BAA6B,YAAY;AAE1C,eAAO,MADW,QAAQA,UAAgB,QAAQ,YAAY,CAAC,CAAC,CACrD,CAAC,QAAQ;GAAC;GAAK;GAAK;GAAI,CAAC;GACpC;AAEF,IAAG,yCAAyC,YAAY;AAEtD,eAAO,MADW,QAAQA,UAAgB,QAAQ,MAAM,SAAS,MAAM,CAAC,CAAC,CAC9D,CAAC,QAAQ,CAAC,OAAO,MAAM,CAAC;GACnC;AAEF,IAAG,mDAAmD,YAAY;AAEhE,eAAO,MADW,QAAQA,UAAgB,QAAQ,OAAO,CAAC,CAAC,CAChD,CAAC,QAAQ,CAAC,KAAK,IAAI,CAAC;GAC/B;AAEF,IAAG,uBAAuB,YAAY;AAEpC,eAAO,MADW,QAAQA,UAAgB,QAAQ,aAAa,CAAC,CAAC,CACtD,CAAC,QAAQ,CAAC,KAAK,IAAI,CAAC;GAC/B;EACF;AAEF,SAAS,qBAAqB;AAC5B,IAAG,mDAAmD,YAAY;AAOhE,eAAO,MANW,QAChB,QAAQ,mDAAmD,CAAC,KAC1DA,WACAC,MAAY,MAAM,CACnB,CACF,CACU,CAAC,QAAQ,CAClB;GAAE,IAAI;GAAO,OAAO;GAAG,EACvB;GAAE,IAAI;GAAO,OAAO;GAAG,CACxB,CAAC;GACF;AAEF,IAAG,+CAA+C,YAAY;EAC5D,MAAM,SAAS,MAAM,cACnB,QAAQ,qCAAqC,CAAC,KAAKD,WAAiBC,MAAY,MAAM,CAAC,CACxF;AACD,eAAO,OAAO,UAAU,OAAO,CAAC,CAAC,KAAK,KAAK;AAC3C,MAAI,OAAO,UAAU,OAAO,EAAE;AAC5B,gBAAO,OAAO,QAAQ,KAAK,CAAC,KAAK,iBAAiB;AAClD,gBAAO,OAAO,QAAQ,KAAK,CAAC,KAAK,WAAW;;GAE9C;AAEF,IAAG,gDAAgD,YAAY;EAC7D,MAAM,SAAS,MAAM,cACnB,QAAQ,wCAAwC,CAAC,KAAKD,WAAiBC,MAAY,MAAM,CAAC,CAC3F;AACD,eAAO,OAAO,UAAU,OAAO,CAAC,CAAC,KAAK,KAAK;AAC3C,MAAI,OAAO,UAAU,OAAO,CAC1B,cAAO,OAAO,QAAQ,KAAK,CAAC,KAAK,iBAAiB;GAEpD;EACF;AAEF,SAAS,0BAA0B;AACjC,IAAG,oDAAoD,YAAY;EACjE,MAAM,SAAS,CACb;GAAE,IAAI;GAAK,OAAO;GAAG,EACrB;GAAE,IAAI;GAAK,OAAO;GAAG,CACtB;AAID,eAAO,MAHW,QAChB,OAAO,aAAa,OAAO,CAAC,KAAKC,QAAc,MAAM,EAAEF,WAAiBC,MAAY,MAAM,CAAC,CAC5F,CACU,CAAC,QAAQ,OAAO;GAC3B;EACF"}
|
|
1
|
+
{"version":3,"file":"JSONL.test.mjs","names":["JSONL.fromBytes","JSONL.parse","JSONL.toBytes"],"sources":["../../src/streaming/JSONL.test.ts"],"sourcesContent":["import { Effect, Result, Schema, Stream } from \"effect\"\nimport { describe, expect, it } from \"vitest\"\nimport * as JSONL from \"./JSONL.js\"\n\nconst enc = new TextEncoder()\nconst bytesOf = (...chunks: ReadonlyArray<string>) =>\n Stream.fromIterable(chunks.map((c) => enc.encode(c)))\n\nconst collect = <A, E>(s: Stream.Stream<A, E>) => Effect.runPromise(Stream.runCollect(s))\n\nconst runUntilEnd = <A, E>(s: Stream.Stream<A, E>) =>\n Effect.runPromise(\n s.pipe(\n Stream.result,\n Stream.runCollect,\n Effect.map((results) => ({\n seen: results.filter(Result.isSuccess).map((r) => r.success),\n failures: results.filter(Result.isFailure).map((r) => r.failure),\n })),\n ),\n )\n\nconst collectResult = <A, E>(s: Stream.Stream<A, E>) =>\n Effect.runPromise(Effect.result(Stream.runCollect(s)))\n\nconst Patch = Schema.Struct({ op: Schema.String, value: Schema.Number })\n\ndescribe(\"JSONL.fromBytes\", () => {\n it(\"emits one string per line\", async () => {\n const out = await collect(JSONL.fromBytes(bytesOf(\"a\\nb\\nc\\n\")))\n expect(out).toEqual([\"a\", \"b\", \"c\"])\n })\n\n it(\"buffers lines across chunk boundaries\", async () => {\n const out = await collect(JSONL.fromBytes(bytesOf(\"ab\", \"c\\nde\", \"f\\n\")))\n expect(out).toEqual([\"abc\", \"def\"])\n })\n\n it(\"flushes a trailing line without a final newline\", async () => {\n const out = await collect(JSONL.fromBytes(bytesOf(\"a\\nb\")))\n expect(out).toEqual([\"a\", \"b\"])\n })\n\n it(\"ignores blank lines\", async () => {\n const out = await collect(JSONL.fromBytes(bytesOf(\"a\\n\\n\\nb\\n\")))\n expect(out).toEqual([\"a\", \"b\"])\n })\n\n it(\"discards a buffered partial line when the upstream fails mid-stream\", async () => {\n const failing = Stream.concat(bytesOf('{\"n\":1}\\n{\"n\":2'), Stream.fail(\"boom\" as const))\n const { seen, failures } = await runUntilEnd(JSONL.fromBytes(failing))\n expect(seen).toEqual(['{\"n\":1}'])\n expect(failures).toEqual([\"boom\"])\n })\n})\n\ndescribe(\"JSONL.parse\", () => {\n it(\"decodes well-formed JSON lines through a Schema\", async () => {\n const out = await collect(\n bytesOf(`{\"op\":\"add\",\"value\":1}\\n{\"op\":\"sub\",\"value\":2}\\n`).pipe(\n JSONL.fromBytes,\n JSONL.parse(Patch),\n ),\n )\n expect(out).toEqual([\n { op: \"add\", value: 1 },\n { op: \"sub\", value: 2 },\n ])\n })\n\n it(\"fails with JsonParseError on malformed JSON\", async () => {\n const result = await collectResult(\n bytesOf(`{\"op\":\"add\",\"value\":1}\\nNOT_JSON\\n`).pipe(JSONL.fromBytes, JSONL.parse(Patch)),\n )\n expect(Result.isFailure(result)).toBe(true)\n if (Result.isFailure(result)) {\n expect(result.failure._tag).toBe(\"JsonParseError\")\n expect(result.failure.line).toBe(\"NOT_JSON\")\n }\n })\n\n it(\"fails with JsonParseError on schema mismatch\", async () => {\n const result = await collectResult(\n bytesOf(`{\"op\":\"add\",\"value\":\"not a number\"}\\n`).pipe(JSONL.fromBytes, JSONL.parse(Patch)),\n )\n expect(Result.isFailure(result)).toBe(true)\n if (Result.isFailure(result)) {\n expect(result.failure._tag).toBe(\"JsonParseError\")\n }\n })\n})\n\ndescribe(\"JSONL round-trip\", () => {\n it(\"toBytes then fromBytes/parse recovers the values\", async () => {\n const values = [\n { op: \"a\", value: 1 },\n { op: \"b\", value: 2 },\n ]\n const out = await collect(\n Stream.fromIterable(values).pipe(JSONL.toBytes(Patch), JSONL.fromBytes, JSONL.parse(Patch)),\n )\n expect(out).toEqual(values)\n })\n})\n"],"mappings":";;;;AAIA,MAAM,MAAM,IAAI,aAAa;AAC7B,MAAM,WAAW,GAAG,WAClB,OAAO,aAAa,OAAO,KAAK,MAAM,IAAI,OAAO,EAAE,CAAC,CAAC;AAEvD,MAAM,WAAiB,MAA2B,OAAO,WAAW,OAAO,WAAW,EAAE,CAAC;AAEzF,MAAM,eAAqB,MACzB,OAAO,WACL,EAAE,KACA,OAAO,QACP,OAAO,YACP,OAAO,KAAK,aAAa;CACvB,MAAM,QAAQ,OAAO,OAAO,UAAU,CAAC,KAAK,MAAM,EAAE,QAAQ;CAC5D,UAAU,QAAQ,OAAO,OAAO,UAAU,CAAC,KAAK,MAAM,EAAE,QAAQ;CACjE,EAAE,CACJ,CACF;AAEH,MAAM,iBAAuB,MAC3B,OAAO,WAAW,OAAO,OAAO,OAAO,WAAW,EAAE,CAAC,CAAC;AAExD,MAAM,QAAQ,OAAO,OAAO;CAAE,IAAI,OAAO;CAAQ,OAAO,OAAO;CAAQ,CAAC;AAExE,SAAS,yBAAyB;AAChC,IAAG,6BAA6B,YAAY;AAE1C,eAAO,MADW,QAAQA,UAAgB,QAAQ,YAAY,CAAC,CAAC,CACrD,CAAC,QAAQ;GAAC;GAAK;GAAK;GAAI,CAAC;GACpC;AAEF,IAAG,yCAAyC,YAAY;AAEtD,eAAO,MADW,QAAQA,UAAgB,QAAQ,MAAM,SAAS,MAAM,CAAC,CAAC,CAC9D,CAAC,QAAQ,CAAC,OAAO,MAAM,CAAC;GACnC;AAEF,IAAG,mDAAmD,YAAY;AAEhE,eAAO,MADW,QAAQA,UAAgB,QAAQ,OAAO,CAAC,CAAC,CAChD,CAAC,QAAQ,CAAC,KAAK,IAAI,CAAC;GAC/B;AAEF,IAAG,uBAAuB,YAAY;AAEpC,eAAO,MADW,QAAQA,UAAgB,QAAQ,aAAa,CAAC,CAAC,CACtD,CAAC,QAAQ,CAAC,KAAK,IAAI,CAAC;GAC/B;AAEF,IAAG,uEAAuE,YAAY;EAEpF,MAAM,EAAE,MAAM,aAAa,MAAM,YAAYA,UAD7B,OAAO,OAAO,QAAQ,sBAAkB,EAAE,OAAO,KAAK,OAAgB,CACzB,CAAQ,CAAC;AACtE,eAAO,KAAK,CAAC,QAAQ,CAAC,YAAU,CAAC;AACjC,eAAO,SAAS,CAAC,QAAQ,CAAC,OAAO,CAAC;GAClC;EACF;AAEF,SAAS,qBAAqB;AAC5B,IAAG,mDAAmD,YAAY;AAOhE,eAAO,MANW,QAChB,QAAQ,mDAAmD,CAAC,KAC1DA,WACAC,MAAY,MAAM,CACnB,CACF,CACU,CAAC,QAAQ,CAClB;GAAE,IAAI;GAAO,OAAO;GAAG,EACvB;GAAE,IAAI;GAAO,OAAO;GAAG,CACxB,CAAC;GACF;AAEF,IAAG,+CAA+C,YAAY;EAC5D,MAAM,SAAS,MAAM,cACnB,QAAQ,qCAAqC,CAAC,KAAKD,WAAiBC,MAAY,MAAM,CAAC,CACxF;AACD,eAAO,OAAO,UAAU,OAAO,CAAC,CAAC,KAAK,KAAK;AAC3C,MAAI,OAAO,UAAU,OAAO,EAAE;AAC5B,gBAAO,OAAO,QAAQ,KAAK,CAAC,KAAK,iBAAiB;AAClD,gBAAO,OAAO,QAAQ,KAAK,CAAC,KAAK,WAAW;;GAE9C;AAEF,IAAG,gDAAgD,YAAY;EAC7D,MAAM,SAAS,MAAM,cACnB,QAAQ,wCAAwC,CAAC,KAAKD,WAAiBC,MAAY,MAAM,CAAC,CAC3F;AACD,eAAO,OAAO,UAAU,OAAO,CAAC,CAAC,KAAK,KAAK;AAC3C,MAAI,OAAO,UAAU,OAAO,CAC1B,cAAO,OAAO,QAAQ,KAAK,CAAC,KAAK,iBAAiB;GAEpD;EACF;AAEF,SAAS,0BAA0B;AACjC,IAAG,oDAAoD,YAAY;EACjE,MAAM,SAAS,CACb;GAAE,IAAI;GAAK,OAAO;GAAG,EACrB;GAAE,IAAI;GAAK,OAAO;GAAG,CACtB;AAID,eAAO,MAHW,QAChB,OAAO,aAAa,OAAO,CAAC,KAAKC,QAAc,MAAM,EAAEF,WAAiBC,MAAY,MAAM,CAAC,CAC5F,CACU,CAAC,QAAQ,OAAO;GAC3B;EACF"}
|
package/dist/streaming/SSE.d.mts
CHANGED
|
@@ -6,7 +6,7 @@ declare namespace SSE_d_exports {
|
|
|
6
6
|
}
|
|
7
7
|
/**
|
|
8
8
|
* One Server-Sent Event. Fields per the WHATWG spec:
|
|
9
|
-
* - `event`: optional event name (default "message"
|
|
9
|
+
* - `event`: optional event name (absent for the default "message")
|
|
10
10
|
* - `data`: payload, with multiple `data:` lines joined by `\n`
|
|
11
11
|
* - `id`: optional last-event id
|
|
12
12
|
*/
|
|
@@ -17,8 +17,10 @@ type Event = {
|
|
|
17
17
|
};
|
|
18
18
|
/**
|
|
19
19
|
* Decode a `Stream<Uint8Array>` (e.g. an HTTP response body) into a
|
|
20
|
-
* `Stream<SSE.Event
|
|
21
|
-
*
|
|
20
|
+
* `Stream<SSE.Event>`, using the spec parser from `effect/unstable/encoding`.
|
|
21
|
+
* An event is dispatched only once its terminating blank line arrives, so a
|
|
22
|
+
* partial event at stream end (clean or failed) is discarded per the spec
|
|
23
|
+
* rather than surfaced as a truncated frame.
|
|
22
24
|
*/
|
|
23
25
|
declare const fromBytes: <E, R>(self: Stream.Stream<Uint8Array, E, R>) => Stream.Stream<Event, E, R>;
|
|
24
26
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SSE.d.mts","names":[],"sources":["../../src/streaming/SSE.ts"],"mappings":";;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"SSE.d.mts","names":[],"sources":["../../src/streaming/SSE.ts"],"mappings":";;;;;;;;;;;;KASY,KAAA;EAAA,SACD,KAAA;EAAA,SACA,IAAA;EAAA,SACA,EAAA;AAAA;;;;;;;AA8BX;cAAa,SAAA,SACX,IAAA,EAAM,MAAA,CAAO,MAAA,CAAO,UAAA,EAAY,CAAA,EAAG,CAAA,MAClC,MAAA,CAAO,MAAA,CAAO,KAAA,EAAO,CAAA,EAAG,CAAA;;;;;cAed,OAAA,SAAiB,IAAA,EAAM,MAAA,CAAO,MAAA,CAAO,KAAA,EAAO,CAAA,EAAG,CAAA,MAAK,MAAA,CAAO,MAAA,CAAO,UAAA,EAAY,CAAA,EAAG,CAAA"}
|
package/dist/streaming/SSE.mjs
CHANGED
|
@@ -1,57 +1,47 @@
|
|
|
1
1
|
import { n as __exportAll } from "../chunk-uyGKjUfl.mjs";
|
|
2
2
|
import { Stream } from "effect";
|
|
3
|
+
import { Sse } from "effect/unstable/encoding";
|
|
3
4
|
//#region src/streaming/SSE.ts
|
|
4
5
|
var SSE_exports = /* @__PURE__ */ __exportAll({
|
|
5
6
|
fromBytes: () => fromBytes,
|
|
6
7
|
toBytes: () => toBytes
|
|
7
8
|
});
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const value = line.slice(colon + 1);
|
|
22
|
-
return [line.slice(0, colon), value.startsWith(" ") ? value.slice(1) : value];
|
|
23
|
-
};
|
|
24
|
-
const parseBlock = (block) => {
|
|
25
|
-
const lines = block.split("\n").filter((l) => l.length > 0 && !l.startsWith(":"));
|
|
26
|
-
if (lines.length === 0) return null;
|
|
27
|
-
const fields = lines.map(parseField);
|
|
28
|
-
const dataLines = fields.filter(([f]) => f === "data").map(([, v]) => v);
|
|
29
|
-
const event = fields.find(([f]) => f === "event")?.[1];
|
|
30
|
-
const id = fields.find(([f]) => f === "id")?.[1];
|
|
31
|
-
const out = { data: dataLines.join("\n") };
|
|
32
|
-
if (event !== void 0) out.event = event;
|
|
33
|
-
if (id !== void 0) out.id = id;
|
|
34
|
-
return out;
|
|
9
|
+
const toEvent = (wire) => ({
|
|
10
|
+
data: wire.data,
|
|
11
|
+
...wire.event !== "message" && { event: wire.event },
|
|
12
|
+
...wire.id !== void 0 && { id: wire.id }
|
|
13
|
+
});
|
|
14
|
+
const makeParserState = () => {
|
|
15
|
+
const out = [];
|
|
16
|
+
return {
|
|
17
|
+
parser: Sse.makeParser((event) => {
|
|
18
|
+
if (event._tag === "Event") out.push(toEvent(event));
|
|
19
|
+
}),
|
|
20
|
+
out
|
|
21
|
+
};
|
|
35
22
|
};
|
|
36
23
|
/**
|
|
37
24
|
* Decode a `Stream<Uint8Array>` (e.g. an HTTP response body) into a
|
|
38
|
-
* `Stream<SSE.Event
|
|
39
|
-
*
|
|
25
|
+
* `Stream<SSE.Event>`, using the spec parser from `effect/unstable/encoding`.
|
|
26
|
+
* An event is dispatched only once its terminating blank line arrives, so a
|
|
27
|
+
* partial event at stream end (clean or failed) is discarded per the spec
|
|
28
|
+
* rather than surfaced as a truncated frame.
|
|
40
29
|
*/
|
|
41
|
-
const fromBytes = (self) => self.pipe(decodeText, Stream.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (ev.id !== void 0) parts.push(`id: ${ev.id}`);
|
|
46
|
-
for (const line of ev.data.split("\n")) parts.push(`data: ${line}`);
|
|
47
|
-
return parts.join("\n") + "\n\n";
|
|
48
|
-
};
|
|
30
|
+
const fromBytes = (self) => self.pipe(Stream.decodeText, Stream.mapAccum(makeParserState, (state, text) => {
|
|
31
|
+
state.parser.feed(text);
|
|
32
|
+
return [state, state.out.splice(0)];
|
|
33
|
+
}));
|
|
49
34
|
const encoder = new TextEncoder();
|
|
50
35
|
/**
|
|
51
36
|
* Encode a `Stream<Event>` as `Stream<Uint8Array>` ready to send on an
|
|
52
37
|
* HTTP response with `Content-Type: text/event-stream`.
|
|
53
38
|
*/
|
|
54
|
-
const toBytes = (self) => Stream.map(self, (ev) => encoder.encode(
|
|
39
|
+
const toBytes = (self) => Stream.map(self, (ev) => encoder.encode(Sse.encoder.write({
|
|
40
|
+
_tag: "Event",
|
|
41
|
+
event: ev.event ?? "message",
|
|
42
|
+
id: ev.id,
|
|
43
|
+
data: ev.data
|
|
44
|
+
})));
|
|
55
45
|
//#endregion
|
|
56
46
|
export { fromBytes, SSE_exports as t, toBytes };
|
|
57
47
|
|