@jigs.design/server 0.3.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/dist/compile.js +15 -5
- package/dist/core.js +26 -4
- package/dist/inputs.js +16 -1
- package/dist/model-server.js +1 -0
- package/dist/types.d.ts +18 -7
- package/package.json +6 -6
package/dist/compile.js
CHANGED
|
@@ -57,6 +57,17 @@ function compileCatalog(catalog) {
|
|
|
57
57
|
return out;
|
|
58
58
|
}
|
|
59
59
|
const TAG = /<([a-z][A-Za-z0-9]*)/g;
|
|
60
|
+
/** Each procedure kind as a diagnostic names it. */
|
|
61
|
+
const KIND_NAMES = {
|
|
62
|
+
action: "an action",
|
|
63
|
+
data: "provided data",
|
|
64
|
+
mutation: "a mutation",
|
|
65
|
+
query: "a query"
|
|
66
|
+
};
|
|
67
|
+
/** The hook that reads or calls a procedure of `kind`: a provided slot is read like a query. */
|
|
68
|
+
function hookFor(kind) {
|
|
69
|
+
return kind === "query" || kind === "data" ? "useQuery" : "useMutation";
|
|
70
|
+
}
|
|
60
71
|
function procedureDiagnostics(host, source, uses) {
|
|
61
72
|
const diagnostics = [];
|
|
62
73
|
const report = (use, code, message) => {
|
|
@@ -83,11 +94,11 @@ function procedureDiagnostics(host, source, uses) {
|
|
|
83
94
|
}
|
|
84
95
|
const procedure = Object.hasOwn(host.procedures, use.name) ? host.procedures[use.name] : void 0;
|
|
85
96
|
if (procedure === void 0) {
|
|
86
|
-
report(use, "unknown_procedure", `"${use.name}" is not a query, mutation or
|
|
97
|
+
report(use, "unknown_procedure", `"${use.name}" is not a query, mutation, action or provided data on this host; its handle reports UNKNOWN_PROCEDURE`);
|
|
87
98
|
continue;
|
|
88
99
|
}
|
|
89
|
-
const expected = procedure.kind
|
|
90
|
-
if (expected !== use.hook) report(use, "procedure_kind", `\`${use.hook}("${use.name}")\` names ${procedure.kind
|
|
100
|
+
const expected = hookFor(procedure.kind);
|
|
101
|
+
if (expected !== use.hook) report(use, "procedure_kind", `\`${use.hook}("${use.name}")\` names ${KIND_NAMES[procedure.kind]}. Write \`${expected}("${use.name}")\`${expected === "useQuery" ? "" : " and call `mutate` from a handler"}`);
|
|
91
102
|
}
|
|
92
103
|
return diagnostics;
|
|
93
104
|
}
|
|
@@ -96,8 +107,7 @@ function procedureTable(host, uses) {
|
|
|
96
107
|
for (const use of uses) {
|
|
97
108
|
if (use.name === null || use.tanstack) continue;
|
|
98
109
|
const procedure = Object.hasOwn(host.procedures, use.name) ? host.procedures[use.name] : void 0;
|
|
99
|
-
|
|
100
|
-
if (procedure === void 0 || expected !== use.hook) continue;
|
|
110
|
+
if (procedure === void 0 || hookFor(procedure.kind) !== use.hook) continue;
|
|
101
111
|
table.set(use.name, {
|
|
102
112
|
invalidates: procedure.kind === "mutation" ? procedure.invalidates : [],
|
|
103
113
|
kind: procedure.kind
|
package/dist/core.js
CHANGED
|
@@ -36,6 +36,10 @@ function strictly(subject, compiled) {
|
|
|
36
36
|
function noProps(name, props) {
|
|
37
37
|
if (Object.keys(props).length > 0) throw new JigsError("BAD_INPUT", `"${name}" is a document page, which takes no props`);
|
|
38
38
|
}
|
|
39
|
+
/** An `AbortError`, as `fetch` and other APIs reject with on an aborted signal. */
|
|
40
|
+
function isAbortError(cause) {
|
|
41
|
+
return cause instanceof Error && cause.name === "AbortError";
|
|
42
|
+
}
|
|
39
43
|
/** A call a policy refused. The procedure stays in the prompt. */
|
|
40
44
|
const denial = (name) => ({ error: {
|
|
41
45
|
code: "FORBIDDEN",
|
|
@@ -91,7 +95,7 @@ function hostFingerprint(host) {
|
|
|
91
95
|
kind: procedure.kind,
|
|
92
96
|
name,
|
|
93
97
|
output: procedure.output === void 0 ? null : z.toJSONSchema(procedure.output),
|
|
94
|
-
projection: procedure.kind === "action" || procedure.projection === void 0 ? null : z.toJSONSchema(procedure.projection.schema)
|
|
98
|
+
projection: procedure.kind === "action" || procedure.kind === "data" || procedure.projection === void 0 ? null : z.toJSONSchema(procedure.projection.schema)
|
|
95
99
|
})),
|
|
96
100
|
jigs: host.jigs.map((entry) => [
|
|
97
101
|
entry.name,
|
|
@@ -113,7 +117,7 @@ function defineRun(procedures, handlers) {
|
|
|
113
117
|
function createJigsServer(options) {
|
|
114
118
|
const { host } = options;
|
|
115
119
|
checkMarkdownElements(host.components);
|
|
116
|
-
for (const [name, procedure] of Object.entries(host.procedures)) if (procedure.kind !== "action" && !Object.hasOwn(options.run, name)) throw new Error(`createJigsServer has no run function for "${name}"`);
|
|
120
|
+
for (const [name, procedure] of Object.entries(host.procedures)) if (procedure.kind !== "action" && procedure.kind !== "data" && !Object.hasOwn(options.run, name)) throw new Error(`createJigsServer has no run function for "${name}"`);
|
|
117
121
|
const confirmer = confirmerFor(options);
|
|
118
122
|
const checker = createChecker(host);
|
|
119
123
|
const promptOptions = (mode) => options.instructions === void 0 ? { mode } : {
|
|
@@ -220,7 +224,11 @@ function createJigsServer(options) {
|
|
|
220
224
|
};
|
|
221
225
|
const dispatch = async (request, context, trace) => {
|
|
222
226
|
const procedure = Object.hasOwn(host.procedures, request.name) ? host.procedures[request.name] : void 0;
|
|
223
|
-
const run = procedure?.kind !== "action" && Object.hasOwn(runs, request.name) ? runs[request.name] : void 0;
|
|
227
|
+
const run = procedure?.kind !== "action" && procedure?.kind !== "data" && Object.hasOwn(runs, request.name) ? runs[request.name] : void 0;
|
|
228
|
+
if (procedure?.kind === "data") return { error: {
|
|
229
|
+
code: "BAD_INPUT",
|
|
230
|
+
message: `"${request.name}" is provided data: it is supplied by the page, not fetched`
|
|
231
|
+
} };
|
|
224
232
|
if (procedure === void 0 || procedure.kind !== "action" && run === void 0) return { error: {
|
|
225
233
|
code: "UNKNOWN_PROCEDURE",
|
|
226
234
|
message: `"${request.name}" is not a query, mutation or action on this host`
|
|
@@ -256,6 +264,10 @@ function createJigsServer(options) {
|
|
|
256
264
|
try {
|
|
257
265
|
result = await run(input, context);
|
|
258
266
|
} catch (error) {
|
|
267
|
+
if (context.signal.aborted || isAbortError(error)) {
|
|
268
|
+
trace.aborted = true;
|
|
269
|
+
return { error: errorData(null) };
|
|
270
|
+
}
|
|
259
271
|
if (isJigsError(error)) return { error: errorData(error) };
|
|
260
272
|
onError(error, {
|
|
261
273
|
name: request.name,
|
|
@@ -327,6 +339,7 @@ function createJigsServer(options) {
|
|
|
327
339
|
const call = async (request, context) => {
|
|
328
340
|
const started = performance.now();
|
|
329
341
|
const trace = {
|
|
342
|
+
aborted: false,
|
|
330
343
|
gate: null,
|
|
331
344
|
input: request.input
|
|
332
345
|
};
|
|
@@ -339,7 +352,8 @@ function createJigsServer(options) {
|
|
|
339
352
|
name: request.name,
|
|
340
353
|
outcome: trace.gate ?? "success"
|
|
341
354
|
};
|
|
342
|
-
if (
|
|
355
|
+
if (trace.aborted) report.outcome = "aborted";
|
|
356
|
+
else if ("error" in outcome) {
|
|
343
357
|
report.error = outcome.error;
|
|
344
358
|
report.outcome = trace.gate === "denied" ? "denied" : "error";
|
|
345
359
|
}
|
|
@@ -671,9 +685,17 @@ function createJigsServer(options) {
|
|
|
671
685
|
onError,
|
|
672
686
|
page: pageRequest
|
|
673
687
|
};
|
|
688
|
+
const data = (name, value) => {
|
|
689
|
+
const slot = Object.hasOwn(host.procedures, name) ? host.procedures[name] : void 0;
|
|
690
|
+
if (slot?.kind !== "data") throw new JigsError("BAD_INPUT", `"${name}" is not provided data on this host`);
|
|
691
|
+
const parsed = slot.output.safeParse(value);
|
|
692
|
+
if (!parsed.success) throw new JigsError("BAD_INPUT", `"${name}": ${issuesText(parsed.error)}`);
|
|
693
|
+
return parsed.data;
|
|
694
|
+
};
|
|
674
695
|
const core = {
|
|
675
696
|
call,
|
|
676
697
|
compile,
|
|
698
|
+
data,
|
|
677
699
|
events,
|
|
678
700
|
frame,
|
|
679
701
|
handler: createHandler({
|
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);
|
package/dist/model-server.js
CHANGED
package/dist/types.d.ts
CHANGED
|
@@ -2,8 +2,8 @@ import { Advice, DesignCheckCode, Placement } from "./design.js";
|
|
|
2
2
|
import { ConfirmationStore } from "./confirm.js";
|
|
3
3
|
import { EditFailureReason } from "@jigs.design/compiler";
|
|
4
4
|
import { PromptMode } from "@jigs.design/prompt";
|
|
5
|
-
import { ActionDefinition, CallPolicy, Catalog, Host, InputOf, JigDefinition, MutationDefinition, OutputOf, PageName, ProcedureDefinition, Procedures, QueryDefinition } from "@jigs.design/host";
|
|
6
|
-
import { ChatBlock, ChatBlockStatus, Diagnostic, Invalidation, JigsErrorData, Message, PageInvalidation, PageProps, PageRef, Program, SessionEvent } from "@jigs.design/protocol";
|
|
5
|
+
import { ActionDefinition, CallPolicy, Catalog, DataDefinition, Host, InputOf, JigDefinition, MutationDefinition, OutputOf, PageName, ProcedureDefinition, Procedures, QueryDefinition } from "@jigs.design/host";
|
|
6
|
+
import { ChatBlock, ChatBlockStatus, Diagnostic, Invalidation, JigsErrorData, JsonValue, Message, PageInvalidation, PageProps, PageRef, Program, SessionEvent } from "@jigs.design/protocol";
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
//#region src/types.d.ts
|
|
9
9
|
/**
|
|
@@ -159,9 +159,10 @@ type DeepReadonly<T> = T extends object ? { readonly [K in keyof T]: DeepReadonl
|
|
|
159
159
|
type RunOutput<P extends ProcedureDefinition> = DeepReadonly<OutputOf<P>>;
|
|
160
160
|
/**
|
|
161
161
|
* One function per query and mutation. Actions have none: the page handles
|
|
162
|
-
* them, and the server only checks their input.
|
|
162
|
+
* them, and the server only checks their input. Provided data has none
|
|
163
|
+
* either: the page supplies it.
|
|
163
164
|
*/
|
|
164
|
-
export type RunFunctions<TProcedures extends Procedures, TContext> = { readonly [K in keyof TProcedures as TProcedures[K] extends ActionDefinition ? never : K]: (input: InputOf<TProcedures[K]>, call: CallContext<TContext>) => RunOutput<TProcedures[K]> | Promise<RunOutput<TProcedures[K]>>; };
|
|
165
|
+
export type RunFunctions<TProcedures extends Procedures, TContext> = { readonly [K in keyof TProcedures as TProcedures[K] extends ActionDefinition | DataDefinition ? never : K]: (input: InputOf<TProcedures[K]>, call: CallContext<TContext>) => RunOutput<TProcedures[K]> | Promise<RunOutput<TProcedures[K]>>; };
|
|
165
166
|
export type ContextFunction<TContext> = (request: Request) => TContext | Promise<TContext>;
|
|
166
167
|
/** The names of a host's mutations. */
|
|
167
168
|
type MutationName<TProcedures extends Procedures> = { [K in keyof TProcedures & string]: TProcedures[K] extends MutationDefinition ? K : never; }[keyof TProcedures & string];
|
|
@@ -180,7 +181,7 @@ export type PolicyCall<TProcedures extends Procedures, TContext> = { [K in Mutat
|
|
|
180
181
|
* as "requires-action" when the server asks the page, and once as
|
|
181
182
|
* "confirmed" when it ran with the user's approval.
|
|
182
183
|
*/
|
|
183
|
-
export type CallReportOutcome = "confirmed" | "denied" | "error" | "requires-action" | "success";
|
|
184
|
+
export type CallReportOutcome = "aborted" | "confirmed" | "denied" | "error" | "requires-action" | "success";
|
|
184
185
|
/**
|
|
185
186
|
* One call to a query, mutation or action, or one page request, as
|
|
186
187
|
* `onCall` receives it.
|
|
@@ -189,7 +190,7 @@ export interface CallReport<TContext> {
|
|
|
189
190
|
ctx: TContext;
|
|
190
191
|
/** From the call's arrival to its outcome, in milliseconds. */
|
|
191
192
|
durationMs: number;
|
|
192
|
-
/** What the browser received, when `outcome` is
|
|
193
|
+
/** What the browser received, when `outcome` is "denied" or "error". */
|
|
193
194
|
error?: JigsErrorData;
|
|
194
195
|
/**
|
|
195
196
|
* The parsed input, or the input as the browser sent it when the call
|
|
@@ -205,7 +206,9 @@ export interface CallReport<TContext> {
|
|
|
205
206
|
/**
|
|
206
207
|
* "denied" when a policy or a bad confirmation refused the call, or
|
|
207
208
|
* `pagePolicy` a page; "error" for any other failure, including one
|
|
208
|
-
* `run` threw and a page no list has
|
|
209
|
+
* `run` threw and a page no list has; "aborted" when `run` rejected
|
|
210
|
+
* after the browser abandoned the call (its `signal` aborted), which
|
|
211
|
+
* does not reach `onError`.
|
|
209
212
|
*/
|
|
210
213
|
outcome: CallReportOutcome;
|
|
211
214
|
}
|
|
@@ -642,6 +645,14 @@ export interface JigsServer<TContext, TPageName extends string = never> {
|
|
|
642
645
|
diagnostics: ReadonlyArray<Diagnostic>;
|
|
643
646
|
program: Program;
|
|
644
647
|
}>;
|
|
648
|
+
/**
|
|
649
|
+
* `value` parsed with the schema of provided slot `name`, for a page
|
|
650
|
+
* that wants it checked before it passes it to `JigsView` as `data`.
|
|
651
|
+
* Throws `JigsError("BAD_INPUT")` when it does not match, or when `name`
|
|
652
|
+
* is not provided data on this host. The view does not check it: a value
|
|
653
|
+
* that does not match renders as the program reads it.
|
|
654
|
+
*/
|
|
655
|
+
data: (name: string, value: JsonValue) => JsonValue;
|
|
645
656
|
/**
|
|
646
657
|
* The push stream for `request`, whose `ctx` the caller already has: a
|
|
647
658
|
* foreign `Origin` is `403`, a server without `events` answers `204`, and
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jigs.design/server",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "createJigsServer: checks and streams programs, runs procedures, and serves the sandbox frame.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -29,11 +29,11 @@
|
|
|
29
29
|
"access": "public"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@jigs.design/
|
|
33
|
-
"@jigs.design/
|
|
34
|
-
"@jigs.design/protocol": "0.3.
|
|
35
|
-
"@jigs.design/runtime": "0.3.
|
|
36
|
-
"@jigs.design/
|
|
32
|
+
"@jigs.design/host": "0.3.1",
|
|
33
|
+
"@jigs.design/compiler": "0.3.1",
|
|
34
|
+
"@jigs.design/protocol": "0.3.1",
|
|
35
|
+
"@jigs.design/runtime": "0.3.1",
|
|
36
|
+
"@jigs.design/prompt": "0.3.1"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"zod": "^4.0.0"
|