@async/framework 0.11.15 → 0.11.17
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/CHANGELOG.md +27 -0
- package/README.md +10 -14
- package/browser.d.ts +45 -3
- package/browser.js +746 -47
- package/browser.min.js +1 -1
- package/browser.ts +746 -47
- package/browser.umd.js +746 -47
- package/browser.umd.min.js +1 -1
- package/framework.d.ts +45 -3
- package/framework.ts +746 -47
- package/package.json +24 -2
- package/runtime/events.d.ts +48 -0
- package/runtime/events.js +208 -0
- package/runtime/shared.js +85 -0
- package/runtime/signals.d.ts +31 -0
- package/runtime/signals.js +209 -0
- package/runtime.d.ts +72 -0
- package/runtime.js +63 -0
- package/server.js +746 -47
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@async/framework",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.17",
|
|
4
4
|
"description": "No-build Loader app runtime with browser and server entrypoints, signals, command events, route partials, cache split, SSR activation, and streaming boundaries.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -60,6 +60,21 @@
|
|
|
60
60
|
"import": "./server.js",
|
|
61
61
|
"default": "./server.js"
|
|
62
62
|
},
|
|
63
|
+
"./runtime": {
|
|
64
|
+
"types": "./runtime.d.ts",
|
|
65
|
+
"import": "./runtime.js",
|
|
66
|
+
"default": "./runtime.js"
|
|
67
|
+
},
|
|
68
|
+
"./runtime/signals": {
|
|
69
|
+
"types": "./runtime/signals.d.ts",
|
|
70
|
+
"import": "./runtime/signals.js",
|
|
71
|
+
"default": "./runtime/signals.js"
|
|
72
|
+
},
|
|
73
|
+
"./runtime/events": {
|
|
74
|
+
"types": "./runtime/events.d.ts",
|
|
75
|
+
"import": "./runtime/events.js",
|
|
76
|
+
"default": "./runtime/events.js"
|
|
77
|
+
},
|
|
63
78
|
"./package.json": "./package.json"
|
|
64
79
|
},
|
|
65
80
|
"files": [
|
|
@@ -75,6 +90,13 @@
|
|
|
75
90
|
"framework.d.ts",
|
|
76
91
|
"framework.ts",
|
|
77
92
|
"package.json",
|
|
78
|
-
"server.js"
|
|
93
|
+
"server.js",
|
|
94
|
+
"runtime.d.ts",
|
|
95
|
+
"runtime/signals.d.ts",
|
|
96
|
+
"runtime/events.d.ts",
|
|
97
|
+
"runtime.js",
|
|
98
|
+
"runtime/signals.js",
|
|
99
|
+
"runtime/events.js",
|
|
100
|
+
"runtime/shared.js"
|
|
79
101
|
]
|
|
80
102
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Generated by scripts/build-framework-bundle.js. Do not edit by hand.
|
|
2
|
+
// Type declarations for @async/framework/runtime/events.
|
|
3
|
+
|
|
4
|
+
export type ElementLocator = string | { readonly selector: string; readonly optional?: boolean };
|
|
5
|
+
export type EventRuntimeContext = {
|
|
6
|
+
event: Event;
|
|
7
|
+
element: Element;
|
|
8
|
+
el: Element;
|
|
9
|
+
root: ParentNode;
|
|
10
|
+
signals?: { set(path: string, value: unknown): void };
|
|
11
|
+
};
|
|
12
|
+
export type EventValueSource =
|
|
13
|
+
| readonly ["event.target.value"]
|
|
14
|
+
| readonly ["event.target.checked"]
|
|
15
|
+
| readonly ["constant", value: unknown];
|
|
16
|
+
export type EventCommand =
|
|
17
|
+
| readonly ["handler", id: string]
|
|
18
|
+
| readonly ["preventDefault"]
|
|
19
|
+
| readonly ["stopPropagation"]
|
|
20
|
+
| readonly ["stopImmediatePropagation"]
|
|
21
|
+
| readonly ["setSignal", path: string, valueSource: EventValueSource];
|
|
22
|
+
export type EventBindingRecord = readonly [element: number, event: string, commands: readonly EventCommand[]];
|
|
23
|
+
export type StrictHandlerDescriptor = {
|
|
24
|
+
readonly mode?: "strict";
|
|
25
|
+
readonly module?: string;
|
|
26
|
+
readonly browserImport: string;
|
|
27
|
+
readonly exportName: string;
|
|
28
|
+
readonly version?: string;
|
|
29
|
+
readonly integrity?: string;
|
|
30
|
+
};
|
|
31
|
+
export type HandlerDescriptor = ((context: EventRuntimeContext) => unknown | Promise<unknown>) | StrictHandlerDescriptor;
|
|
32
|
+
export type EventRuntimePlan = {
|
|
33
|
+
readonly version?: 1;
|
|
34
|
+
readonly events: readonly EventBindingRecord[];
|
|
35
|
+
readonly handlers?: Record<string, HandlerDescriptor>;
|
|
36
|
+
};
|
|
37
|
+
export type EventRuntimeOptions = {
|
|
38
|
+
readonly elements?: readonly ElementLocator[];
|
|
39
|
+
readonly signal?: AbortSignal;
|
|
40
|
+
readonly signals?: { set(path: string, value: unknown): void };
|
|
41
|
+
readonly importModule?: (specifier: string) => Promise<Record<string, unknown>>;
|
|
42
|
+
readonly onDiagnostic?: (diagnostic: Record<string, unknown>) => void;
|
|
43
|
+
};
|
|
44
|
+
export type EventRuntimeController = {
|
|
45
|
+
readonly stopped: boolean;
|
|
46
|
+
stop(): void;
|
|
47
|
+
};
|
|
48
|
+
export declare function startEvents(rootOrOptions: ParentNode | ({ root: ParentNode; plan: EventRuntimePlan } & EventRuntimeOptions), plan?: EventRuntimePlan, options?: EventRuntimeOptions): EventRuntimeController;
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createRuntimeController,
|
|
3
|
+
normalizeRuntimeStartArgs,
|
|
4
|
+
resolveElements
|
|
5
|
+
} from "./shared.js";
|
|
6
|
+
|
|
7
|
+
export function startEvents(rootOrOptions, planArg, optionsArg) {
|
|
8
|
+
const { root, plan, options } = normalizeRuntimeStartArgs(rootOrOptions, planArg, optionsArg);
|
|
9
|
+
assertEventPlan(plan);
|
|
10
|
+
assertHandlers(plan.handlers ?? {});
|
|
11
|
+
const runtimeOptions = {
|
|
12
|
+
...options,
|
|
13
|
+
root,
|
|
14
|
+
moduleCache: new Map()
|
|
15
|
+
};
|
|
16
|
+
const elements = options.resolvedElements ?? resolveElements(root, options.elements ?? [], {
|
|
17
|
+
onDiagnostic: options.onDiagnostic
|
|
18
|
+
});
|
|
19
|
+
const listeners = [];
|
|
20
|
+
let stopped = false;
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const bindingsByType = groupEventBindings(plan.events, elements);
|
|
24
|
+
for (const [eventType, bindings] of bindingsByType) {
|
|
25
|
+
const listener = (event) => {
|
|
26
|
+
void dispatchPlannedEvent(event, bindings, plan.handlers ?? {}, runtimeOptions);
|
|
27
|
+
};
|
|
28
|
+
root.addEventListener(eventType, listener);
|
|
29
|
+
listeners.push([eventType, listener]);
|
|
30
|
+
}
|
|
31
|
+
} catch (error) {
|
|
32
|
+
for (const [eventType, listener] of listeners) {
|
|
33
|
+
root.removeEventListener(eventType, listener);
|
|
34
|
+
}
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return createRuntimeController({
|
|
39
|
+
get stopped() {
|
|
40
|
+
return stopped;
|
|
41
|
+
},
|
|
42
|
+
stop() {
|
|
43
|
+
if (stopped) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
stopped = true;
|
|
47
|
+
for (const [eventType, listener] of listeners.splice(0).reverse()) {
|
|
48
|
+
root.removeEventListener(eventType, listener);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}, options.signal);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function assertEventPlan(plan) {
|
|
55
|
+
if (!plan || typeof plan !== "object") {
|
|
56
|
+
throw new TypeError("Event runtime plan must be an object.");
|
|
57
|
+
}
|
|
58
|
+
if (plan.version !== undefined && plan.version !== 1) {
|
|
59
|
+
throw new Error(`Unsupported event runtime plan version: ${String(plan.version)}.`);
|
|
60
|
+
}
|
|
61
|
+
if (!Array.isArray(plan.events)) {
|
|
62
|
+
throw new TypeError("Event runtime plan requires an events array.");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function groupEventBindings(records, elements) {
|
|
67
|
+
const groups = new Map();
|
|
68
|
+
for (const record of records) {
|
|
69
|
+
const [elementIndex, eventType, commands] = assertEventBinding(record);
|
|
70
|
+
const element = elements[elementIndex];
|
|
71
|
+
if (!element) {
|
|
72
|
+
throw new Error(`Event binding target ${elementIndex} was not resolved.`);
|
|
73
|
+
}
|
|
74
|
+
if (!groups.has(eventType)) {
|
|
75
|
+
groups.set(eventType, []);
|
|
76
|
+
}
|
|
77
|
+
groups.get(eventType).push({ element, commands });
|
|
78
|
+
}
|
|
79
|
+
return groups;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function assertEventBinding(record) {
|
|
83
|
+
if (!Array.isArray(record) || record.length !== 3) {
|
|
84
|
+
throw new TypeError("Event binding records must be [element, event, commands].");
|
|
85
|
+
}
|
|
86
|
+
const [elementIndex, eventType, commands] = record;
|
|
87
|
+
if (!Number.isInteger(elementIndex) || elementIndex < 0) {
|
|
88
|
+
throw new TypeError("Event binding element index must be a non-negative integer.");
|
|
89
|
+
}
|
|
90
|
+
if (typeof eventType !== "string" || eventType.length === 0) {
|
|
91
|
+
throw new TypeError("Event binding event type must be a non-empty string.");
|
|
92
|
+
}
|
|
93
|
+
if (!Array.isArray(commands)) {
|
|
94
|
+
throw new TypeError("Event binding commands must be an array.");
|
|
95
|
+
}
|
|
96
|
+
return record;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function dispatchPlannedEvent(event, bindings, handlers, options) {
|
|
100
|
+
for (const binding of bindings) {
|
|
101
|
+
if (!event.composedPath?.().includes(binding.element) && !binding.element.contains(event.target)) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
for (const command of binding.commands) {
|
|
105
|
+
const result = await runCommand(command, event, binding.element, handlers, options);
|
|
106
|
+
if (result === "stop-immediate") {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function runCommand(command, event, element, handlers, options) {
|
|
114
|
+
if (!Array.isArray(command) || typeof command[0] !== "string") {
|
|
115
|
+
throw new TypeError("Event command must be a tuple with a command name.");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
switch (command[0]) {
|
|
119
|
+
case "preventDefault":
|
|
120
|
+
event.preventDefault();
|
|
121
|
+
return;
|
|
122
|
+
case "stopPropagation":
|
|
123
|
+
event.stopPropagation();
|
|
124
|
+
return;
|
|
125
|
+
case "stopImmediatePropagation":
|
|
126
|
+
event.stopImmediatePropagation();
|
|
127
|
+
return "stop-immediate";
|
|
128
|
+
case "setSignal":
|
|
129
|
+
if (!options.signals || typeof options.signals.set !== "function") {
|
|
130
|
+
throw new Error("setSignal command requires a signal runtime controller.");
|
|
131
|
+
}
|
|
132
|
+
options.signals?.set(command[1], readEventValue(event, command[2]));
|
|
133
|
+
return;
|
|
134
|
+
case "handler": {
|
|
135
|
+
const handler = await resolveHandler(command[1], handlers, options);
|
|
136
|
+
await handler({ event, element, el: element, root: options.root, signals: options.signals });
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
default:
|
|
140
|
+
throw new Error(`Unsupported event command: ${command[0]}.`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function readEventValue(event, source) {
|
|
145
|
+
if (!Array.isArray(source)) {
|
|
146
|
+
throw new TypeError("Event value source must be a tuple.");
|
|
147
|
+
}
|
|
148
|
+
switch (source[0]) {
|
|
149
|
+
case "event.target.value":
|
|
150
|
+
return event.target?.value;
|
|
151
|
+
case "event.target.checked":
|
|
152
|
+
return Boolean(event.target?.checked);
|
|
153
|
+
case "constant":
|
|
154
|
+
return source[1];
|
|
155
|
+
default:
|
|
156
|
+
throw new Error(`Unsupported event value source: ${source[0]}.`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function resolveHandler(id, handlers, options) {
|
|
161
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
162
|
+
throw new TypeError("Handler command requires a non-empty id.");
|
|
163
|
+
}
|
|
164
|
+
const descriptor = handlers[id];
|
|
165
|
+
if (typeof descriptor === "function") {
|
|
166
|
+
return descriptor;
|
|
167
|
+
}
|
|
168
|
+
assertStrictDescriptor(id, descriptor);
|
|
169
|
+
const importModule = options.importModule ?? ((specifier) => import(specifier));
|
|
170
|
+
if (!options.moduleCache.has(descriptor.browserImport)) {
|
|
171
|
+
options.moduleCache.set(descriptor.browserImport, importModule(descriptor.browserImport));
|
|
172
|
+
}
|
|
173
|
+
const module = await options.moduleCache.get(descriptor.browserImport);
|
|
174
|
+
const handler = module?.[descriptor.exportName];
|
|
175
|
+
if (typeof handler !== "function") {
|
|
176
|
+
throw new TypeError(`Strict handler "${id}" did not resolve export "${descriptor.exportName}".`);
|
|
177
|
+
}
|
|
178
|
+
return handler;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function assertHandlers(handlers) {
|
|
182
|
+
for (const [id, descriptor] of Object.entries(handlers)) {
|
|
183
|
+
if (typeof descriptor !== "function") {
|
|
184
|
+
assertStrictDescriptor(id, descriptor);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function assertStrictDescriptor(id, descriptor) {
|
|
190
|
+
if (!descriptor || typeof descriptor !== "object") {
|
|
191
|
+
throw new TypeError(`Handler "${id}" must be a function or strict descriptor.`);
|
|
192
|
+
}
|
|
193
|
+
if (descriptor.mode !== undefined && descriptor.mode !== "strict") {
|
|
194
|
+
throw new TypeError(`Handler "${id}" must use a strict descriptor.`);
|
|
195
|
+
}
|
|
196
|
+
if (typeof descriptor.browserImport !== "string" || descriptor.browserImport.length === 0) {
|
|
197
|
+
throw new TypeError(`Handler "${id}" requires browserImport.`);
|
|
198
|
+
}
|
|
199
|
+
if (typeof descriptor.exportName !== "string" || descriptor.exportName.length === 0) {
|
|
200
|
+
throw new TypeError(`Handler "${id}" requires exportName.`);
|
|
201
|
+
}
|
|
202
|
+
if (descriptor.version !== undefined) {
|
|
203
|
+
const url = new URL(descriptor.browserImport, "https://async.local/");
|
|
204
|
+
if (url.searchParams.get("v") !== String(descriptor.version)) {
|
|
205
|
+
throw new TypeError(`Handler "${id}" browserImport must include version query v=${descriptor.version}.`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export function createRuntimeController(controller, signal) {
|
|
2
|
+
if (signal) {
|
|
3
|
+
if (signal.aborted) {
|
|
4
|
+
controller.stop();
|
|
5
|
+
} else {
|
|
6
|
+
signal.addEventListener("abort", () => controller.stop(), { once: true });
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
return controller;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function normalizeRuntimeStartArgs(rootOrOptions, planArg, optionsArg) {
|
|
13
|
+
if (rootOrOptions && typeof rootOrOptions === "object" && "root" in rootOrOptions) {
|
|
14
|
+
return {
|
|
15
|
+
root: requireRoot(rootOrOptions.root),
|
|
16
|
+
plan: rootOrOptions.plan,
|
|
17
|
+
options: {
|
|
18
|
+
...rootOrOptions.options,
|
|
19
|
+
...withoutKeys(rootOrOptions, ["root", "plan", "options"])
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
root: requireRoot(rootOrOptions),
|
|
26
|
+
plan: planArg,
|
|
27
|
+
options: optionsArg ?? {}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function resolveElements(root, locators, options = {}) {
|
|
32
|
+
if (!Array.isArray(locators)) {
|
|
33
|
+
throw new TypeError("Runtime element locators must be an array.");
|
|
34
|
+
}
|
|
35
|
+
return locators.map((locator, index) => resolveElement(root, locator, index, options));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function resolveElement(root, locator, index, options) {
|
|
39
|
+
const record = normalizeLocator(locator);
|
|
40
|
+
const element = root.querySelector(record.selector);
|
|
41
|
+
if (!element && !record.optional) {
|
|
42
|
+
throw new Error(`Runtime locator ${index} did not match: ${record.selector}`);
|
|
43
|
+
}
|
|
44
|
+
if (!element) {
|
|
45
|
+
options.onDiagnostic?.({
|
|
46
|
+
type: "missing-optional-locator",
|
|
47
|
+
index,
|
|
48
|
+
selector: record.selector
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return element;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizeLocator(locator) {
|
|
55
|
+
if (typeof locator === "string") {
|
|
56
|
+
return { selector: locator, optional: false };
|
|
57
|
+
}
|
|
58
|
+
if (locator && typeof locator === "object" && typeof locator.selector === "string") {
|
|
59
|
+
return {
|
|
60
|
+
selector: locator.selector,
|
|
61
|
+
optional: Boolean(locator.optional)
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
throw new TypeError("Runtime element locator must be a selector string or selector record.");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function requireRoot(root) {
|
|
68
|
+
if (!root || typeof root.querySelector !== "function") {
|
|
69
|
+
throw new TypeError("Runtime root must be a Document, Element, or DocumentFragment with querySelector.");
|
|
70
|
+
}
|
|
71
|
+
if (typeof root.addEventListener !== "function") {
|
|
72
|
+
throw new TypeError("Runtime root must support addEventListener.");
|
|
73
|
+
}
|
|
74
|
+
return root;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function withoutKeys(source, keys) {
|
|
78
|
+
const result = {};
|
|
79
|
+
for (const [key, value] of Object.entries(source)) {
|
|
80
|
+
if (!keys.includes(key)) {
|
|
81
|
+
result[key] = value;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Generated by scripts/build-framework-bundle.js. Do not edit by hand.
|
|
2
|
+
// Type declarations for @async/framework/runtime/signals.
|
|
3
|
+
|
|
4
|
+
export type ElementLocator = string | { readonly selector: string; readonly optional?: boolean };
|
|
5
|
+
export type SignalRuntimePlan = {
|
|
6
|
+
readonly version?: 1;
|
|
7
|
+
readonly values?: readonly (readonly [path: string, value: unknown])[];
|
|
8
|
+
readonly bindings?: readonly SignalBindingRecord[];
|
|
9
|
+
};
|
|
10
|
+
export type SignalBindingRecord =
|
|
11
|
+
| readonly [element: number, kind: "text", path: string]
|
|
12
|
+
| readonly [element: number, kind: "value", path: string]
|
|
13
|
+
| readonly [element: number, kind: "attr", name: string, path: string]
|
|
14
|
+
| readonly [element: number, kind: "prop", name: string, path: string]
|
|
15
|
+
| readonly [element: number, kind: "class", token: string, path: string]
|
|
16
|
+
| readonly [element: number, kind: "classList", path: string];
|
|
17
|
+
export type SignalRuntimeOptions = {
|
|
18
|
+
readonly elements?: readonly ElementLocator[];
|
|
19
|
+
readonly signal?: AbortSignal;
|
|
20
|
+
readonly onDiagnostic?: (diagnostic: Record<string, unknown>) => void;
|
|
21
|
+
};
|
|
22
|
+
export type SignalRuntimeController = {
|
|
23
|
+
readonly stopped: boolean;
|
|
24
|
+
stop(): void;
|
|
25
|
+
get(path: string): unknown;
|
|
26
|
+
set(path: string, value: unknown): void;
|
|
27
|
+
update(path: string, fn: (value: unknown) => unknown): unknown;
|
|
28
|
+
subscribe(path: string, fn: (value: unknown) => void): () => void;
|
|
29
|
+
snapshot(): Record<string, unknown>;
|
|
30
|
+
};
|
|
31
|
+
export declare function startSignals(rootOrOptions: ParentNode | ({ root: ParentNode; plan: SignalRuntimePlan } & SignalRuntimeOptions), plan?: SignalRuntimePlan, options?: SignalRuntimeOptions): SignalRuntimeController;
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createRuntimeController,
|
|
3
|
+
normalizeRuntimeStartArgs,
|
|
4
|
+
resolveElements
|
|
5
|
+
} from "./shared.js";
|
|
6
|
+
|
|
7
|
+
export function startSignals(rootOrOptions, planArg, optionsArg) {
|
|
8
|
+
const { root, plan, options } = normalizeRuntimeStartArgs(rootOrOptions, planArg, optionsArg);
|
|
9
|
+
assertSignalPlan(plan);
|
|
10
|
+
const store = new Map();
|
|
11
|
+
const subscribers = new Map();
|
|
12
|
+
const cleanups = [];
|
|
13
|
+
let stopped = false;
|
|
14
|
+
|
|
15
|
+
for (const [path, value] of plan.values ?? []) {
|
|
16
|
+
assertPath(path);
|
|
17
|
+
store.set(path, value);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const elements = options.resolvedElements ?? resolveElements(root, options.elements ?? [], {
|
|
21
|
+
onDiagnostic: options.onDiagnostic
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
for (const binding of plan.bindings ?? []) {
|
|
26
|
+
cleanups.push(bindSignal(binding, elements, { get, subscribe }));
|
|
27
|
+
}
|
|
28
|
+
} catch (error) {
|
|
29
|
+
for (const cleanup of cleanups.splice(0).reverse()) {
|
|
30
|
+
cleanup();
|
|
31
|
+
}
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const controller = createRuntimeController({
|
|
36
|
+
get stopped() {
|
|
37
|
+
return stopped;
|
|
38
|
+
},
|
|
39
|
+
get,
|
|
40
|
+
set,
|
|
41
|
+
update,
|
|
42
|
+
subscribe,
|
|
43
|
+
snapshot,
|
|
44
|
+
stop() {
|
|
45
|
+
if (stopped) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
stopped = true;
|
|
49
|
+
for (const cleanup of cleanups.splice(0).reverse()) {
|
|
50
|
+
cleanup();
|
|
51
|
+
}
|
|
52
|
+
subscribers.clear();
|
|
53
|
+
}
|
|
54
|
+
}, options.signal);
|
|
55
|
+
|
|
56
|
+
return controller;
|
|
57
|
+
|
|
58
|
+
function get(path) {
|
|
59
|
+
assertPath(path);
|
|
60
|
+
return store.get(path);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function set(path, value) {
|
|
64
|
+
assertOpen();
|
|
65
|
+
assertPath(path);
|
|
66
|
+
if (Object.is(store.get(path), value)) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
store.set(path, value);
|
|
70
|
+
for (const fn of subscribers.get(path) ?? []) {
|
|
71
|
+
fn(value);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function update(path, fn) {
|
|
76
|
+
if (typeof fn !== "function") {
|
|
77
|
+
throw new TypeError("update(path, fn) requires a function.");
|
|
78
|
+
}
|
|
79
|
+
const next = fn(get(path));
|
|
80
|
+
set(path, next);
|
|
81
|
+
return next;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function subscribe(path, fn) {
|
|
85
|
+
assertOpen();
|
|
86
|
+
assertPath(path);
|
|
87
|
+
if (typeof fn !== "function") {
|
|
88
|
+
throw new TypeError("subscribe(path, fn) requires a function.");
|
|
89
|
+
}
|
|
90
|
+
if (!subscribers.has(path)) {
|
|
91
|
+
subscribers.set(path, new Set());
|
|
92
|
+
}
|
|
93
|
+
subscribers.get(path).add(fn);
|
|
94
|
+
return () => {
|
|
95
|
+
subscribers.get(path)?.delete(fn);
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function snapshot() {
|
|
100
|
+
return Object.fromEntries(store);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function assertOpen() {
|
|
104
|
+
if (stopped) {
|
|
105
|
+
throw new Error("Signal runtime controller has stopped.");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function assertSignalPlan(plan) {
|
|
111
|
+
if (!plan || typeof plan !== "object") {
|
|
112
|
+
throw new TypeError("Signal runtime plan must be an object.");
|
|
113
|
+
}
|
|
114
|
+
if (plan.version !== undefined && plan.version !== 1) {
|
|
115
|
+
throw new Error(`Unsupported signal runtime plan version: ${String(plan.version)}.`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function bindSignal(record, elements, signals) {
|
|
120
|
+
const [elementIndex, kind, ...args] = assertSignalBinding(record);
|
|
121
|
+
const element = elements[elementIndex];
|
|
122
|
+
if (!element) {
|
|
123
|
+
throw new Error(`Signal binding target ${elementIndex} was not resolved.`);
|
|
124
|
+
}
|
|
125
|
+
const path = args.at(-1);
|
|
126
|
+
const apply = () => applyBinding(element, kind, args, signals.get(path));
|
|
127
|
+
apply();
|
|
128
|
+
return signals.subscribe(path, apply);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function assertSignalBinding(record) {
|
|
132
|
+
if (!Array.isArray(record) || record.length < 3) {
|
|
133
|
+
throw new TypeError("Signal binding records must be tuple arrays.");
|
|
134
|
+
}
|
|
135
|
+
const [elementIndex, kind] = record;
|
|
136
|
+
if (!Number.isInteger(elementIndex) || elementIndex < 0) {
|
|
137
|
+
throw new TypeError("Signal binding element index must be a non-negative integer.");
|
|
138
|
+
}
|
|
139
|
+
if (!["text", "value", "attr", "prop", "class", "classList"].includes(kind)) {
|
|
140
|
+
throw new Error(`Unsupported signal binding kind: ${String(kind)}.`);
|
|
141
|
+
}
|
|
142
|
+
return record;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function applyBinding(element, kind, args, value) {
|
|
146
|
+
switch (kind) {
|
|
147
|
+
case "text":
|
|
148
|
+
element.textContent = stringify(value);
|
|
149
|
+
return;
|
|
150
|
+
case "value":
|
|
151
|
+
if ("value" in element) {
|
|
152
|
+
element.value = value ?? "";
|
|
153
|
+
} else {
|
|
154
|
+
applyAttribute(element, "value", value);
|
|
155
|
+
}
|
|
156
|
+
return;
|
|
157
|
+
case "attr":
|
|
158
|
+
applyAttribute(element, args[0], value);
|
|
159
|
+
return;
|
|
160
|
+
case "prop":
|
|
161
|
+
element[args[0]] = value;
|
|
162
|
+
return;
|
|
163
|
+
case "class":
|
|
164
|
+
element.classList.toggle(args[0], Boolean(value));
|
|
165
|
+
return;
|
|
166
|
+
case "classList":
|
|
167
|
+
applyClassList(element, value);
|
|
168
|
+
return;
|
|
169
|
+
default:
|
|
170
|
+
throw new Error(`Unsupported signal binding kind: ${String(kind)}.`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function applyAttribute(element, name, value) {
|
|
175
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
176
|
+
throw new TypeError("Attribute signal binding requires an attribute name.");
|
|
177
|
+
}
|
|
178
|
+
if (value === false || value === null || value === undefined) {
|
|
179
|
+
element.removeAttribute(name);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (value === true) {
|
|
183
|
+
element.setAttribute(name, "");
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
element.setAttribute(name, String(value));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function applyClassList(element, value) {
|
|
190
|
+
const previous = element.__asyncRuntimeClassList ?? new Set();
|
|
191
|
+
for (const token of previous) {
|
|
192
|
+
element.classList.remove(token);
|
|
193
|
+
}
|
|
194
|
+
const next = new Set(String(value ?? "").split(/\s+/).filter(Boolean));
|
|
195
|
+
for (const token of next) {
|
|
196
|
+
element.classList.add(token);
|
|
197
|
+
}
|
|
198
|
+
element.__asyncRuntimeClassList = next;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function assertPath(path) {
|
|
202
|
+
if (typeof path !== "string" || path.length === 0) {
|
|
203
|
+
throw new TypeError("Signal path must be a non-empty string.");
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function stringify(value) {
|
|
208
|
+
return value === null || value === undefined ? "" : String(value);
|
|
209
|
+
}
|
package/runtime.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Generated by scripts/build-framework-bundle.js. Do not edit by hand.
|
|
2
|
+
// Type declarations for @async/framework/runtime.
|
|
3
|
+
|
|
4
|
+
export type ElementLocator = string | { readonly selector: string; readonly optional?: boolean };
|
|
5
|
+
export type RuntimeSliceController = {
|
|
6
|
+
readonly stopped: boolean;
|
|
7
|
+
stop(): void;
|
|
8
|
+
};
|
|
9
|
+
export type SignalRuntimePlan = {
|
|
10
|
+
readonly version?: 1;
|
|
11
|
+
readonly values?: readonly (readonly [path: string, value: unknown])[];
|
|
12
|
+
readonly bindings?: readonly SignalBindingRecord[];
|
|
13
|
+
};
|
|
14
|
+
export type SignalBindingRecord =
|
|
15
|
+
| readonly [element: number, kind: "text", path: string]
|
|
16
|
+
| readonly [element: number, kind: "value", path: string]
|
|
17
|
+
| readonly [element: number, kind: "attr", name: string, path: string]
|
|
18
|
+
| readonly [element: number, kind: "prop", name: string, path: string]
|
|
19
|
+
| readonly [element: number, kind: "class", token: string, path: string]
|
|
20
|
+
| readonly [element: number, kind: "classList", path: string];
|
|
21
|
+
export type EventRuntimePlan = {
|
|
22
|
+
readonly version?: 1;
|
|
23
|
+
readonly events: readonly EventBindingRecord[];
|
|
24
|
+
readonly handlers?: Record<string, HandlerDescriptor>;
|
|
25
|
+
};
|
|
26
|
+
export type EventBindingRecord = readonly [element: number, event: string, commands: readonly EventCommand[]];
|
|
27
|
+
export type EventCommand =
|
|
28
|
+
| readonly ["handler", id: string]
|
|
29
|
+
| readonly ["preventDefault"]
|
|
30
|
+
| readonly ["stopPropagation"]
|
|
31
|
+
| readonly ["stopImmediatePropagation"]
|
|
32
|
+
| readonly ["setSignal", path: string, valueSource: EventValueSource];
|
|
33
|
+
export type EventValueSource =
|
|
34
|
+
| readonly ["event.target.value"]
|
|
35
|
+
| readonly ["event.target.checked"]
|
|
36
|
+
| readonly ["constant", value: unknown];
|
|
37
|
+
export type EventRuntimeContext = {
|
|
38
|
+
event: Event;
|
|
39
|
+
element: Element;
|
|
40
|
+
el: Element;
|
|
41
|
+
root: ParentNode;
|
|
42
|
+
signals?: SignalRuntimeController;
|
|
43
|
+
};
|
|
44
|
+
export type StrictHandlerDescriptor = {
|
|
45
|
+
readonly mode?: "strict";
|
|
46
|
+
readonly module?: string;
|
|
47
|
+
readonly browserImport: string;
|
|
48
|
+
readonly exportName: string;
|
|
49
|
+
readonly version?: string;
|
|
50
|
+
readonly integrity?: string;
|
|
51
|
+
};
|
|
52
|
+
export type HandlerDescriptor = ((context: EventRuntimeContext) => unknown | Promise<unknown>) | StrictHandlerDescriptor;
|
|
53
|
+
export type RuntimePlan = {
|
|
54
|
+
readonly version: 1;
|
|
55
|
+
readonly elements?: readonly ElementLocator[];
|
|
56
|
+
readonly signals?: SignalRuntimePlan;
|
|
57
|
+
readonly events?: EventRuntimePlan;
|
|
58
|
+
};
|
|
59
|
+
export type RuntimeStartOptions = {
|
|
60
|
+
readonly signal?: AbortSignal;
|
|
61
|
+
readonly importModule?: (specifier: string) => Promise<Record<string, unknown>>;
|
|
62
|
+
readonly onDiagnostic?: (diagnostic: Record<string, unknown>) => void;
|
|
63
|
+
};
|
|
64
|
+
export type SignalRuntimeController = RuntimeSliceController & {
|
|
65
|
+
get(path: string): unknown;
|
|
66
|
+
set(path: string, value: unknown): void;
|
|
67
|
+
update(path: string, fn: (value: unknown) => unknown): unknown;
|
|
68
|
+
subscribe(path: string, fn: (value: unknown) => void): () => void;
|
|
69
|
+
snapshot(): Record<string, unknown>;
|
|
70
|
+
};
|
|
71
|
+
export type RuntimeController = RuntimeSliceController;
|
|
72
|
+
export declare function start(rootOrOptions: ParentNode | ({ root: ParentNode; plan: RuntimePlan } & RuntimeStartOptions), plan?: RuntimePlan, options?: RuntimeStartOptions): RuntimeController;
|