@jxsuite/runtime 4.0.3 → 5.1.0

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/src/statements.ts CHANGED
@@ -10,6 +10,7 @@
10
10
  * `dispatchEvent` with `CustomEventInit` members, `stopPropagation` and `preventDefault`.
11
11
  */
12
12
 
13
+ import { isFunction, isNonNullObject } from "@jxsuite/schema/guards";
13
14
  import {
14
15
  compileExpression,
15
16
  compileOperandSource,
@@ -17,13 +18,19 @@ import {
17
18
  evaluateOperand,
18
19
  } from "./expression.ts";
19
20
 
20
- import type { JxStatement } from "@jxsuite/schema/types";
21
- import type { CompileOpts, ExpressionNode, IterCtx } from "./expression.ts";
21
+ import type {
22
+ JxDispatchStatement,
23
+ JxExpressionNode,
24
+ JxIfStatement,
25
+ JxStatement,
26
+ JxSwitchStatement,
27
+ } from "@jxsuite/schema/types";
28
+ import type { CompileOpts, IterCtx } from "./expression.ts";
22
29
  import type { JxScope } from "./types.ts";
23
30
 
24
31
  interface RunOpts {
25
32
  /** Named-formula-style arguments bound for $args/<name> refs in the body. */
26
- args?: Record<string, unknown>;
33
+ args?: IterCtx["args"];
27
34
  /**
28
35
  * Dispatch target for dispatchEvent statements when there is no event to read `currentTarget`
29
36
  * from — a parameterised body invoked through `call`, or a lifecycle hook. A thunk is read at
@@ -32,31 +39,49 @@ interface RunOpts {
32
39
  target?: EventTarget | null | (() => EventTarget | null);
33
40
  }
34
41
 
35
- type StatementKind =
36
- | "expression"
37
- | "if"
38
- | "switch"
39
- | "dispatch"
40
- | "stopPropagation"
41
- | "preventDefault";
42
+ /*
43
+ * Statement bodies come from documents that are not schema-validated at runtime, so the branch
44
+ * arrays the schema requires may still be absent; these views say so and keep the fallbacks
45
+ * honest.
46
+ */
47
+ interface UncheckedIfStatement extends Omit<JxIfStatement, "then"> {
48
+ then?: JxStatement[];
49
+ }
50
+
51
+ interface UncheckedSwitchStatement extends Omit<JxSwitchStatement, "cases"> {
52
+ cases?: JxSwitchStatement["cases"];
53
+ }
42
54
 
43
- function statementKind(statement: JxStatement): StatementKind {
55
+ type ClassifiedStatement =
56
+ | { kind: "expression"; node: JxExpressionNode }
57
+ | { kind: "if"; node: UncheckedIfStatement }
58
+ | { kind: "switch"; node: UncheckedSwitchStatement }
59
+ | { kind: "dispatch"; node: JxDispatchStatement }
60
+ | { kind: "stopPropagation" }
61
+ | { kind: "preventDefault" };
62
+
63
+ function classifyStatement(statement: JxStatement): ClassifiedStatement {
44
64
  if ("operator" in statement) {
45
- return "expression";
65
+ return { kind: "expression", node: statement };
46
66
  }
47
67
  if ("if" in statement) {
48
- return "if";
68
+ return { kind: "if", node: statement };
49
69
  }
50
70
  if ("$switch" in statement) {
51
- return "switch";
71
+ return { kind: "switch", node: statement };
52
72
  }
53
73
  if ("stopPropagation" in statement) {
54
- return "stopPropagation";
74
+ return { kind: "stopPropagation" };
55
75
  }
56
76
  if ("preventDefault" in statement) {
57
- return "preventDefault";
77
+ return { kind: "preventDefault" };
58
78
  }
59
- return "dispatch";
79
+ return { kind: "dispatch", node: statement };
80
+ }
81
+
82
+ /** A value `await` would adopt: anything with a callable `then` (ECMA-262 thenable). */
83
+ function isThenable(value: unknown): value is PromiseLike<unknown> {
84
+ return (isNonNullObject(value) || isFunction(value)) && "then" in value && isFunction(value.then);
60
85
  }
61
86
 
62
87
  /**
@@ -72,16 +97,18 @@ export async function runStatements(
72
97
  ): Promise<void> {
73
98
  const iterCtx: IterCtx | undefined = opts.args ? { args: opts.args } : undefined;
74
99
  for (const statement of statements) {
75
- switch (statementKind(statement)) {
100
+ const classified = classifyStatement(statement);
101
+ switch (classified.kind) {
76
102
  case "expression": {
77
- const result = evaluateExpression(statement as ExpressionNode, state, event, iterCtx);
78
- if (result && typeof (result as PromiseLike<unknown>).then === "function") {
79
- await (result as PromiseLike<unknown>);
103
+ const result = evaluateExpression(classified.node, state, event, iterCtx);
104
+ if (isThenable(result)) {
105
+ // Awaited for its ordering alone: the next statement runs once this one settles.
106
+ void (await result);
80
107
  }
81
108
  break;
82
109
  }
83
110
  case "if": {
84
- const branch = statement as { if: unknown; then: JxStatement[]; else?: JxStatement[] };
111
+ const branch = classified.node;
85
112
  const taken = evaluateOperand(branch.if, state, event, iterCtx) ? branch.then : branch.else;
86
113
  if (Array.isArray(taken)) {
87
114
  await runStatements(taken, state, event, opts);
@@ -89,11 +116,7 @@ export async function runStatements(
89
116
  break;
90
117
  }
91
118
  case "switch": {
92
- const branch = statement as {
93
- $switch: unknown;
94
- cases: Record<string, JxStatement[]>;
95
- default?: JxStatement[];
96
- };
119
+ const branch = classified.node;
97
120
  const key = String(evaluateOperand(branch.$switch, state, event, iterCtx));
98
121
  const cases = branch.cases ?? {};
99
122
  const taken = Object.hasOwn(cases, key) ? cases[key] : branch.default;
@@ -103,14 +126,9 @@ export async function runStatements(
103
126
  break;
104
127
  }
105
128
  case "dispatch": {
106
- const dispatch = statement as {
107
- dispatchEvent: string;
108
- detail?: unknown;
109
- bubbles?: boolean;
110
- composed?: boolean;
111
- };
129
+ const dispatch = classified.node;
112
130
  // An event's own target wins; the option is the fallback for bodies run without one.
113
- const fallback = typeof opts.target === "function" ? opts.target() : opts.target;
131
+ const fallback = isFunction(opts.target) ? opts.target() : opts.target;
114
132
  const target = event?.currentTarget ?? fallback;
115
133
  if (!target) {
116
134
  break;
@@ -164,13 +182,14 @@ export function compileStatements(
164
182
  const indent = opts.indent ?? "";
165
183
  const lines: string[] = [];
166
184
  for (const statement of statements) {
167
- switch (statementKind(statement)) {
185
+ const classified = classifyStatement(statement);
186
+ switch (classified.kind) {
168
187
  case "expression": {
169
- lines.push(`${indent}${compileExpression(statement as ExpressionNode, opts)};`);
188
+ lines.push(`${indent}${compileExpression(classified.node, opts)};`);
170
189
  break;
171
190
  }
172
191
  case "if": {
173
- const branch = statement as { if: unknown; then: JxStatement[]; else?: JxStatement[] };
192
+ const branch = classified.node;
174
193
  const test = compileOperandSource(branch.if, opts);
175
194
  const inner = { ...opts, indent: `${indent} ` };
176
195
  lines.push(`${indent}if (${test}) {`, compileStatements(branch.then ?? [], inner));
@@ -181,11 +200,7 @@ export function compileStatements(
181
200
  break;
182
201
  }
183
202
  case "switch": {
184
- const branch = statement as {
185
- $switch: unknown;
186
- cases: Record<string, JxStatement[]>;
187
- default?: JxStatement[];
188
- };
203
+ const branch = classified.node;
189
204
  const disc = compileOperandSource(branch.$switch, opts);
190
205
  const inner = { ...opts, indent: `${indent} ` };
191
206
  lines.push(`${indent}switch (String(${disc})) {`);
@@ -208,12 +223,7 @@ export function compileStatements(
208
223
  break;
209
224
  }
210
225
  case "dispatch": {
211
- const dispatch = statement as {
212
- dispatchEvent: string;
213
- detail?: unknown;
214
- bubbles?: boolean;
215
- composed?: boolean;
216
- };
226
+ const dispatch = classified.node;
217
227
  const eventName = opts.eventParam ?? "event";
218
228
  const target = opts.dispatchTarget ?? `(${eventName} && ${eventName}.currentTarget)`;
219
229
  const init: string[] = [];
package/src/types.ts CHANGED
@@ -1,17 +1,101 @@
1
1
  /// <reference lib="dom" />
2
- import type { JxDocument, JxElement, JxPath } from "@jxsuite/schema/types";
2
+ import type { JxDocument, JxDocumentValue, JxElement, JxPath } from "@jxsuite/schema/types";
3
+ import type { Ref } from "@vue/reactivity";
3
4
 
4
5
  export type { JxPath } from "@jxsuite/schema/types";
5
6
 
6
7
  /**
7
- * The live reactive scope: a prototype-chained object whose keys are user-defined state names.
8
- * Values are runtime JS values (signals, functions, JSON data) and must be narrowed at use — this
9
- * alias marks the dynamic boundary by name.
8
+ * What one member of a {@link JxScope} holds: every kind of value the runtime puts into a scope or
9
+ * hands to a document's code.
10
+ *
11
+ * - A document's own values (`JxDocumentValue`): JSON, and the typed shapes a naked `state` entry, a
12
+ * `$`-local or a literal `$props` value copies in (spec.md §5.3).
13
+ * - `undefined`, for a member or an argument that is absent.
14
+ * - Arrays and records of scope values: reactive objects, host records, `$props`, and the instance an
15
+ * external class constructs (spec.md §12.3), every one of them read by member name.
16
+ * - Functions ({@link JxScopeFunction}): handlers, formulas, `$src` exports, server-function proxies
17
+ * and a host's own functions.
18
+ * - Refs: the computed and data-source entries the reactive proxy unwraps on read.
19
+ * - The `$map` context a mapped row renders under ({@link JxRowContext}).
20
+ * - DOM objects: event targets (nodes, the window, the document) and events, which `window#/`,
21
+ * `document#/` and `event#/` references read.
22
+ * - The platform objects a `$prototype` entry resolves to (spec.md §11.2): `Map`, `Set`, `FormData`,
23
+ * `Blob`, and the `IDBObjectStore` an `IndexedDB` entry's `getStore()` yields.
24
+ * - A promise of any of those: a computed entry over an `async` function settles later.
25
+ *
26
+ * Narrow it with the guards in `@jxsuite/schema/guards`, which work on the union.
10
27
  */
11
- export type JxScope = Record<string, unknown>;
28
+ export type JxScopeValue = JxSettledValue | Promise<JxSettledValue>;
29
+
30
+ /**
31
+ * A scope member that is not a promise: what a {@link JxScopeValue} settles to. A promise never
32
+ * fulfils with another promise, which is what lets an `await` on a scope member end.
33
+ */
34
+ export type JxSettledValue =
35
+ | JxDocumentValue
36
+ | undefined
37
+ | JxScopeValue[]
38
+ | readonly JxScopeValue[]
39
+ | JxScope
40
+ | JxScopeFunction
41
+ | Ref<JxScopeValue>
42
+ | JxRowContext
43
+ | EventTarget
44
+ | Event
45
+ | ReadonlyMap<JxScopeValue, JxScopeValue>
46
+ | ReadonlySet<JxScopeValue>
47
+ | FormData
48
+ | Blob
49
+ | IDBObjectStore;
50
+
51
+ /**
52
+ * The live reactive scope (spec.md §15): a prototype-chained record of state entries, `$props`, a
53
+ * host's members and the runtime's `$` vocabulary, each read and written by name.
54
+ */
55
+ export interface JxScope {
56
+ [name: string]: JxScopeValue;
57
+ }
58
+
59
+ /**
60
+ * A function a scope holds, as the scope sees it: a handler, a formula, a `$src` export, a
61
+ * server-function proxy or a host's own function. JavaScript checks neither the arguments a call
62
+ * passes nor what it returns, so this is every function; a call site narrows it to the signature it
63
+ * calls with, and a result it reads is a scope member by the contract of whatever stored the
64
+ * function (a computed entry, a formula, a server call).
65
+ */
66
+ export type JxScopeFunction = (...args: never[]) => void;
67
+
68
+ /**
69
+ * The `$map` context a mapped row renders under (spec.md §10.2): the row's item, and its index in
70
+ * the list. Both are written in place when the row is reused.
71
+ */
72
+ export interface JxRowContext {
73
+ index: number;
74
+ item: JxScopeValue;
75
+ }
76
+
77
+ /**
78
+ * A value a host hands the runtime, held to {@link JxScopeValue} member by member.
79
+ *
80
+ * A host declares its scope with interfaces, and TypeScript gives an interface no implicit index
81
+ * signature, so a host's record is never assignable to {@link JxScope}, however its members are
82
+ * typed. This walks the declared type instead: a scope value stays as it is, a `bigint` or a
83
+ * `symbol` is refused (neither is a scope value), a function is held to what it returns, and
84
+ * anything else — an array, a record — is held member by member.
85
+ */
86
+ export type JxHostValue<T> = T extends JxScopeValue
87
+ ? T
88
+ : T extends bigint | symbol
89
+ ? never
90
+ : T extends (...args: infer A) => infer R
91
+ ? (...args: A) => JxHostValue<R>
92
+ : { [K in keyof T]: JxHostValue<T[K]> };
93
+
94
+ /** A host's scope, held to {@link JxScopeValue} member by member (see {@link JxHostValue}). */
95
+ export type JxHostScope<S> = { [K in keyof S]: JxHostValue<S[K]> };
12
96
 
13
97
  /** An event handler stored in the scope: `(scope, event) => void`. */
14
- export type JxEventHandler = (scope: JxScope, event: Event) => unknown;
98
+ export type JxEventHandler = (scope: JxScope, event: Event) => void;
15
99
 
16
100
  /**
17
101
  * A host-supplied document source consulted before the network. Returning `undefined` means "not
@@ -42,14 +126,17 @@ export interface JxContext {
42
126
  skipAutoRequests?: boolean;
43
127
  }
44
128
 
45
- /** Options for {@link mount}: the host's half of the embedding contract (specs/embedding.md). */
46
- export interface JxMountOptions {
129
+ /**
130
+ * Options for {@link mount}: the host's half of the embedding contract (specs/embedding.md). `S` is
131
+ * the host's own scope type, which `mount` holds to {@link JxHostScope}.
132
+ */
133
+ export interface JxMountOptions<S = JxScope> {
47
134
  /**
48
135
  * Host members merged into the document's scope before its own `state` is built: reactive
49
136
  * records, refs, computeds and plain functions. A document `state` entry of the same name wins.
50
137
  * Keys beginning with `$` or `#` are runtime vocabulary and are refused.
51
138
  */
52
- scope?: Record<string, unknown>;
139
+ scope?: S;
53
140
  base?: string;
54
141
  media?: Record<string, string>;
55
142
  resolver?: DocumentResolver;
@@ -71,7 +158,7 @@ export interface JxMount {
71
158
  * touches.
72
159
  */
73
160
  readonly elements: ReadonlySet<string>;
74
- /** Stops every effect, calls `onUnmount`, removes the root. Idempotent. */
161
+ /** Removes the root, stops every effect, then calls `onUnmount`. Idempotent. */
75
162
  readonly dispose: () => void;
76
163
  }
77
164
 
@@ -112,8 +199,22 @@ export interface JxRenderOptions {
112
199
  onNodeMoved?: (node: HTMLElement | Text, path: JxPath) => void;
113
200
  }
114
201
 
202
+ /**
203
+ * A method body compiled by `new Function` from a `.class.json` method definition. Only document
204
+ * code calls it, so its arguments and result are opaque to TypeScript.
205
+ */
206
+ export type DynamicMethod = (...args: never[]) => void;
207
+
208
+ /** The prototype of a {@link DynamicClass}: instance methods attached by name. */
209
+ export type DynamicPrototype = Record<string, DynamicMethod>;
210
+
211
+ /**
212
+ * A class built at runtime from a `.class.json` definition. Its static side carries the static
213
+ * methods attached by name beside the prototype; an instance holds the constructor `config` values
214
+ * under its field names.
215
+ */
115
216
  export interface DynamicClass {
116
- new (config?: Record<string, unknown>): Record<string, unknown>;
117
- [key: string]: unknown;
118
- prototype: Record<string, unknown>;
217
+ new (config?: JxScope): JxScope;
218
+ [member: string]: DynamicMethod | DynamicPrototype;
219
+ prototype: DynamicPrototype;
119
220
  }