@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 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 action on this host; its handle reports UNKNOWN_PROCEDURE`);
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 === "query" ? "useQuery" : "useMutation";
90
- if (expected !== use.hook) report(use, "procedure_kind", `\`${use.hook}("${use.name}")\` names ${procedure.kind === "action" ? "an" : "a"} ${procedure.kind}. Write \`${expected}("${use.name}")\`${procedure.kind === "query" ? "" : " and call `mutate` from a handler"}`);
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
- const expected = procedure?.kind === "query" ? "useQuery" : "useMutation";
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 ("error" in outcome) {
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);
@@ -122,6 +122,7 @@ function createModelServer(internals, core, options) {
122
122
  call: core.call,
123
123
  chat,
124
124
  compile: core.compile,
125
+ data: core.data,
125
126
  events: core.events,
126
127
  frame: core.frame,
127
128
  handler: createHandler({
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 not "success". */
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.0",
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/compiler": "0.3.0",
33
- "@jigs.design/prompt": "0.3.0",
34
- "@jigs.design/protocol": "0.3.0",
35
- "@jigs.design/runtime": "0.3.0",
36
- "@jigs.design/host": "0.3.0"
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"