@jigs.design/server 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/inputs.js ADDED
@@ -0,0 +1,224 @@
1
+ import { lineAt, readArguments } from "./literal.js";
2
+ import { z } from "zod";
3
+ //#region src/inputs.ts
4
+ /** A handle declared as `const name = useMutation("procedure")`. */
5
+ const MUTATION_HANDLE = /(?<![\w$.])(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*useMutation\(\s*(["'`])([A-Za-z_$][\w$]*)\2\s*\)/g;
6
+ const QUERY_CALL = /(?<![\w$.])useQuery\(\s*(["'`])([A-Za-z_$][\w$]*)\1\s*/g;
7
+ /** At most this many problems per call, so one bad literal does not flood the repair. */
8
+ const MAX_PROBLEMS = 3;
9
+ function known(literal) {
10
+ const out = {
11
+ open: [],
12
+ unknown: [],
13
+ value: void 0
14
+ };
15
+ const walk = (node, path) => {
16
+ switch (node.kind) {
17
+ case "array": return node.items.map((item, index) => walk(item, [...path, index]));
18
+ case "dynamic":
19
+ out.unknown.push(path);
20
+ return;
21
+ case "object":
22
+ if (node.spread) out.open.push(path);
23
+ return Object.fromEntries([...node.entries].map(([key, value]) => [key, walk(value, [...path, key])]));
24
+ case "value": return node.value;
25
+ }
26
+ };
27
+ if (literal !== void 0) out.value = walk(literal, []);
28
+ return out;
29
+ }
30
+ /** The part of `literal` at `path`, when it is written there. */
31
+ function literalAt(literal, path) {
32
+ let current = literal;
33
+ for (const part of path) if (current?.kind === "object" && !Number.isInteger(part)) current = current.entries.get(String(part));
34
+ else if (current?.kind === "array" && Number.isInteger(part)) current = current.items[Number(part)];
35
+ else return;
36
+ return current;
37
+ }
38
+ const startsWith = (path, prefix) => prefix.length <= path.length && prefix.every((part, index) => path[index] === part);
39
+ const samePath = (a, b) => a.length === b.length && startsWith(a, b);
40
+ /** A path part that is a key or an index; zod's paths may also hold symbols. */
41
+ function isKey(part) {
42
+ return typeof part !== "symbol";
43
+ }
44
+ function pathText(path) {
45
+ return path.map((part, index) => Number.isInteger(part) ? `[${part}]` : index === 0 ? part : `.${part}`).join("");
46
+ }
47
+ const jsonSchemas = /* @__PURE__ */ new WeakMap();
48
+ /**
49
+ * The JSON Schema of an input schema, as input. `defineHost` requires that
50
+ * every input has one; null when it still fails, and then only the schema's
51
+ * own parse is checked.
52
+ */
53
+ function jsonSchemaOf(schema) {
54
+ const cached = jsonSchemas.get(schema);
55
+ if (cached !== void 0) return cached;
56
+ let converted;
57
+ try {
58
+ converted = z.toJSONSchema(schema, { io: "input" });
59
+ } catch {
60
+ converted = null;
61
+ }
62
+ jsonSchemas.set(schema, converted);
63
+ return converted;
64
+ }
65
+ /** A node and the alternatives it allows, such as an object or null. */
66
+ function branches(node) {
67
+ return [node, ...(node.anyOf ?? []).flatMap((branch) => branches(branch))];
68
+ }
69
+ /** A node written as `true`, which takes anything, as a schema. */
70
+ const ANYTHING = {};
71
+ const asNode = (node) => node === true || node === false ? ANYTHING : node;
72
+ /** An object node's fields, and whether it takes keys beyond them. */
73
+ function objectFields(node) {
74
+ const objects = branches(node).filter((branch) => branch.type === "object" || branch.properties !== void 0);
75
+ if (objects.length === 0) return null;
76
+ const fields = /* @__PURE__ */ new Map();
77
+ let open = false;
78
+ for (const object of objects) {
79
+ for (const [key, field] of Object.entries(object.properties ?? {})) fields.set(key, asNode(field));
80
+ if (object.additionalProperties !== void 0 && object.additionalProperties !== false) open = true;
81
+ }
82
+ return {
83
+ fields,
84
+ open
85
+ };
86
+ }
87
+ /** An array node's element, or null for any other node. */
88
+ function arrayElement(node) {
89
+ for (const branch of branches(node)) {
90
+ const { items } = branch;
91
+ if (items !== void 0 && !Array.isArray(items)) return asNode(items);
92
+ }
93
+ return null;
94
+ }
95
+ /** The node that checks the value at `path`, when objects and arrays lead there. */
96
+ function nodeAt(root, path) {
97
+ let current = root;
98
+ for (const part of path) {
99
+ if (current === null) return null;
100
+ current = Number.isInteger(part) ? arrayElement(current) : objectFields(current)?.fields.get(String(part)) ?? null;
101
+ }
102
+ return current;
103
+ }
104
+ /** Keys written in `literal` that no object node on its path has. */
105
+ function unknownKeys(node, literal, path) {
106
+ const element = arrayElement(node);
107
+ if (literal.kind === "array" && element !== null) return literal.items.flatMap((item, index) => unknownKeys(element, item, [...path, index]));
108
+ const object = objectFields(node);
109
+ if (literal.kind !== "object" || object === null) return [];
110
+ const found = [];
111
+ for (const [key, value] of literal.entries) {
112
+ const field = object.fields.get(key);
113
+ if (field !== void 0) found.push(...unknownKeys(field, value, [...path, key]));
114
+ else if (!object.open) found.push({
115
+ fields: [...object.fields.keys()],
116
+ path: [...path, key]
117
+ });
118
+ }
119
+ return found;
120
+ }
121
+ /** The host's `.describe()` text for a field. */
122
+ function descriptionOf(node) {
123
+ return node === null ? void 0 : branches(node).find((branch) => branch.description !== void 0)?.description;
124
+ }
125
+ /** What is wrong with one issue, in a few words a model can act on. */
126
+ function problem(issue, written, root) {
127
+ const path = issue.path.filter(isKey);
128
+ const field = pathText(path);
129
+ const name = field === "" ? "the input" : field;
130
+ const shown = written === void 0 ? "nothing" : JSON.stringify(known(written).value);
131
+ const hint = descriptionOf(nodeAt(root, path));
132
+ const about = hint === void 0 ? "" : ` (${hint})`;
133
+ if (written === void 0 && issue.code === "invalid_type") {
134
+ const object = field === "" && root !== null ? objectFields(root) : null;
135
+ return object === null ? `${name} is missing${about}` : `it needs an input: { ${[...object.fields.keys()].join(", ")} }`;
136
+ }
137
+ switch (issue.code) {
138
+ case "invalid_format": return issue.format === "regex" ? `${name} must match ${issue.pattern ?? "its pattern"}${about}, not ${shown}` : `${name} must be a ${issue.format}${about}, not ${shown}`;
139
+ case "invalid_type": return `${name} must be ${issue.expected === "number" ? "a number" : issue.expected}, not ${shown}${about}`;
140
+ case "invalid_value": return `${name} must be one of ${issue.values.map((value) => JSON.stringify(value)).join(", ")}, not ${shown}`;
141
+ default: return `${name}: ${issue.message}${about}`;
142
+ }
143
+ }
144
+ /**
145
+ * Problems with an input written as a literal, by the procedure's schema:
146
+ * unknown keys, and values the schema refuses. Anything computed is left to
147
+ * run time.
148
+ */
149
+ function literalProblems(schema, literal) {
150
+ const problems = [];
151
+ const root = jsonSchemaOf(schema);
152
+ if (literal !== void 0) for (const extra of root === null ? [] : unknownKeys(root, literal, [])) {
153
+ const where = extra.path.slice(0, -1);
154
+ const takes = extra.fields.length === 0 ? "takes no fields" : `takes ${extra.fields.join(", ")}`;
155
+ problems.push(`there is no ${pathText(extra.path)}; ${where.length === 0 ? "the input" : pathText(where)} ${takes}`);
156
+ }
157
+ const facts = known(literal);
158
+ let parsed;
159
+ try {
160
+ parsed = z.safeParse(schema, facts.value);
161
+ } catch {
162
+ return problems;
163
+ }
164
+ if (parsed.success) return problems;
165
+ for (const issue of parsed.error.issues) {
166
+ const path = issue.path.filter(isKey);
167
+ const written = literalAt(literal, path);
168
+ const computed = facts.unknown.some((prefix) => startsWith(path, prefix));
169
+ const spread = written === void 0 && facts.open.some((open) => samePath(open, path.slice(0, -1)));
170
+ const unionUnknown = issue.code === "invalid_union" && [...facts.unknown, ...facts.open].some((inner) => startsWith(inner, path));
171
+ if (issue.code !== "unrecognized_keys" && !computed && !spread && !unionUnknown) problems.push(problem(issue, written, root));
172
+ }
173
+ return problems;
174
+ }
175
+ /**
176
+ * Input written as a literal in `useQuery("name", input)` or a mutation
177
+ * handle's `mutate(input)`, checked against the procedure's input schema.
178
+ * Each problem is an error, so the model repairs it instead of the spec
179
+ * failing at run time with `BAD_INPUT` or silently reading nothing.
180
+ */
181
+ function inputDiagnostics(host, source) {
182
+ const diagnostics = [];
183
+ const procedureOf = (name) => Object.hasOwn(host.procedures, name) ? host.procedures[name] : void 0;
184
+ const report = (index, subject, problems) => {
185
+ for (const message of problems.slice(0, MAX_PROBLEMS)) diagnostics.push({
186
+ code: "procedure_input",
187
+ message: `${subject}: ${message}`,
188
+ severity: "error",
189
+ span: { line: lineAt(source, index) }
190
+ });
191
+ };
192
+ for (const match of source.matchAll(QUERY_CALL)) {
193
+ const name = match[2] ?? "";
194
+ const procedure = procedureOf(name);
195
+ if (procedure?.kind !== "query" || procedure.input === void 0) continue;
196
+ const rest = match.index + match[0].length;
197
+ const char = source.charAt(rest);
198
+ if (char !== "," && char !== ")") continue;
199
+ const [input] = char === ")" ? [] : readArguments(source, rest + 1);
200
+ if (input?.kind === "dynamic") continue;
201
+ report(match.index, `useQuery("${name}")`, literalProblems(procedure.input, input));
202
+ }
203
+ const handles = /* @__PURE__ */ new Map();
204
+ for (const match of source.matchAll(MUTATION_HANDLE)) {
205
+ const handle = match[1] ?? "";
206
+ const name = match[3] ?? "";
207
+ const before = handles.get(handle);
208
+ handles.set(handle, before === void 0 || before === name ? name : null);
209
+ }
210
+ for (const [handle, name] of handles) {
211
+ const procedure = name === null ? void 0 : procedureOf(name);
212
+ if (name === null || procedure === void 0 || procedure.kind === "query" || procedure.input === void 0) continue;
213
+ const escaped = handle.replaceAll("$", String.raw`\$`);
214
+ const call = new RegExp(String.raw`(?<![\w$.])${escaped}\s*\.\s*mutate\(`, "g");
215
+ for (const match of source.matchAll(call)) {
216
+ const [input] = readArguments(source, match.index + match[0].length);
217
+ if (input?.kind === "dynamic") continue;
218
+ report(match.index, `${handle}.mutate() for "${name}"`, literalProblems(procedure.input, input));
219
+ }
220
+ }
221
+ return diagnostics;
222
+ }
223
+ //#endregion
224
+ export { branches, inputDiagnostics, objectFields };
@@ -0,0 +1,39 @@
1
+ import { z } from "zod";
2
+ //#region src/invalidation.ts
3
+ const json = z.json();
4
+ /** One scope's targets; throws when it fails or returns something not JSON. */
5
+ function scoped(name, scope, input, output) {
6
+ const result = scope(input, output);
7
+ if (result === true) return [{ name }];
8
+ return (Array.isArray(result) ? result : [result]).map((entry) => {
9
+ if (!json.safeParse(entry).success) throw new Error(`The invalidates scope for "${name}" returned a value that is not JSON`);
10
+ return {
11
+ input: entry,
12
+ name
13
+ };
14
+ });
15
+ }
16
+ /**
17
+ * The queries a successful mutation refreshes, with the inputs its scopes
18
+ * pick. A scope that throws refreshes its whole query, after `report`.
19
+ */
20
+ function invalidationsOf(procedure, input, output, report) {
21
+ const targets = [];
22
+ for (const name of procedure.invalidates) {
23
+ const scope = procedure.scopes[name];
24
+ if (scope === void 0 || scope === true) {
25
+ targets.push({ name });
26
+ continue;
27
+ }
28
+ try {
29
+ const run = scope;
30
+ targets.push(...scoped(name, run, input, output));
31
+ } catch (error) {
32
+ report(error instanceof Error ? error : new Error(String(error), { cause: error }));
33
+ targets.push({ name });
34
+ }
35
+ }
36
+ return targets;
37
+ }
38
+ //#endregion
39
+ export { invalidationsOf };
@@ -0,0 +1,258 @@
1
+ //#region src/literal.ts
2
+ const IDENTIFIER = /[A-Za-z_$][\w$]*/y;
3
+ const NUMBER = /-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/y;
4
+ const KEYWORD = /(true|false|null)(?![\w$])/y;
5
+ const SPACE = /\s*/y;
6
+ const DYNAMIC = { kind: "dynamic" };
7
+ /** Objects and arrays nested deeper than this are read as `dynamic`, so deep nesting cannot overflow the stack. */
8
+ const MAX_DEPTH = 64;
9
+ function skipSpace(source, index) {
10
+ if (index >= source.length) return source.length;
11
+ SPACE.lastIndex = index;
12
+ SPACE.exec(source);
13
+ return SPACE.lastIndex;
14
+ }
15
+ /** Index just past the string or template literal that starts at `index`. */
16
+ function skipQuoted(source, index) {
17
+ const quote = source.charAt(index);
18
+ let at = index + 1;
19
+ let depth = 0;
20
+ while (at < source.length) {
21
+ const char = source.charAt(at);
22
+ if (char === "\\") {
23
+ at += 2;
24
+ continue;
25
+ }
26
+ if (quote === "`" && char === "$" && source.charAt(at + 1) === "{") {
27
+ depth++;
28
+ at += 2;
29
+ continue;
30
+ }
31
+ if (depth > 0 && char === "}") depth--;
32
+ else if (depth === 0 && char === quote) return at + 1;
33
+ at++;
34
+ }
35
+ return Math.min(at, source.length);
36
+ }
37
+ /**
38
+ * Index of the `,` or closing bracket that ends the expression starting at
39
+ * `index`, skipping nested brackets and strings.
40
+ */
41
+ function skipExpression(source, index) {
42
+ let depth = 0;
43
+ let at = index;
44
+ while (at < source.length) {
45
+ const char = source.charAt(at);
46
+ if (char === "\"" || char === "'" || char === "`") {
47
+ at = skipQuoted(source, at);
48
+ continue;
49
+ }
50
+ if (char === "(" || char === "[" || char === "{") depth++;
51
+ else if (char === ")" || char === "]" || char === "}") {
52
+ if (depth === 0) return at;
53
+ depth--;
54
+ } else if (char === "," && depth === 0) return at;
55
+ at++;
56
+ }
57
+ return at;
58
+ }
59
+ /** The 1-based line of `source` that `index` is on. */
60
+ function lineAt(source, index) {
61
+ let line = 1;
62
+ for (let at = source.indexOf("\n"); at !== -1 && at < index;) {
63
+ line++;
64
+ at = source.indexOf("\n", at + 1);
65
+ }
66
+ return line;
67
+ }
68
+ /** A string literal's value, or null when it has escapes this does not read. */
69
+ function stringValue(text) {
70
+ const body = text.slice(1, -1);
71
+ if (!body.includes("\\")) return body;
72
+ if (text.startsWith("\"")) try {
73
+ return JSON.parse(text);
74
+ } catch {
75
+ return null;
76
+ }
77
+ return null;
78
+ }
79
+ /** Whether the expression goes on past a value just read, as in `"a" + b`. */
80
+ function continues(source, index) {
81
+ const next = source.charAt(skipSpace(source, index));
82
+ return next !== "" && next !== "," && next !== ")" && next !== "]" && next !== "}";
83
+ }
84
+ /** The value written at `index`, `depth` objects and arrays deep, and where it ends. */
85
+ function readValue(source, index, depth) {
86
+ const start = skipSpace(source, index);
87
+ const char = source.charAt(start);
88
+ let read = null;
89
+ if (char === "{" || char === "[") {
90
+ if (depth < MAX_DEPTH) read = char === "{" ? readObject(source, start, depth) : readArray(source, start, depth);
91
+ } else if (char === "\"" || char === "'" || char === "`") {
92
+ const end = skipQuoted(source, start);
93
+ const text = source.slice(start, end);
94
+ const value = char === "`" ? text.includes("${") || text.includes("\\") ? null : text.slice(1, -1) : stringValue(text);
95
+ read = {
96
+ end,
97
+ literal: value === null ? DYNAMIC : {
98
+ kind: "value",
99
+ value
100
+ }
101
+ };
102
+ } else {
103
+ NUMBER.lastIndex = start;
104
+ const number = NUMBER.exec(source);
105
+ KEYWORD.lastIndex = start;
106
+ const keyword = number === null ? KEYWORD.exec(source) : null;
107
+ if (number !== null && !/[\w$.]/.test(source.charAt(NUMBER.lastIndex))) read = {
108
+ end: NUMBER.lastIndex,
109
+ literal: {
110
+ kind: "value",
111
+ value: Number(number[0])
112
+ }
113
+ };
114
+ else if (keyword !== null) {
115
+ const word = keyword[1];
116
+ read = {
117
+ end: KEYWORD.lastIndex,
118
+ literal: {
119
+ kind: "value",
120
+ value: word === "null" ? null : word === "true"
121
+ }
122
+ };
123
+ }
124
+ }
125
+ if (read === null || continues(source, read.end)) return {
126
+ end: skipExpression(source, start),
127
+ literal: DYNAMIC
128
+ };
129
+ return read;
130
+ }
131
+ function readObject(source, start, depth) {
132
+ const entries = /* @__PURE__ */ new Map();
133
+ let spread = false;
134
+ let at = start + 1;
135
+ let last = -1;
136
+ for (;;) {
137
+ at = skipSpace(source, at);
138
+ if (at <= last) return {
139
+ end: at,
140
+ literal: DYNAMIC
141
+ };
142
+ last = at;
143
+ const char = source.charAt(at);
144
+ if (char === "}") return {
145
+ end: at + 1,
146
+ literal: {
147
+ entries,
148
+ kind: "object",
149
+ spread
150
+ }
151
+ };
152
+ if (char === "") return {
153
+ end: at,
154
+ literal: DYNAMIC
155
+ };
156
+ if (char === ",") {
157
+ at++;
158
+ continue;
159
+ }
160
+ if (source.startsWith("...", at)) {
161
+ spread = true;
162
+ at = skipExpression(source, at + 3);
163
+ continue;
164
+ }
165
+ let key = null;
166
+ if (char === "\"" || char === "'") {
167
+ const end = skipQuoted(source, at);
168
+ key = stringValue(source.slice(at, end));
169
+ at = end;
170
+ } else {
171
+ IDENTIFIER.lastIndex = at;
172
+ const name = IDENTIFIER.exec(source);
173
+ if (name !== null) {
174
+ key = name[0];
175
+ at = IDENTIFIER.lastIndex;
176
+ }
177
+ }
178
+ at = skipSpace(source, at);
179
+ if (key === null) {
180
+ spread = true;
181
+ at = skipExpression(source, at);
182
+ continue;
183
+ }
184
+ const after = source.charAt(at);
185
+ if (after === ":") {
186
+ const value = readValue(source, at + 1, depth + 1);
187
+ entries.set(key, value.literal);
188
+ at = value.end;
189
+ } else if (after === "," || after === "}") entries.set(key, DYNAMIC);
190
+ else {
191
+ entries.set(key, DYNAMIC);
192
+ at = skipExpression(source, at);
193
+ }
194
+ }
195
+ }
196
+ function readArray(source, start, depth) {
197
+ const items = [];
198
+ let spread = false;
199
+ let at = start + 1;
200
+ let last = -1;
201
+ for (;;) {
202
+ at = skipSpace(source, at);
203
+ if (at <= last) return {
204
+ end: at,
205
+ literal: DYNAMIC
206
+ };
207
+ last = at;
208
+ const char = source.charAt(at);
209
+ if (char === "]") return {
210
+ end: at + 1,
211
+ literal: spread ? DYNAMIC : {
212
+ items,
213
+ kind: "array"
214
+ }
215
+ };
216
+ if (char === "") return {
217
+ end: at,
218
+ literal: DYNAMIC
219
+ };
220
+ if (char === ",") {
221
+ at++;
222
+ continue;
223
+ }
224
+ if (source.startsWith("...", at)) {
225
+ spread = true;
226
+ at = skipExpression(source, at + 3);
227
+ continue;
228
+ }
229
+ const value = readValue(source, at, depth + 1);
230
+ items.push(value.literal);
231
+ at = value.end;
232
+ }
233
+ }
234
+ /**
235
+ * The arguments of a call whose `(` is just before `index`, up to its `)`:
236
+ * each read as a literal where it is one.
237
+ */
238
+ function readArguments(source, index) {
239
+ const args = [];
240
+ let at = index;
241
+ let last = -1;
242
+ for (;;) {
243
+ at = skipSpace(source, at);
244
+ if (at <= last) return args;
245
+ last = at;
246
+ const char = source.charAt(at);
247
+ if (char === ")" || char === "") return args;
248
+ if (char === ",") {
249
+ at++;
250
+ continue;
251
+ }
252
+ const value = readValue(source, at, 0);
253
+ args.push(value.literal);
254
+ at = value.end;
255
+ }
256
+ }
257
+ //#endregion
258
+ export { lineAt, readArguments };
package/dist/node.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ import { IncomingMessage, ServerResponse } from "node:http";
2
+ //#region src/node.d.ts
3
+ /** A request as Express hands it over: `body` is set when a body parser already ran. */
4
+ type NodeRequest = IncomingMessage & {
5
+ body?: unknown;
6
+ originalUrl?: string;
7
+ };
8
+ interface HandlerOwner {
9
+ handler: (request: Request) => Promise<Response>;
10
+ maxBodyBytes: number;
11
+ }
12
+ /**
13
+ * Mounts `pickle.handler` on Express or plain Node. It builds a standard
14
+ * `Request`, so `context` is the same function on every platform, and aborts
15
+ * the request's signal when the connection closes early.
16
+ */
17
+ export declare function nodeHandler(pickle: HandlerOwner): (request: NodeRequest, response: ServerResponse) => void;
18
+ //#endregion
19
+ export {};
package/dist/node.js ADDED
@@ -0,0 +1,91 @@
1
+ import { once } from "node:events";
2
+ //#region src/node.ts
3
+ /**
4
+ * The raw body, read only up to one byte past `max`, so the handler refuses
5
+ * an oversized body without this holding it in memory.
6
+ */
7
+ async function readBody(request, max) {
8
+ if (request.method === "GET" || request.method === "HEAD") return null;
9
+ if (request.body !== void 0) return JSON.stringify(request.body);
10
+ const chunks = [];
11
+ let size = 0;
12
+ for await (const chunk of request) {
13
+ const buffer = Buffer.from(chunk);
14
+ chunks.push(buffer);
15
+ size += buffer.byteLength;
16
+ if (size > max) {
17
+ request.destroy();
18
+ break;
19
+ }
20
+ }
21
+ return Uint8Array.from(Buffer.concat(chunks));
22
+ }
23
+ /**
24
+ * Waits for room to write: true on `drain`, false when the connection closes
25
+ * first, which a destroyed response does instead of draining.
26
+ */
27
+ async function drained(response) {
28
+ if (response.destroyed) return false;
29
+ const stop = new AbortController();
30
+ try {
31
+ return await Promise.race([once(response, "drain", { signal: stop.signal }).then(() => true), once(response, "close", { signal: stop.signal }).then(() => false)]);
32
+ } finally {
33
+ stop.abort();
34
+ }
35
+ }
36
+ function toHeaders(request) {
37
+ const headers = new Headers();
38
+ for (const [name, value] of Object.entries(request.headers)) if (Array.isArray(value)) for (const item of value) headers.append(name, item);
39
+ else if (value !== void 0) headers.set(name, value);
40
+ return headers;
41
+ }
42
+ /**
43
+ * Mounts `pickle.handler` on Express or plain Node. It builds a standard
44
+ * `Request`, so `context` is the same function on every platform, and aborts
45
+ * the request's signal when the connection closes early.
46
+ */
47
+ function nodeHandler(pickle) {
48
+ return (request, response) => {
49
+ const abort = new AbortController();
50
+ response.on("close", () => {
51
+ if (!response.writableFinished) abort.abort();
52
+ });
53
+ (async () => {
54
+ try {
55
+ const encrypted = "encrypted" in request.socket;
56
+ const url = new URL(request.originalUrl ?? request.url ?? "/", `${encrypted ? "https" : "http"}://${request.headers.host ?? "localhost"}`);
57
+ const body = await readBody(request, pickle.maxBodyBytes);
58
+ const init = {
59
+ headers: toHeaders(request),
60
+ method: request.method ?? "GET",
61
+ signal: abort.signal
62
+ };
63
+ if (body !== null) init.body = body;
64
+ const reply = await pickle.handler(new Request(url, init));
65
+ response.statusCode = reply.status;
66
+ reply.headers.forEach((value, name) => {
67
+ response.setHeader(name, value);
68
+ });
69
+ if (reply.body === null) {
70
+ response.end();
71
+ return;
72
+ }
73
+ const reader = reply.body.getReader();
74
+ for (;;) {
75
+ const chunk = await reader.read();
76
+ if (chunk.done) break;
77
+ if (!response.write(chunk.value) && !await drained(response)) {
78
+ await reader.cancel();
79
+ return;
80
+ }
81
+ }
82
+ response.end();
83
+ } catch {
84
+ if (!response.headersSent) response.statusCode = 500;
85
+ response.end();
86
+ }
87
+ })();
88
+ };
89
+ }
90
+ //#endregion
91
+ export { nodeHandler };