@jigs.design/server 0.2.0 → 0.3.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/README.md +19 -13
- package/dist/chat.d.ts +2 -2
- package/dist/chat.js +46 -34
- package/dist/compile-cache.js +60 -0
- package/dist/compile.d.ts +1 -1
- package/dist/compile.js +182 -40
- package/dist/{server.d.ts → core.d.ts} +7 -3
- package/dist/core.js +722 -0
- package/dist/errors.d.ts +2 -2
- package/dist/errors.js +3 -3
- package/dist/events.js +129 -0
- package/dist/handler.d.ts +4 -0
- package/dist/handler.js +344 -0
- package/dist/index.d.ts +5 -5
- package/dist/index.js +3 -2
- package/dist/inputs.js +16 -1
- package/dist/model-server.js +155 -0
- package/dist/session.d.ts +1 -2
- package/dist/session.js +116 -52
- package/dist/types.d.ts +292 -55
- package/package.json +6 -6
- package/dist/server.js +0 -786
package/dist/errors.d.ts
CHANGED
|
@@ -10,8 +10,8 @@ export declare class JigsError extends Error {
|
|
|
10
10
|
constructor(code: ThrownCode, message: string);
|
|
11
11
|
}
|
|
12
12
|
/**
|
|
13
|
-
* A source that failed a strict compile, or a
|
|
14
|
-
*
|
|
13
|
+
* A source that failed a strict compile, or a page that failed at startup
|
|
14
|
+
* or at its first call. `diagnostics` holds the error and warning diagnostics; the message
|
|
15
15
|
* lists them with their lines.
|
|
16
16
|
*/
|
|
17
17
|
export declare class JigsCompileError extends Error {
|
package/dist/errors.js
CHANGED
|
@@ -13,13 +13,13 @@ var JigsError = class extends Error {
|
|
|
13
13
|
function isJigsError(cause) {
|
|
14
14
|
return typeof cause === "object" && cause !== null && BRAND in cause;
|
|
15
15
|
}
|
|
16
|
-
/** Diagnostics that make a
|
|
16
|
+
/** Diagnostics that make a page or a strict compile fail. */
|
|
17
17
|
function fatalDiagnostics(diagnostics) {
|
|
18
18
|
return diagnostics.filter((diagnostic) => diagnostic.severity === "error" || diagnostic.severity === "warning");
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
21
|
-
* A source that failed a strict compile, or a
|
|
22
|
-
*
|
|
21
|
+
* A source that failed a strict compile, or a page that failed at startup
|
|
22
|
+
* or at its first call. `diagnostics` holds the error and warning diagnostics; the message
|
|
23
23
|
* lists them with their lines.
|
|
24
24
|
*/
|
|
25
25
|
var JigsCompileError = class extends Error {
|
package/dist/events.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { INTERNAL_MESSAGE } from "./errors.js";
|
|
2
|
+
import { sseFrame, sseResponse } from "./handler.js";
|
|
3
|
+
//#region src/events.ts
|
|
4
|
+
/** The longest `Last-Event-ID`, and event id, the stream accepts. */
|
|
5
|
+
const MAX_EVENT_ID = 200;
|
|
6
|
+
const PRINTABLE = /^[ -~]*$/;
|
|
7
|
+
/** The reconnect delay the stream asks for: 3 s, plus up to 2 s so a fleet of tabs spreads out. */
|
|
8
|
+
const RETRY_MS = 3e3;
|
|
9
|
+
const RETRY_JITTER_MS = 2e3;
|
|
10
|
+
const isEventId = (id) => id.length <= MAX_EVENT_ID && PRINTABLE.test(id);
|
|
11
|
+
/** An event as the client reads it: a page's `saved` mark stays here. */
|
|
12
|
+
function onTheWire(event) {
|
|
13
|
+
if (event.type !== "invalidate-pages") return event;
|
|
14
|
+
return {
|
|
15
|
+
pages: event.pages.map((page) => page.props === void 0 ? { name: page.name } : {
|
|
16
|
+
name: page.name,
|
|
17
|
+
props: page.props
|
|
18
|
+
}),
|
|
19
|
+
type: event.type
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/** The client's `Last-Event-ID`, or null when it has none or one out of bounds. */
|
|
23
|
+
function sinceOf(request) {
|
|
24
|
+
const since = request.headers.get("last-event-id");
|
|
25
|
+
return since === null || since === "" || !isEventId(since) ? null : since;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Text frames written as they come and read by the response one at a time:
|
|
29
|
+
* `push` queues one, `end` ends the iteration once the queue drains.
|
|
30
|
+
*/
|
|
31
|
+
function frameQueue() {
|
|
32
|
+
const queued = [];
|
|
33
|
+
let ended = false;
|
|
34
|
+
let wake = null;
|
|
35
|
+
const notify = () => {
|
|
36
|
+
wake?.();
|
|
37
|
+
wake = null;
|
|
38
|
+
};
|
|
39
|
+
return {
|
|
40
|
+
end: () => {
|
|
41
|
+
ended = true;
|
|
42
|
+
notify();
|
|
43
|
+
},
|
|
44
|
+
get ended() {
|
|
45
|
+
return ended;
|
|
46
|
+
},
|
|
47
|
+
async *frames() {
|
|
48
|
+
for (;;) {
|
|
49
|
+
const next = queued.shift();
|
|
50
|
+
if (next !== void 0) {
|
|
51
|
+
yield next;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (ended) return;
|
|
55
|
+
await new Promise((resolve) => {
|
|
56
|
+
wake = resolve;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
push: (frame) => {
|
|
61
|
+
if (ended) return;
|
|
62
|
+
queued.push(frame);
|
|
63
|
+
notify();
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* One client's push stream: runs the host's `events` with a `send` that
|
|
69
|
+
* writes to it, and the unsubscribe it returned once the stream ends.
|
|
70
|
+
*/
|
|
71
|
+
async function eventStream(deps, request, context) {
|
|
72
|
+
const queue = frameQueue();
|
|
73
|
+
const abort = new AbortController();
|
|
74
|
+
const leave = () => abort.abort();
|
|
75
|
+
if (context.signal.aborted) abort.abort();
|
|
76
|
+
else context.signal.addEventListener("abort", leave, { once: true });
|
|
77
|
+
let unsubscribe = null;
|
|
78
|
+
let unsubscribed = false;
|
|
79
|
+
const stop = () => {
|
|
80
|
+
context.signal.removeEventListener("abort", leave);
|
|
81
|
+
queue.end();
|
|
82
|
+
if (!unsubscribed && unsubscribe !== null) {
|
|
83
|
+
unsubscribed = true;
|
|
84
|
+
try {
|
|
85
|
+
unsubscribe();
|
|
86
|
+
} catch (error) {
|
|
87
|
+
deps.onError(error, { type: "events" });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
abort.signal.addEventListener("abort", stop, { once: true });
|
|
92
|
+
const send = (event, options) => {
|
|
93
|
+
if (options?.id !== void 0 && !isEventId(options.id)) throw new Error(`An event id must be at most ${MAX_EVENT_ID} printable characters; got "${options.id.slice(0, 40)}"`);
|
|
94
|
+
queue.push(sseFrame(onTheWire(event), options?.id));
|
|
95
|
+
};
|
|
96
|
+
const close = () => {
|
|
97
|
+
queue.push(sseFrame({ type: "close" }));
|
|
98
|
+
queue.end();
|
|
99
|
+
};
|
|
100
|
+
try {
|
|
101
|
+
unsubscribe = await deps.events({
|
|
102
|
+
close,
|
|
103
|
+
ctx: context.ctx,
|
|
104
|
+
send,
|
|
105
|
+
signal: abort.signal,
|
|
106
|
+
since: sinceOf(request)
|
|
107
|
+
});
|
|
108
|
+
} catch (error) {
|
|
109
|
+
deps.onError(error, { type: "events" });
|
|
110
|
+
abort.abort();
|
|
111
|
+
return deps.http.failure(request, {
|
|
112
|
+
code: "INTERNAL",
|
|
113
|
+
message: INTERNAL_MESSAGE
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
if (abort.signal.aborted) stop();
|
|
117
|
+
return sseResponse(deps.http, request, queue.frames(), {
|
|
118
|
+
abort,
|
|
119
|
+
cleanup: () => abort.abort(),
|
|
120
|
+
failed: (cause) => {
|
|
121
|
+
deps.onError(cause, { type: "events" });
|
|
122
|
+
return sseFrame({ type: "close" });
|
|
123
|
+
},
|
|
124
|
+
retryMs: RETRY_MS + Math.floor(Math.random() * RETRY_JITTER_MS),
|
|
125
|
+
vary: "Accept"
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
//#endregion
|
|
129
|
+
export { eventStream };
|
package/dist/handler.js
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { INTERNAL_MESSAGE, statusOf } from "./errors.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
//#region src/handler.ts
|
|
4
|
+
const VERSION_HEADER = "jigs-version";
|
|
5
|
+
const HEARTBEAT_MS = 15e3;
|
|
6
|
+
/**
|
|
7
|
+
* The body as text, or null once it passes `max` bytes. Reading stops there,
|
|
8
|
+
* so an oversized body is never held in memory.
|
|
9
|
+
*/
|
|
10
|
+
async function readLimited(request, max) {
|
|
11
|
+
if (request.body === null) return "";
|
|
12
|
+
const reader = request.body.getReader();
|
|
13
|
+
const chunks = [];
|
|
14
|
+
let size = 0;
|
|
15
|
+
for (;;) {
|
|
16
|
+
const chunk = await reader.read();
|
|
17
|
+
if (chunk.done) break;
|
|
18
|
+
size += chunk.value.byteLength;
|
|
19
|
+
if (size > max) {
|
|
20
|
+
await reader.cancel();
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
chunks.push(chunk.value);
|
|
24
|
+
}
|
|
25
|
+
const bytes = new Uint8Array(size);
|
|
26
|
+
let offset = 0;
|
|
27
|
+
for (const chunk of chunks) {
|
|
28
|
+
bytes.set(chunk, offset);
|
|
29
|
+
offset += chunk.byteLength;
|
|
30
|
+
}
|
|
31
|
+
return new TextDecoder().decode(bytes);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* An origin such as `https://app.example.com`. Throws on anything else,
|
|
35
|
+
* including an entry with no `http:` or `https:` scheme: `localhost:3000`
|
|
36
|
+
* parses as a URL whose origin is `"null"`, the `Origin` that sandboxed
|
|
37
|
+
* frames and `file:` pages send.
|
|
38
|
+
*/
|
|
39
|
+
function originOf(entry) {
|
|
40
|
+
let url = null;
|
|
41
|
+
try {
|
|
42
|
+
url = new URL(entry);
|
|
43
|
+
} catch {}
|
|
44
|
+
if (url === null || url.protocol !== "http:" && url.protocol !== "https:" || url.origin === "null") throw new Error(`"${entry}" is not an origin: write its scheme and host, such as https://app.example.com`);
|
|
45
|
+
return url.origin;
|
|
46
|
+
}
|
|
47
|
+
const bodySchema = z.discriminatedUnion("type", [
|
|
48
|
+
z.object({
|
|
49
|
+
confirmation: z.string().max(1e3).optional(),
|
|
50
|
+
input: z.json().optional(),
|
|
51
|
+
kind: z.enum([
|
|
52
|
+
"action",
|
|
53
|
+
"mutation",
|
|
54
|
+
"query"
|
|
55
|
+
], { error: "Send the procedure's kind as \"kind\": \"query\", \"mutation\" or \"action\"" }),
|
|
56
|
+
name: z.string().max(200),
|
|
57
|
+
type: z.literal("call")
|
|
58
|
+
}),
|
|
59
|
+
z.object({
|
|
60
|
+
document: z.json().optional(),
|
|
61
|
+
messages: z.json(),
|
|
62
|
+
options: z.json().optional(),
|
|
63
|
+
type: z.literal("session")
|
|
64
|
+
}),
|
|
65
|
+
z.object({
|
|
66
|
+
name: z.string().min(1).max(200),
|
|
67
|
+
props: z.record(z.string(), z.union([
|
|
68
|
+
z.boolean(),
|
|
69
|
+
z.number(),
|
|
70
|
+
z.string()
|
|
71
|
+
]), { error: "Send props as an object of strings, numbers and booleans" }).optional(),
|
|
72
|
+
type: z.literal("page")
|
|
73
|
+
})
|
|
74
|
+
]);
|
|
75
|
+
/**
|
|
76
|
+
* Runs a `safeParse`, with a value nested too deeply to walk reported as a
|
|
77
|
+
* failure instead of a thrown `RangeError`.
|
|
78
|
+
*/
|
|
79
|
+
function parseBounded(parse) {
|
|
80
|
+
let parsed;
|
|
81
|
+
try {
|
|
82
|
+
parsed = parse();
|
|
83
|
+
} catch {
|
|
84
|
+
return {
|
|
85
|
+
message: "The request body is nested too deeply.",
|
|
86
|
+
success: false
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
return parsed.success ? {
|
|
90
|
+
data: parsed.data,
|
|
91
|
+
success: true
|
|
92
|
+
} : {
|
|
93
|
+
message: issuesText(parsed.error),
|
|
94
|
+
success: false
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function issuesText(error) {
|
|
98
|
+
return error.issues.slice(0, 3).map((issue) => issue.path.length === 0 ? issue.message : `${issue.path.join(".")}: ${issue.message}`).join("; ");
|
|
99
|
+
}
|
|
100
|
+
function createHttp(options) {
|
|
101
|
+
const serverOrigin = (request) => options.ownOrigin ?? new URL(request.url).origin;
|
|
102
|
+
const corsHeaders = (request) => {
|
|
103
|
+
const headers = new Headers({ [VERSION_HEADER]: options.version });
|
|
104
|
+
const origin = request.headers.get("origin");
|
|
105
|
+
if (origin !== null && options.allowedOrigins.has(origin)) {
|
|
106
|
+
headers.set("access-control-allow-origin", origin);
|
|
107
|
+
headers.set("access-control-allow-credentials", "true");
|
|
108
|
+
headers.set("access-control-expose-headers", VERSION_HEADER);
|
|
109
|
+
headers.append("vary", "Origin");
|
|
110
|
+
}
|
|
111
|
+
return headers;
|
|
112
|
+
};
|
|
113
|
+
const json = (request, status, value) => {
|
|
114
|
+
const headers = corsHeaders(request);
|
|
115
|
+
headers.set("content-type", "application/json");
|
|
116
|
+
headers.set("cache-control", "no-store");
|
|
117
|
+
return new Response(JSON.stringify(value), {
|
|
118
|
+
headers,
|
|
119
|
+
status
|
|
120
|
+
});
|
|
121
|
+
};
|
|
122
|
+
const foreignOrigin = (request) => {
|
|
123
|
+
const origin = request.headers.get("origin");
|
|
124
|
+
return origin !== null && origin !== serverOrigin(request) && !options.allowedOrigins.has(origin) ? {
|
|
125
|
+
code: "FORBIDDEN",
|
|
126
|
+
message: "This origin may not call this server."
|
|
127
|
+
} : null;
|
|
128
|
+
};
|
|
129
|
+
const refusal = (request) => {
|
|
130
|
+
const foreign = foreignOrigin(request);
|
|
131
|
+
if (foreign !== null) return foreign;
|
|
132
|
+
if (!(request.headers.get("content-type") ?? "").toLowerCase().startsWith("application/json")) return {
|
|
133
|
+
code: "BAD_INPUT",
|
|
134
|
+
message: "Send the request body as application/json."
|
|
135
|
+
};
|
|
136
|
+
return null;
|
|
137
|
+
};
|
|
138
|
+
return {
|
|
139
|
+
corsHeaders,
|
|
140
|
+
failure: (request, error) => json(request, statusOf(error.code), { error }),
|
|
141
|
+
foreignOrigin,
|
|
142
|
+
json,
|
|
143
|
+
refusal
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/** One server-sent event: an optional `id:` line, then its JSON as `data:`. */
|
|
147
|
+
function sseFrame(value, id) {
|
|
148
|
+
const data = `data: ${JSON.stringify(value)}\n\n`;
|
|
149
|
+
return id === void 0 ? data : `id: ${id}\n${data}`;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* A server-sent event stream of already-framed text, with a `: keep-alive`
|
|
153
|
+
* comment every 15 s. Cancelling the body (the client left) aborts `abort`
|
|
154
|
+
* and ends `frames`. Session turns and the push stream share it.
|
|
155
|
+
*/
|
|
156
|
+
function sseResponse(http, request, frames, options) {
|
|
157
|
+
const encoder = new TextEncoder();
|
|
158
|
+
const iterator = frames[Symbol.asyncIterator]();
|
|
159
|
+
let heartbeat;
|
|
160
|
+
let open = true;
|
|
161
|
+
const finish = () => {
|
|
162
|
+
if (!open) return;
|
|
163
|
+
open = false;
|
|
164
|
+
clearInterval(heartbeat);
|
|
165
|
+
options.cleanup();
|
|
166
|
+
};
|
|
167
|
+
const body = new ReadableStream({
|
|
168
|
+
async cancel() {
|
|
169
|
+
finish();
|
|
170
|
+
options.abort.abort();
|
|
171
|
+
await iterator.return?.();
|
|
172
|
+
},
|
|
173
|
+
async pull(controller) {
|
|
174
|
+
let next;
|
|
175
|
+
try {
|
|
176
|
+
next = await iterator.next();
|
|
177
|
+
} catch (error) {
|
|
178
|
+
finish();
|
|
179
|
+
controller.enqueue(encoder.encode(options.failed(error)));
|
|
180
|
+
controller.close();
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (next.done === true) {
|
|
184
|
+
finish();
|
|
185
|
+
controller.close();
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
controller.enqueue(encoder.encode(next.value));
|
|
189
|
+
},
|
|
190
|
+
start(controller) {
|
|
191
|
+
if (options.retryMs !== void 0) controller.enqueue(encoder.encode(`retry: ${options.retryMs}\n\n`));
|
|
192
|
+
heartbeat = setInterval(() => {
|
|
193
|
+
if (!open) return;
|
|
194
|
+
try {
|
|
195
|
+
controller.enqueue(encoder.encode(": keep-alive\n\n"));
|
|
196
|
+
} catch {
|
|
197
|
+
finish();
|
|
198
|
+
}
|
|
199
|
+
}, HEARTBEAT_MS);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
const headers = http.corsHeaders(request);
|
|
203
|
+
headers.set("content-type", "text/event-stream; charset=utf-8");
|
|
204
|
+
headers.set("cache-control", "no-store");
|
|
205
|
+
headers.set("x-accel-buffering", "no");
|
|
206
|
+
if (options.vary !== void 0) headers.append("vary", options.vary);
|
|
207
|
+
return new Response(body, {
|
|
208
|
+
headers,
|
|
209
|
+
status: 200
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
/** Session events as a server-sent event stream. */
|
|
213
|
+
function sessionStream(deps, request, events, abort, cleanup) {
|
|
214
|
+
async function* frames() {
|
|
215
|
+
for await (const event of events) yield sseFrame(event);
|
|
216
|
+
}
|
|
217
|
+
return sseResponse(deps.http, request, frames(), {
|
|
218
|
+
abort,
|
|
219
|
+
cleanup,
|
|
220
|
+
failed: (cause) => {
|
|
221
|
+
deps.onError(cause, { type: "session" });
|
|
222
|
+
return sseFrame({
|
|
223
|
+
error: {
|
|
224
|
+
code: "INTERNAL",
|
|
225
|
+
message: INTERNAL_MESSAGE
|
|
226
|
+
},
|
|
227
|
+
type: "error"
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Whether a `GET` asks for the push stream: its `Accept` lists
|
|
234
|
+
* `text/event-stream` by name. A wildcard is not a match, so the frame's own
|
|
235
|
+
* loads (an iframe's `text/html`, the view's policy probe) still get the
|
|
236
|
+
* frame.
|
|
237
|
+
*/
|
|
238
|
+
function acceptsEventStream(request) {
|
|
239
|
+
return (request.headers.get("accept") ?? "").split(",").some((range) => (range.split(";")[0] ?? "").trim().toLowerCase() === "text/event-stream");
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* The fetch handler: preflight, the frame, the push stream, calls, pages
|
|
243
|
+
* and session turns. The core
|
|
244
|
+
* and the model server build one each, differing only in `session`, so they
|
|
245
|
+
* read and refuse requests the same way.
|
|
246
|
+
*/
|
|
247
|
+
function createHandler(deps) {
|
|
248
|
+
const { http, maxBodyBytes } = deps;
|
|
249
|
+
const tooLarge = (request) => http.failure(request, {
|
|
250
|
+
code: "BAD_INPUT",
|
|
251
|
+
message: `The request body is larger than ${maxBodyBytes} bytes.`
|
|
252
|
+
});
|
|
253
|
+
return async (request) => {
|
|
254
|
+
const origin = request.headers.get("origin");
|
|
255
|
+
if (request.method === "OPTIONS") {
|
|
256
|
+
const headers = http.corsHeaders(request);
|
|
257
|
+
if (origin !== null && deps.allowedOrigins.has(origin)) {
|
|
258
|
+
headers.set("access-control-allow-methods", "GET, POST");
|
|
259
|
+
headers.set("access-control-allow-headers", request.headers.get("access-control-request-headers") ?? "content-type");
|
|
260
|
+
headers.set("access-control-max-age", "600");
|
|
261
|
+
}
|
|
262
|
+
return new Response(null, {
|
|
263
|
+
headers,
|
|
264
|
+
status: 204
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
if (request.method === "GET") return acceptsEventStream(request) ? deps.events(request) : deps.frame(request);
|
|
268
|
+
if (request.method !== "POST") {
|
|
269
|
+
const headers = http.corsHeaders(request);
|
|
270
|
+
headers.set("allow", "GET, POST, OPTIONS");
|
|
271
|
+
return new Response(null, {
|
|
272
|
+
headers,
|
|
273
|
+
status: 405
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
const refused = http.refusal(request);
|
|
277
|
+
if (refused !== null) return http.failure(request, refused);
|
|
278
|
+
if (Number(request.headers.get("content-length") ?? "0") > maxBodyBytes) return tooLarge(request);
|
|
279
|
+
let text;
|
|
280
|
+
try {
|
|
281
|
+
text = await readLimited(request, maxBodyBytes);
|
|
282
|
+
} catch {
|
|
283
|
+
return http.failure(request, {
|
|
284
|
+
code: "BAD_INPUT",
|
|
285
|
+
message: "The request body could not be read."
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
if (text === null) return tooLarge(request);
|
|
289
|
+
let raw;
|
|
290
|
+
try {
|
|
291
|
+
raw = JSON.parse(text);
|
|
292
|
+
} catch {
|
|
293
|
+
return http.failure(request, {
|
|
294
|
+
code: "BAD_INPUT",
|
|
295
|
+
message: "The request body is not JSON."
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
const body = parseBounded(() => bodySchema.safeParse(raw));
|
|
299
|
+
if (!body.success) return http.failure(request, {
|
|
300
|
+
code: "BAD_INPUT",
|
|
301
|
+
message: body.message
|
|
302
|
+
});
|
|
303
|
+
const context = await deps.contextFor(request, body.data.type);
|
|
304
|
+
if ("error" in context) return http.failure(request, context.error);
|
|
305
|
+
if (body.data.type === "page") {
|
|
306
|
+
const reply = await deps.page({
|
|
307
|
+
name: body.data.name,
|
|
308
|
+
props: body.data.props ?? {}
|
|
309
|
+
}, {
|
|
310
|
+
ctx: context.ctx,
|
|
311
|
+
signal: request.signal
|
|
312
|
+
});
|
|
313
|
+
return "error" in reply ? http.failure(request, reply.error) : http.json(request, 200, reply);
|
|
314
|
+
}
|
|
315
|
+
if (body.data.type === "call") {
|
|
316
|
+
const callRequest = {
|
|
317
|
+
input: body.data.input,
|
|
318
|
+
kind: body.data.kind,
|
|
319
|
+
name: body.data.name
|
|
320
|
+
};
|
|
321
|
+
if (body.data.confirmation !== void 0) callRequest.confirmation = body.data.confirmation;
|
|
322
|
+
const outcome = await deps.call(callRequest, {
|
|
323
|
+
ctx: context.ctx,
|
|
324
|
+
signal: request.signal
|
|
325
|
+
});
|
|
326
|
+
return http.json(request, "error" in outcome ? statusOf(outcome.error.code) : 200, outcome);
|
|
327
|
+
}
|
|
328
|
+
const routed = await deps.session(body.data, {
|
|
329
|
+
ctx: context.ctx,
|
|
330
|
+
signal: request.signal
|
|
331
|
+
});
|
|
332
|
+
if ("error" in routed) return http.failure(request, routed.error);
|
|
333
|
+
const abort = new AbortController();
|
|
334
|
+
const forward = () => abort.abort();
|
|
335
|
+
if (request.signal.aborted) abort.abort();
|
|
336
|
+
else request.signal.addEventListener("abort", forward, { once: true });
|
|
337
|
+
return sessionStream(deps, request, routed.turn({
|
|
338
|
+
ctx: context.ctx,
|
|
339
|
+
signal: abort.signal
|
|
340
|
+
}), abort, () => request.signal.removeEventListener("abort", forward));
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
//#endregion
|
|
344
|
+
export { VERSION_HEADER, acceptsEventStream, createHandler, createHttp, issuesText, originOf, parseBounded, sseFrame, sseResponse };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { Advice, DesignCheckCode, Placement } from "./design.js";
|
|
2
2
|
import { ConfirmationStore, memoryConfirmations } from "./confirm.js";
|
|
3
|
-
import { CallContext, CallOutcome, CallReport, CallReportOutcome, CallRequest, ChatBlockResult, ChatBlockSource, ChatEvent, ChatOptions, ChatTurn, CompileOptions, ContextFunction, EditFailureReason, ErrorInfo, JigsServer, JigsServerOptions, ModelFunction, ModelRequest, ModelRequestKind, PolicyCall, RequiresAction, RunFunctions, SessionRequest, Turn, TurnEdits, TurnRewrite, TurnRound, Usage } from "./types.js";
|
|
3
|
+
import { CallContext, CallOutcome, CallReport, CallReportOutcome, CallRequest, ChatBlockResult, ChatBlockSource, ChatEvent, ChatOptions, ChatTurn, CompileOptions, ContextFunction, EditFailureReason, ErrorInfo, EventSubscriber, EventsFunction, JigsModelServer, JigsServer, JigsServerOptions, ModelFunction, ModelOptions, ModelRequest, ModelRequestKind, PageCall, PageFunction, PagePolicy, PageResolver, PolicyCall, PushInvalidate, PushInvalidatePages, PushInvalidation, PushPage, PushSend, RequiresAction, ResolvedPage, RunFunctions, SavedPage, SessionRequest, Turn, TurnEdits, TurnRewrite, TurnRound, Usage } from "./types.js";
|
|
4
4
|
import { chatBlockText } from "./chat.js";
|
|
5
5
|
import { JigsCompileError, JigsError, ThrownCode } from "./errors.js";
|
|
6
|
-
import { createJigsServer, defineRun } from "./
|
|
6
|
+
import { createJigsServer, defineRun } from "./core.js";
|
|
7
7
|
import { addUsage } from "./usage.js";
|
|
8
|
-
import { PromptMode } from "@jigs.design/prompt";
|
|
9
|
-
import { ChatBlock, ChatBlockStatus, Diagnostic, JigsErrorCode, JigsErrorData, Message, Program, SessionEvent, Severity, Span } from "@jigs.design/protocol";
|
|
10
|
-
export { type Advice, type CallContext, type CallOutcome, type CallReport, type CallReportOutcome, type CallRequest, type ChatBlock, type ChatBlockResult, type ChatBlockSource, type ChatBlockStatus, type ChatEvent, type ChatOptions, type ChatTurn, type CompileOptions, type ConfirmationStore, type ContextFunction, type DesignCheckCode, type Diagnostic, type EditFailureReason, type ErrorInfo, JigsCompileError, JigsError, type JigsErrorCode, type JigsErrorData, type JigsServer, type JigsServerOptions, type Message, type ModelFunction, type ModelRequest, type ModelRequestKind, type Placement, type PolicyCall, type Program, type PromptMode, type RequiresAction, type RunFunctions, type SessionEvent, type SessionRequest, type Severity, type Span, type ThrownCode, type Turn, type TurnEdits, type TurnRewrite, type TurnRound, type Usage, addUsage, chatBlockText, createJigsServer, defineRun, memoryConfirmations };
|
|
8
|
+
import { PromptMode, askOf, withDocument } from "@jigs.design/prompt";
|
|
9
|
+
import { ChatBlock, ChatBlockStatus, Diagnostic, JigsErrorCode, JigsErrorData, Message, PageProps, PageRef, Program, SessionDocument, SessionEvent, Severity, Span } from "@jigs.design/protocol";
|
|
10
|
+
export { type Advice, type CallContext, type CallOutcome, type CallReport, type CallReportOutcome, type CallRequest, type ChatBlock, type ChatBlockResult, type ChatBlockSource, type ChatBlockStatus, type ChatEvent, type ChatOptions, type ChatTurn, type CompileOptions, type ConfirmationStore, type ContextFunction, type DesignCheckCode, type Diagnostic, type EditFailureReason, type ErrorInfo, type EventSubscriber, type EventsFunction, JigsCompileError, JigsError, type JigsErrorCode, type JigsErrorData, type JigsModelServer, type JigsServer, type JigsServerOptions, type Message, type ModelFunction, type ModelOptions, type ModelRequest, type ModelRequestKind, type PageCall, type PageFunction, type PagePolicy, type PageProps, type PageRef, type PageResolver, type Placement, type PolicyCall, type Program, type PromptMode, type PushInvalidate, type PushInvalidatePages, type PushInvalidation, type PushPage, type PushSend, type RequiresAction, type ResolvedPage, type RunFunctions, type SavedPage, type SessionDocument, type SessionEvent, type SessionRequest, type Severity, type Span, type ThrownCode, type Turn, type TurnEdits, type TurnRewrite, type TurnRound, type Usage, addUsage, askOf, chatBlockText, createJigsServer, defineRun, memoryConfirmations, withDocument };
|
package/dist/index.js
CHANGED
|
@@ -2,5 +2,6 @@ import { JigsCompileError, JigsError } from "./errors.js";
|
|
|
2
2
|
import { addUsage } from "./usage.js";
|
|
3
3
|
import { chatBlockText } from "./chat.js";
|
|
4
4
|
import { memoryConfirmations } from "./confirm.js";
|
|
5
|
-
import { createJigsServer, defineRun } from "./
|
|
6
|
-
|
|
5
|
+
import { createJigsServer, defineRun } from "./core.js";
|
|
6
|
+
import { askOf, withDocument } from "@jigs.design/prompt";
|
|
7
|
+
export { JigsCompileError, JigsError, addUsage, askOf, chatBlockText, createJigsServer, defineRun, memoryConfirmations, withDocument };
|
package/dist/inputs.js
CHANGED
|
@@ -3,6 +3,11 @@ import { z } from "zod";
|
|
|
3
3
|
//#region src/inputs.ts
|
|
4
4
|
/** A handle declared as `const name = useMutation("procedure")`. */
|
|
5
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
|
+
/**
|
|
7
|
+
* What may follow a provided slot's name in `useQuery`: the call's end, or
|
|
8
|
+
* an empty input (`undefined` or `null`) before or without options.
|
|
9
|
+
*/
|
|
10
|
+
const NO_INPUT = /^(?:,\s*)?\)|^,\s*(?:undefined|null)(?![\w$])/;
|
|
6
11
|
const QUERY_CALL = /(?<![\w$.])useQuery\(\s*(["'`])([A-Za-z_$][\w$]*)\1\s*/g;
|
|
7
12
|
/** At most this many problems per call, so one bad literal does not flood the repair. */
|
|
8
13
|
const MAX_PROBLEMS = 3;
|
|
@@ -192,8 +197,18 @@ function inputDiagnostics(host, source) {
|
|
|
192
197
|
for (const match of source.matchAll(QUERY_CALL)) {
|
|
193
198
|
const name = match[2] ?? "";
|
|
194
199
|
const procedure = procedureOf(name);
|
|
195
|
-
if (procedure?.kind !== "query" || procedure.input === void 0) continue;
|
|
196
200
|
const rest = match.index + match[0].length;
|
|
201
|
+
if (procedure?.kind === "data") {
|
|
202
|
+
if (NO_INPUT.test(source.slice(rest))) continue;
|
|
203
|
+
diagnostics.push({
|
|
204
|
+
code: "data_takes_no_input",
|
|
205
|
+
message: `useQuery("${name}") reads provided data, which takes no input. Drop the input; the app provides this data`,
|
|
206
|
+
severity: "error",
|
|
207
|
+
span: { line: lineAt(source, match.index) }
|
|
208
|
+
});
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (procedure?.kind !== "query" || procedure.input === void 0) continue;
|
|
197
212
|
const char = source.charAt(rest);
|
|
198
213
|
if (char !== "," && char !== ")") continue;
|
|
199
214
|
const [input] = char === ")" ? [] : readArguments(source, rest + 1);
|