@blastlauncher/protocol 0.0.0-stage → 3.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,314 @@
1
+ // The JSON Schema description of the embedder protocol (decision 0009, point 5).
2
+ // It is one exported structure that `scripts/protocol/generate-schema.ts`
3
+ // serializes verbatim into `packages/protocol/schema/`; the checked-in file
4
+ // must regenerate byte-identical (`bun run protocol:check`). Draft 2020-12
5
+ // with a deliberately small keyword set: `type`, `properties`, `required`,
6
+ // `additionalProperties`, `items`, `enum`, `const`, `oneOf`, `anyOf` and
7
+ // `$ref` — the schema test ships its own validator for exactly those.
8
+ import { CAPABILITIES } from "./capabilities.js";
9
+ import { EMBEDDER_METHODS, EMBEDDER_PROTOCOL_VERSION } from "./embedder.js";
10
+ const ref = (name) => ({ $ref: `#/$defs/${name}` });
11
+ const json = ref("jsonValue");
12
+ const string = { type: "string" };
13
+ const integer = { type: "integer" };
14
+ const embedderError = ref("embedderError");
15
+ const nullResult = { type: "null" };
16
+ const envelope = (required, properties) => ({
17
+ type: "object",
18
+ properties,
19
+ required,
20
+ additionalProperties: false,
21
+ });
22
+ const stringRecord = (item) => ({
23
+ type: "object",
24
+ additionalProperties: item,
25
+ });
26
+ /**
27
+ * The result each request method answers. Loose shapes stay JSON values: the
28
+ * engine forwards manifests, dependency reports and preference data without
29
+ * narrowing them here.
30
+ */
31
+ const RESULTS = {
32
+ "extension.open": envelope(["extension"], { extension: string }),
33
+ "extension.launch": envelope(["session"], { session: string }),
34
+ "extension.manifest": json,
35
+ "extension.dependencies.check": json,
36
+ "extension.dependencies.install": json,
37
+ "extension.preferences": json,
38
+ "extension.savePreferences": nullResult,
39
+ "extension.watch": nullResult,
40
+ "extension.unwatch": nullResult,
41
+ "extension.close": nullResult,
42
+ "session.event": nullResult,
43
+ "session.input": nullResult,
44
+ "session.close": nullResult,
45
+ "engine.shutdown": nullResult,
46
+ };
47
+ /** Optional params per method, mirroring the `?` markers of the wire shapes. */
48
+ const OPTIONAL_KEYS = {
49
+ "extension.launch": [
50
+ "argumentValues",
51
+ "launchContext",
52
+ "bundleTimeoutMs",
53
+ "installDependencies",
54
+ "views",
55
+ ],
56
+ };
57
+ const PARAMS = {
58
+ "extension.open": { dir: string },
59
+ "extension.manifest": { extension: string },
60
+ "extension.watch": { extension: string },
61
+ "extension.unwatch": { extension: string },
62
+ "extension.close": { extension: string },
63
+ "extension.dependencies.check": { extension: string },
64
+ "extension.dependencies.install": { extension: string },
65
+ "extension.preferences": { extension: string, command: string },
66
+ "extension.savePreferences": {
67
+ extension: string,
68
+ command: string,
69
+ values: stringRecord({ type: ["string", "number", "boolean"] }),
70
+ },
71
+ "extension.launch": {
72
+ extension: string,
73
+ command: string,
74
+ argumentValues: stringRecord(string),
75
+ launchContext: stringRecord(json),
76
+ bundleTimeoutMs: { type: "integer", minimum: 0, maximum: 2_147_483_647 },
77
+ installDependencies: { type: "boolean" },
78
+ views: envelope(["platform"], { platform: { enum: ["macOS", "Windows", "Linux"] } }),
79
+ },
80
+ "session.input": { session: string, input: ref("controllerInput") },
81
+ "session.event": {
82
+ session: string,
83
+ node: string,
84
+ handler: string,
85
+ args: { type: "array", items: json },
86
+ },
87
+ "session.close": { session: string },
88
+ "engine.shutdown": {},
89
+ };
90
+ // One `request/<method>` envelope def and one `result/<method>` def per
91
+ // method, so an embedder can validate both halves of the exchange.
92
+ const requestAndResultEntries = EMBEDDER_METHODS.flatMap((name) => {
93
+ const optional = OPTIONAL_KEYS[name] ?? [];
94
+ return [
95
+ [
96
+ `request.${name}`,
97
+ envelope(["type", "id", "method"], {
98
+ type: { const: "request" },
99
+ id: string,
100
+ method: { const: name },
101
+ params: {
102
+ type: "object",
103
+ properties: PARAMS[name],
104
+ required: Object.keys(PARAMS[name]).filter((key) => !optional.includes(key)),
105
+ additionalProperties: false,
106
+ },
107
+ }),
108
+ ],
109
+ [`result.${name}`, RESULTS[name]],
110
+ ];
111
+ });
112
+ const EVENT_PROPERTIES = {
113
+ "manifest.changed": { extension: string, manifest: json },
114
+ "manifest.error": { extension: string, error: embedderError },
115
+ "dependencies.log": { extension: string, message: string },
116
+ "session.scene": { session: string, scene: ref("sceneMessage") },
117
+ "session.view": { session: string, view: ref("controllerView") },
118
+ "session.log": { session: string, log: ref("logMessage") },
119
+ "session.exited": { session: string, code: integer, error: embedderError },
120
+ };
121
+ const eventVariants = Object.entries(EVENT_PROPERTIES).map(([name, props]) => envelope(["type", "event", ...(name === "session.view" ? ["session", "view"] : [])], {
122
+ type: { const: "event" },
123
+ event: { const: name },
124
+ ...props,
125
+ }));
126
+ // A capability-request variant per catalog entry: the `name` const pins the
127
+ // received params to that capability's declared shape.
128
+ const capabilityEntries = Object.entries(CAPABILITIES).map(([name, spec]) => [
129
+ `capabilities.${name}`,
130
+ {
131
+ scope: spec.scope,
132
+ summary: spec.summary,
133
+ params: spec.params,
134
+ result: spec.result,
135
+ api: [...spec.api],
136
+ },
137
+ ]);
138
+ const capabilityRequestVariants = Object.keys(CAPABILITIES).map((name) => envelope(["type", "id", "session", "name"], {
139
+ type: { const: "capability.request" },
140
+ id: string,
141
+ session: string,
142
+ name: { const: name },
143
+ params: { $ref: `#/$defs/capabilities.${name}/params` },
144
+ }));
145
+ const sceneNode = {
146
+ type: "object",
147
+ properties: {
148
+ id: string,
149
+ type: string,
150
+ props: {
151
+ type: "object",
152
+ additionalProperties: { anyOf: [ref("handlerRef"), json] },
153
+ },
154
+ children: { type: "array", items: ref("sceneNode") },
155
+ },
156
+ required: ["id", "type", "props", "children"],
157
+ additionalProperties: false,
158
+ };
159
+ /** The whole embedder protocol schema, serialized verbatim by the generator. */
160
+ export const EMBEDDER_PROTOCOL_SCHEMA = {
161
+ $schema: "https://json-schema.org/draft/2020-12/schema",
162
+ $id: "https://blastlauncher.dev/schema/embedder-v1.schema.json",
163
+ title: `Blast embedder protocol v${EMBEDDER_PROTOCOL_VERSION}`,
164
+ description: "NDJSON messages of `blast serve` (decision 0011). Each validated instance " +
165
+ "is one line: an embedder -> server or a server -> embedder message.",
166
+ oneOf: [ref("embedderMessages"), ref("serverMessages")],
167
+ $defs: {
168
+ controllerInput: {
169
+ oneOf: [
170
+ ...["search", "actionsQuery", "dropdownQuery"].map((type) => envelope(["type", "text"], { type: { const: type }, text: string })),
171
+ envelope(["type", "direction"], {
172
+ type: { const: "move" },
173
+ direction: { enum: ["up", "down", "left", "right"] },
174
+ }),
175
+ ...["select", "focus", "toggle"].map((type) => envelope(["type", "id"], { type: { const: type }, id: string })),
176
+ envelope(["type", "id", "value"], {
177
+ type: { const: "setValue" },
178
+ id: string,
179
+ value: string,
180
+ }),
181
+ envelope(["type", "delta"], { type: { const: "optionMove" }, delta: { type: "number" } }),
182
+ ...[
183
+ "primary",
184
+ "openActions",
185
+ "closeActions",
186
+ "back",
187
+ "openDropdown",
188
+ "chooseDropdown",
189
+ "focusNext",
190
+ "focusPrev",
191
+ "optionChoose",
192
+ "submit",
193
+ ].map((type) => envelope(["type"], { type: { const: type } })),
194
+ envelope(["type", "key"], {
195
+ type: { const: "key" },
196
+ key: envelope(["name"], {
197
+ name: string,
198
+ ...Object.fromEntries(["ctrl", "meta", "shift", "option", "super"].map((key) => [key, { type: "boolean" }])),
199
+ }),
200
+ }),
201
+ envelope(["type", "index"], { type: { const: "toastAction" }, index: { type: "number" } }),
202
+ envelope(["type", "confirm"], {
203
+ type: { const: "alert" },
204
+ confirm: { type: "boolean" },
205
+ remember: { type: "boolean" },
206
+ }),
207
+ ],
208
+ },
209
+ controllerView: {
210
+ $comment: "Opaque JSON view model: packages/client-core/src/controller.ts ControllerView.",
211
+ type: "object",
212
+ additionalProperties: json,
213
+ },
214
+ jsonValue: {
215
+ type: ["string", "number", "boolean", "null", "array", "object"],
216
+ items: json,
217
+ additionalProperties: json,
218
+ },
219
+ handlerRef: envelope(["$handler"], { $handler: string }),
220
+ embedderError: envelope(["code", "message"], { code: string, message: string, data: json }),
221
+ sceneNode,
222
+ sceneMessage: envelope(["type", "revision", "root"], {
223
+ type: { const: "scene" },
224
+ revision: integer,
225
+ root: { anyOf: [ref("sceneNode"), { type: "null" }] },
226
+ }),
227
+ logMessage: envelope(["type", "level", "message"], {
228
+ type: { const: "log" },
229
+ level: { type: "string", enum: ["debug", "info", "warn", "error"] },
230
+ message: string,
231
+ }),
232
+ ...Object.fromEntries(requestAndResultEntries),
233
+ request: {
234
+ oneOf: EMBEDDER_METHODS.map((name) => ref(`request.${name}`)),
235
+ },
236
+ "embedder.capability.result": {
237
+ oneOf: [
238
+ envelope(["type", "id", "ok", "value"], {
239
+ type: { const: "capability.result" },
240
+ id: string,
241
+ ok: { const: true },
242
+ value: json,
243
+ }),
244
+ envelope(["type", "id", "ok", "error"], {
245
+ type: { const: "capability.result" },
246
+ id: string,
247
+ ok: { const: false },
248
+ error: embedderError,
249
+ }),
250
+ ],
251
+ },
252
+ "embedder.capability.progress": envelope(["type", "id", "value"], {
253
+ type: { const: "capability.progress" },
254
+ id: string,
255
+ value: json,
256
+ }),
257
+ "embedder.hello": envelope(["type", "protocol", "capabilities"], {
258
+ type: { const: "hello" },
259
+ protocol: integer,
260
+ capabilities: { type: "array", items: string },
261
+ }),
262
+ embedderMessages: {
263
+ oneOf: [
264
+ ref("embedder.hello"),
265
+ ref("request"),
266
+ ref("embedder.capability.result"),
267
+ ref("embedder.capability.progress"),
268
+ ],
269
+ },
270
+ "server.welcome": envelope(["type", "protocol", "engine", "catalog"], {
271
+ type: { const: "welcome" },
272
+ protocol: integer,
273
+ engine: envelope(["version"], { version: string }),
274
+ catalog: { type: "object", additionalProperties: json },
275
+ }),
276
+ "server.error": envelope(["type", "error"], {
277
+ type: { const: "error" },
278
+ error: embedderError,
279
+ }),
280
+ response: {
281
+ oneOf: [
282
+ envelope(["type", "id", "ok", "result"], {
283
+ type: { const: "response" },
284
+ id: string,
285
+ ok: { const: true },
286
+ result: json,
287
+ }),
288
+ envelope(["type", "id", "ok", "error"], {
289
+ type: { const: "response" },
290
+ id: string,
291
+ ok: { const: false },
292
+ error: embedderError,
293
+ }),
294
+ ],
295
+ },
296
+ "server.event": { oneOf: eventVariants },
297
+ "server.capability.request": { oneOf: capabilityRequestVariants },
298
+ "server.capability.cancel": envelope(["type", "id"], {
299
+ type: { const: "capability.cancel" },
300
+ id: string,
301
+ }),
302
+ serverMessages: {
303
+ oneOf: [
304
+ ref("server.welcome"),
305
+ ref("server.error"),
306
+ ref("response"),
307
+ ref("server.event"),
308
+ ref("server.capability.request"),
309
+ ref("server.capability.cancel"),
310
+ ],
311
+ },
312
+ ...Object.fromEntries(capabilityEntries),
313
+ },
314
+ };
@@ -0,0 +1,173 @@
1
+ import type { JsonValue } from "./messages.js";
2
+ export declare const EMBEDDER_PROTOCOL_VERSION = 1;
3
+ export type EmbedderError = {
4
+ code: string;
5
+ message: string;
6
+ data?: JsonValue;
7
+ };
8
+ /** Structural wire counterpart of client-core's ControllerInput; no runtime dependency. */
9
+ export type EmbedderControllerInput = {
10
+ type: "search" | "actionsQuery" | "dropdownQuery";
11
+ text: string;
12
+ } | {
13
+ type: "move";
14
+ direction: "up" | "down" | "left" | "right";
15
+ } | {
16
+ type: "select" | "focus" | "toggle";
17
+ id: string;
18
+ } | {
19
+ type: "setValue";
20
+ id: string;
21
+ value: string;
22
+ } | {
23
+ type: "optionMove";
24
+ delta: number;
25
+ } | {
26
+ type: "primary" | "openActions" | "closeActions" | "back" | "openDropdown" | "chooseDropdown" | "focusNext" | "focusPrev" | "optionChoose" | "submit";
27
+ } | {
28
+ type: "key";
29
+ key: {
30
+ name: string;
31
+ ctrl?: boolean;
32
+ meta?: boolean;
33
+ shift?: boolean;
34
+ option?: boolean;
35
+ super?: boolean;
36
+ };
37
+ } | {
38
+ type: "toastAction";
39
+ index: number;
40
+ } | {
41
+ type: "alert";
42
+ confirm: boolean;
43
+ remember?: boolean;
44
+ };
45
+ export type EmbedderParams = {
46
+ "extension.open": {
47
+ dir: string;
48
+ };
49
+ "extension.manifest": {
50
+ extension: string;
51
+ };
52
+ "extension.watch": {
53
+ extension: string;
54
+ };
55
+ "extension.unwatch": {
56
+ extension: string;
57
+ };
58
+ "extension.close": {
59
+ extension: string;
60
+ };
61
+ "extension.dependencies.check": {
62
+ extension: string;
63
+ };
64
+ "extension.dependencies.install": {
65
+ extension: string;
66
+ };
67
+ "extension.preferences": {
68
+ extension: string;
69
+ command: string;
70
+ };
71
+ "extension.savePreferences": {
72
+ extension: string;
73
+ command: string;
74
+ values: Record<string, string | number | boolean>;
75
+ };
76
+ "extension.launch": {
77
+ extension: string;
78
+ command: string;
79
+ argumentValues?: Record<string, string>;
80
+ launchContext?: Record<string, JsonValue>;
81
+ bundleTimeoutMs?: number;
82
+ installDependencies?: boolean;
83
+ views?: {
84
+ platform: "macOS" | "Windows" | "Linux";
85
+ };
86
+ };
87
+ "session.input": {
88
+ session: string;
89
+ input: EmbedderControllerInput;
90
+ };
91
+ "session.event": {
92
+ session: string;
93
+ node: string;
94
+ handler: string;
95
+ args: JsonValue[];
96
+ };
97
+ "session.close": {
98
+ session: string;
99
+ };
100
+ "engine.shutdown": Record<string, never>;
101
+ };
102
+ export type EmbedderRequest = {
103
+ [M in keyof EmbedderParams]: {
104
+ type: "request";
105
+ id: string;
106
+ method: M;
107
+ params: EmbedderParams[M];
108
+ };
109
+ }[keyof EmbedderParams];
110
+ export type EmbedderMessage = {
111
+ type: "hello";
112
+ protocol: number;
113
+ capabilities: string[];
114
+ } | EmbedderRequest | {
115
+ type: "capability.result";
116
+ id: string;
117
+ ok: true;
118
+ value: JsonValue;
119
+ } | {
120
+ type: "capability.result";
121
+ id: string;
122
+ ok: false;
123
+ error: EmbedderError;
124
+ } | {
125
+ type: "capability.progress";
126
+ id: string;
127
+ value: JsonValue;
128
+ };
129
+ export type EmbedderServerMessage = {
130
+ type: "welcome";
131
+ protocol: number;
132
+ engine: {
133
+ version: string;
134
+ };
135
+ catalog: JsonValue;
136
+ } | {
137
+ type: "error";
138
+ error: EmbedderError;
139
+ } | {
140
+ type: "response";
141
+ id: string;
142
+ ok: true;
143
+ result: JsonValue;
144
+ } | {
145
+ type: "response";
146
+ id: string;
147
+ ok: false;
148
+ error: EmbedderError;
149
+ } | {
150
+ type: "event";
151
+ event: string;
152
+ [key: string]: JsonValue;
153
+ } | {
154
+ type: "capability.request";
155
+ id: string;
156
+ session: string;
157
+ name: string;
158
+ params: JsonValue;
159
+ } | {
160
+ type: "capability.cancel";
161
+ id: string;
162
+ };
163
+ /** Every request method name, in wire-table order; the schema generator and
164
+ * its test enumerate this instead of re-listing the methods. */
165
+ export declare const EMBEDDER_METHODS: (keyof EmbedderParams & string)[];
166
+ /** Total boundary validator; unknown fields and methods are rejected. */
167
+ export declare function parseEmbedderMessage(value: unknown): {
168
+ ok: true;
169
+ message: EmbedderMessage;
170
+ } | {
171
+ ok: false;
172
+ error: string;
173
+ };
@@ -0,0 +1,151 @@
1
+ export const EMBEDDER_PROTOCOL_VERSION = 1;
2
+ const object = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
3
+ const json = (v) => v === null ||
4
+ typeof v === "string" ||
5
+ typeof v === "boolean" ||
6
+ (typeof v === "number" && Number.isFinite(v)) ||
7
+ (Array.isArray(v) ? v.every(json) : object(v) && Object.values(v).every(json));
8
+ const string = (v) => typeof v === "string";
9
+ const record = (v, check) => object(v) && Object.values(v).every(check);
10
+ const shapes = {
11
+ "extension.open": ["dir"],
12
+ "extension.manifest": ["extension"],
13
+ "extension.watch": ["extension"],
14
+ "extension.unwatch": ["extension"],
15
+ "extension.close": ["extension"],
16
+ "extension.dependencies.check": ["extension"],
17
+ "extension.dependencies.install": ["extension"],
18
+ "extension.preferences": ["extension", "command"],
19
+ "extension.savePreferences": ["extension", "command", "values"],
20
+ "extension.launch": [
21
+ "extension",
22
+ "command",
23
+ "argumentValues?",
24
+ "launchContext?",
25
+ "bundleTimeoutMs?",
26
+ "installDependencies?",
27
+ "views?",
28
+ ],
29
+ "session.input": ["session", "input"],
30
+ "session.event": ["session", "node", "handler", "args"],
31
+ "session.close": ["session"],
32
+ "engine.shutdown": [],
33
+ };
34
+ /** Every request method name, in wire-table order; the schema generator and
35
+ * its test enumerate this instead of re-listing the methods. */
36
+ export const EMBEDDER_METHODS = Object.keys(shapes);
37
+ function fields(v, names, check) {
38
+ return (Object.keys(v).every((k) => names.includes(k) || names.includes(`${k}?`)) &&
39
+ names.every((name) => {
40
+ const key = name.replace(/\?$/, "");
41
+ return (name.endsWith("?") && !(key in v)) || (key in v && check(key, v[key]));
42
+ }));
43
+ }
44
+ function valid(v) {
45
+ if (!object(v))
46
+ return false;
47
+ if (v.type === "hello")
48
+ return fields(v, ["type", "protocol", "capabilities"], (k, x) => k === "protocol"
49
+ ? Number.isSafeInteger(x)
50
+ : k === "capabilities"
51
+ ? Array.isArray(x) && x.every(string)
52
+ : string(x));
53
+ if (v.type === "capability.progress")
54
+ return fields(v, ["type", "id", "value"], (k, x) => (k === "value" ? json(x) : string(x)));
55
+ if (v.type === "capability.result")
56
+ return fields(v, ["type", "id", "ok", v.ok === true ? "value" : "error"], (k, x) => k === "ok"
57
+ ? typeof x === "boolean"
58
+ : k === "value"
59
+ ? json(x)
60
+ : k === "error"
61
+ ? object(x) &&
62
+ fields(x, ["code", "message", "data?"], (key, y) => key === "data" ? json(y) : string(y))
63
+ : string(x));
64
+ if (v.type !== "request" || !string(v.method) || !Object.hasOwn(shapes, v.method))
65
+ return false;
66
+ return fields(v, ["type", "id", "method", "params"], (k, x) => k !== "params"
67
+ ? string(x)
68
+ : object(x) &&
69
+ fields(x, shapes[v.method], (key, y) => {
70
+ if (key === "args")
71
+ return Array.isArray(y) && y.every(json);
72
+ if (key === "argumentValues")
73
+ return record(y, string);
74
+ if (key === "bundleTimeoutMs")
75
+ return typeof y === "number" && Number.isSafeInteger(y) && y >= 0 && y <= 2_147_483_647;
76
+ if (key === "installDependencies")
77
+ return typeof y === "boolean";
78
+ if (key === "launchContext")
79
+ return record(y, json);
80
+ if (key === "views")
81
+ return (object(y) &&
82
+ fields(y, ["platform"], (_, z) => ["macOS", "Windows", "Linux"].includes(z)));
83
+ if (key === "input")
84
+ return controllerInput(y);
85
+ if (key === "values")
86
+ return record(y, (z) => typeof z === "string" ||
87
+ typeof z === "boolean" ||
88
+ (typeof z === "number" && Number.isFinite(z)));
89
+ return string(y);
90
+ }));
91
+ }
92
+ function controllerInput(v) {
93
+ if (!object(v))
94
+ return false;
95
+ const shape = ["search", "actionsQuery", "dropdownQuery"].includes(v.type)
96
+ ? ["type", "text"]
97
+ : v.type === "move"
98
+ ? ["type", "direction"]
99
+ : v.type === "setValue"
100
+ ? ["type", "id", "value"]
101
+ : v.type === "optionMove"
102
+ ? ["type", "delta"]
103
+ : ["select", "focus", "toggle"].includes(v.type)
104
+ ? ["type", "id"]
105
+ : v.type === "key"
106
+ ? ["type", "key"]
107
+ : v.type === "toastAction"
108
+ ? ["type", "index"]
109
+ : v.type === "alert"
110
+ ? ["type", "confirm", "remember?"]
111
+ : [
112
+ "focusNext",
113
+ "focusPrev",
114
+ "optionChoose",
115
+ "submit",
116
+ "primary",
117
+ "openActions",
118
+ "closeActions",
119
+ "back",
120
+ "openDropdown",
121
+ "chooseDropdown",
122
+ ].includes(v.type)
123
+ ? ["type"]
124
+ : undefined;
125
+ return (!!shape &&
126
+ fields(v, shape, (key, value) => {
127
+ if (key === "direction")
128
+ return ["up", "down", "left", "right"].includes(value);
129
+ if (key === "delta")
130
+ return typeof value === "number" && Number.isFinite(value);
131
+ if (key === "index")
132
+ return typeof value === "number" && Number.isFinite(value);
133
+ if (key === "confirm" || key === "remember")
134
+ return typeof value === "boolean";
135
+ if (key === "key")
136
+ return (object(value) &&
137
+ fields(value, ["name", "ctrl?", "meta?", "shift?", "option?", "super?"], (name, x) => name === "name" ? string(x) : typeof x === "boolean"));
138
+ return string(value);
139
+ }));
140
+ }
141
+ /** Total boundary validator; unknown fields and methods are rejected. */
142
+ export function parseEmbedderMessage(value) {
143
+ try {
144
+ if (valid(value))
145
+ return { ok: true, message: value };
146
+ }
147
+ catch {
148
+ /* Cyclic/non-JSON input is invalid too. */
149
+ }
150
+ return { ok: false, error: "Invalid embedder message or request params" };
151
+ }
@@ -0,0 +1,20 @@
1
+ import type { HostMessage, RuntimeMessage } from "./messages.js";
2
+ /** 8 MiB, per docs/decisions/0001: lines have a size cap. */
3
+ export declare const DEFAULT_MAX_LINE_BYTES: number;
4
+ export type WireMessage = HostMessage | RuntimeMessage;
5
+ /** One message as one JSON line, ready to write to the stream. */
6
+ export declare function encodeMessage(message: WireMessage): string;
7
+ export type LineDecoderOptions = {
8
+ /** Longest accepted line in bytes, terminator excluded. */
9
+ maxLineBytes?: number;
10
+ /** Called once per non-blank line with the parsed JSON value, unvalidated. */
11
+ onLine: (value: unknown) => void;
12
+ /** Called once when the stream is unusable; no callback follows. */
13
+ onError: (error: string) => void;
14
+ };
15
+ export type LineDecoder = {
16
+ push(chunk: string | Uint8Array): void;
17
+ /** Closes the stream; a line left without its newline is an error. */
18
+ end(): void;
19
+ };
20
+ export declare function createLineDecoder(options: LineDecoderOptions): LineDecoder;