@async/framework 0.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/CHANGELOG.md +15 -0
- package/LICENSE +21 -0
- package/README.md +608 -0
- package/examples/cache/index.html +16 -0
- package/examples/cache/main.js +47 -0
- package/examples/components/index.html +11 -0
- package/examples/components/main.js +26 -0
- package/examples/counter/index.html +15 -0
- package/examples/counter/main.js +17 -0
- package/examples/partials/index.html +15 -0
- package/examples/partials/main.js +43 -0
- package/examples/product/index.html +32 -0
- package/examples/product/main.js +24 -0
- package/examples/router/index.html +21 -0
- package/examples/router/main.js +52 -0
- package/examples/server-call/index.html +21 -0
- package/examples/server-call/main.js +22 -0
- package/examples/ssr/index.html +12 -0
- package/examples/ssr/main.js +89 -0
- package/examples/streaming/index.html +16 -0
- package/examples/streaming/main.js +30 -0
- package/package.json +67 -0
- package/src/app.js +383 -0
- package/src/async-signal.js +238 -0
- package/src/cache.js +145 -0
- package/src/component.js +182 -0
- package/src/delay.js +30 -0
- package/src/handlers.js +175 -0
- package/src/html.js +65 -0
- package/src/index.js +12 -0
- package/src/loader.js +394 -0
- package/src/partials.js +96 -0
- package/src/router.js +367 -0
- package/src/server.js +369 -0
- package/src/signals.js +483 -0
package/src/server.js
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
const serverEnvelopeKeys = new Set(["value", "signals", "boundary", "html", "redirect", "error"]);
|
|
2
|
+
|
|
3
|
+
export function createServerRegistry(initialMap = {}) {
|
|
4
|
+
const entries = new Map();
|
|
5
|
+
const defaults = {};
|
|
6
|
+
|
|
7
|
+
const registry = {
|
|
8
|
+
register(id, fn) {
|
|
9
|
+
assertServerId(id);
|
|
10
|
+
if (typeof fn !== "function") {
|
|
11
|
+
throw new TypeError(`Server function "${id}" must be a function.`);
|
|
12
|
+
}
|
|
13
|
+
if (entries.has(id)) {
|
|
14
|
+
throw new Error(`Server function "${id}" is already registered.`);
|
|
15
|
+
}
|
|
16
|
+
entries.set(id, fn);
|
|
17
|
+
return id;
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
registerMany(map) {
|
|
21
|
+
for (const [id, fn] of Object.entries(map ?? {})) {
|
|
22
|
+
registry.register(id, fn);
|
|
23
|
+
}
|
|
24
|
+
return registry;
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
resolve(id) {
|
|
28
|
+
assertServerId(id);
|
|
29
|
+
return entries.get(id);
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
async run(id, args = [], context = {}) {
|
|
33
|
+
assertServerId(id);
|
|
34
|
+
const fn = registry.resolve(id);
|
|
35
|
+
if (!fn) {
|
|
36
|
+
throw new Error(`Server function "${id}" is not registered.`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let runContext;
|
|
40
|
+
const server = createServerNamespace((childId, childArgs, childContext = {}) => {
|
|
41
|
+
return registry.run(childId, childArgs, { ...runContext, ...childContext });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const mergedContext = {
|
|
45
|
+
...defaults,
|
|
46
|
+
...context,
|
|
47
|
+
cache: defaults.cache ?? context.cache
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
runContext = {
|
|
51
|
+
...mergedContext,
|
|
52
|
+
id,
|
|
53
|
+
args,
|
|
54
|
+
input: mergedContext.input,
|
|
55
|
+
signals: createSignalReader(mergedContext.signals),
|
|
56
|
+
abort: mergedContext.abort,
|
|
57
|
+
cache: mergedContext.cache,
|
|
58
|
+
server
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return fn.call(runContext, ...args);
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
_setContext(context = {}) {
|
|
65
|
+
Object.assign(defaults, context);
|
|
66
|
+
return registry;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
registry.registerMany(initialMap);
|
|
71
|
+
return createServerNamespace((id, args, context) => registry.run(id, args, context), registry);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function createServerProxy({
|
|
75
|
+
endpoint = "/__async/server",
|
|
76
|
+
fetch: fetchImpl = globalThis.fetch?.bind(globalThis),
|
|
77
|
+
signals,
|
|
78
|
+
loader,
|
|
79
|
+
router,
|
|
80
|
+
cache,
|
|
81
|
+
headers = {}
|
|
82
|
+
} = {}) {
|
|
83
|
+
if (typeof fetchImpl !== "function") {
|
|
84
|
+
throw new TypeError("createServerProxy(...) requires fetch to be available.");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const defaults = { signals, loader, router, cache };
|
|
88
|
+
|
|
89
|
+
async function run(id, args = [], context = {}) {
|
|
90
|
+
assertServerId(id);
|
|
91
|
+
const runContext = { ...defaults, ...context };
|
|
92
|
+
const body = {
|
|
93
|
+
args,
|
|
94
|
+
input: context.input ?? defaultInput(runContext),
|
|
95
|
+
signals: context.signalValues ?? snapshotSignalPaths(context.signalPaths, runContext.signals)
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const response = await fetchImpl(joinEndpoint(endpoint, id), {
|
|
99
|
+
method: "POST",
|
|
100
|
+
headers: {
|
|
101
|
+
"content-type": "application/json",
|
|
102
|
+
...headers
|
|
103
|
+
},
|
|
104
|
+
body: JSON.stringify(body),
|
|
105
|
+
signal: context.abort
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (!response.ok) {
|
|
109
|
+
throw new Error(`Server function "${id}" failed with ${response.status}.`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const result = await readServerResponse(response);
|
|
113
|
+
await applyServerResult(result, runContext);
|
|
114
|
+
return unwrapServerResult(result);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return createServerNamespace(run, {
|
|
118
|
+
run,
|
|
119
|
+
_setContext(context = {}) {
|
|
120
|
+
Object.assign(defaults, context);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function resolveServerCommandArguments(args, context = {}) {
|
|
126
|
+
const resolved = [];
|
|
127
|
+
const signalValues = {};
|
|
128
|
+
const signalPaths = [];
|
|
129
|
+
|
|
130
|
+
for (const arg of args) {
|
|
131
|
+
if (arg.type === "local") {
|
|
132
|
+
resolved.push(resolveLocal(arg.name, context, { forServer: true }));
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const value = readSignal(context.signals, arg.path);
|
|
137
|
+
resolved.push(value);
|
|
138
|
+
signalValues[arg.path] = value;
|
|
139
|
+
signalPaths.push(arg.path);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return { args: resolved, signalValues, signalPaths };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function applyServerResult(result, context = {}) {
|
|
146
|
+
if (!isServerEnvelope(result)) {
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (result.signals && context.signals) {
|
|
151
|
+
for (const [path, value] of Object.entries(result.signals)) {
|
|
152
|
+
context.signals.set?.(path, value);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (result.cache?.browser && context.cache?.restore) {
|
|
157
|
+
context.cache.restore(result.cache.browser);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (result.boundary && Object.hasOwn(result, "html")) {
|
|
161
|
+
context.loader?.swap?.(result.boundary, result.html);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (result.redirect) {
|
|
165
|
+
await context.router?.navigate?.(result.redirect);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (result.error) {
|
|
169
|
+
throw toError(result.error);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function unwrapServerResult(result) {
|
|
176
|
+
if (isServerEnvelope(result) && Object.hasOwn(result, "value")) {
|
|
177
|
+
return result.value;
|
|
178
|
+
}
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function defaultInput(context = {}) {
|
|
183
|
+
const form = findForm(context);
|
|
184
|
+
if (form) {
|
|
185
|
+
return formDataToObject(new form.ownerDocument.defaultView.FormData(form));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const element = context.element ?? context.el ?? context.event?.target;
|
|
189
|
+
if (!element) {
|
|
190
|
+
return {};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
value: "value" in element ? element.value : undefined,
|
|
195
|
+
checked: "checked" in element ? element.checked : undefined,
|
|
196
|
+
dataset: element.dataset ? { ...element.dataset } : {}
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function createServerNamespace(run, root = {}) {
|
|
201
|
+
const cache = new Map();
|
|
202
|
+
|
|
203
|
+
function namespace(parts) {
|
|
204
|
+
const cacheKey = parts.join(".");
|
|
205
|
+
if (cache.has(cacheKey)) {
|
|
206
|
+
return cache.get(cacheKey);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const callable = (...args) => {
|
|
210
|
+
if (parts.length === 0) {
|
|
211
|
+
throw new Error("Server namespace is not directly callable.");
|
|
212
|
+
}
|
|
213
|
+
return Promise.resolve(run(parts.join("."), args)).then(unwrapServerResult);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const proxy = new Proxy(callable, {
|
|
217
|
+
get(_target, prop) {
|
|
218
|
+
if (prop === "then") {
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
221
|
+
if (prop in _target) {
|
|
222
|
+
return _target[prop];
|
|
223
|
+
}
|
|
224
|
+
if (parts.length === 0 && Object.hasOwn(root, prop)) {
|
|
225
|
+
return root[prop];
|
|
226
|
+
}
|
|
227
|
+
if (prop === Symbol.toStringTag) {
|
|
228
|
+
return "AsyncServerNamespace";
|
|
229
|
+
}
|
|
230
|
+
if (prop === "toString") {
|
|
231
|
+
return () => parts.length === 0 ? "server" : `server.${parts.join(".")}`;
|
|
232
|
+
}
|
|
233
|
+
return namespace([...parts, String(prop)]);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
cache.set(cacheKey, proxy);
|
|
238
|
+
return proxy;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return namespace([]);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function readServerResponse(response) {
|
|
245
|
+
const type = response.headers.get("content-type") ?? "";
|
|
246
|
+
if (type.includes("application/json")) {
|
|
247
|
+
return response.json();
|
|
248
|
+
}
|
|
249
|
+
return { value: await response.text() };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function snapshotSignalPaths(paths = [], signals) {
|
|
253
|
+
const snapshot = {};
|
|
254
|
+
for (const path of paths) {
|
|
255
|
+
snapshot[path] = readSignal(signals, path);
|
|
256
|
+
}
|
|
257
|
+
return snapshot;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function readSignal(signals, path) {
|
|
261
|
+
if (!signals || typeof signals.get !== "function") {
|
|
262
|
+
throw new Error(`Signal "${path}" cannot be read without a signal registry.`);
|
|
263
|
+
}
|
|
264
|
+
return signals.get(path);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function createSignalReader(signals) {
|
|
268
|
+
if (!signals || typeof signals.get === "function") {
|
|
269
|
+
return signals;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
get(path) {
|
|
274
|
+
return readPath(signals, path);
|
|
275
|
+
},
|
|
276
|
+
snapshot() {
|
|
277
|
+
return { ...signals };
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function readPath(source, path) {
|
|
283
|
+
return String(path)
|
|
284
|
+
.split(".")
|
|
285
|
+
.reduce((value, part) => value?.[part], source);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function resolveLocal(name, context, { forServer } = {}) {
|
|
289
|
+
if ((name === "$event" || name === "$el") && forServer) {
|
|
290
|
+
throw new Error(`${name} cannot be passed to a server command.`);
|
|
291
|
+
}
|
|
292
|
+
if (name === "$event") {
|
|
293
|
+
return context.event;
|
|
294
|
+
}
|
|
295
|
+
if (name === "$el") {
|
|
296
|
+
return context.element ?? context.el;
|
|
297
|
+
}
|
|
298
|
+
if (name === "$value") {
|
|
299
|
+
const element = context.element ?? context.el ?? context.event?.target;
|
|
300
|
+
return element?.value;
|
|
301
|
+
}
|
|
302
|
+
if (name === "$checked") {
|
|
303
|
+
const element = context.element ?? context.el ?? context.event?.target;
|
|
304
|
+
return element?.checked;
|
|
305
|
+
}
|
|
306
|
+
if (name === "$form") {
|
|
307
|
+
const form = findForm(context);
|
|
308
|
+
return form ? formDataToObject(new form.ownerDocument.defaultView.FormData(form)) : {};
|
|
309
|
+
}
|
|
310
|
+
if (name === "$dataset") {
|
|
311
|
+
const element = context.element ?? context.el ?? context.event?.target;
|
|
312
|
+
return element?.dataset ? { ...element.dataset } : {};
|
|
313
|
+
}
|
|
314
|
+
throw new Error(`Event local "${name}" is not supported.`);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function findForm(context) {
|
|
318
|
+
const event = context.event;
|
|
319
|
+
const element = context.element ?? context.el ?? event?.target;
|
|
320
|
+
if (element?.tagName === "FORM") {
|
|
321
|
+
return element;
|
|
322
|
+
}
|
|
323
|
+
if (event?.type === "submit" && event.target?.tagName === "FORM") {
|
|
324
|
+
return event.target;
|
|
325
|
+
}
|
|
326
|
+
if (event?.type === "submit" && element?.form) {
|
|
327
|
+
return element.form;
|
|
328
|
+
}
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function formDataToObject(formData) {
|
|
333
|
+
const output = {};
|
|
334
|
+
for (const [key, value] of formData.entries()) {
|
|
335
|
+
if (Object.hasOwn(output, key)) {
|
|
336
|
+
output[key] = Array.isArray(output[key]) ? [...output[key], value] : [output[key], value];
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
output[key] = value;
|
|
340
|
+
}
|
|
341
|
+
return output;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function joinEndpoint(endpoint, id) {
|
|
345
|
+
return `${String(endpoint).replace(/\/$/, "")}/${encodeURIComponent(id)}`;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function isServerEnvelope(value) {
|
|
349
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
return Object.keys(value).some((key) => serverEnvelopeKeys.has(key));
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function toError(value) {
|
|
356
|
+
if (value instanceof Error) {
|
|
357
|
+
return value;
|
|
358
|
+
}
|
|
359
|
+
if (value && typeof value === "object" && typeof value.message === "string") {
|
|
360
|
+
return Object.assign(new Error(value.message), value);
|
|
361
|
+
}
|
|
362
|
+
return new Error(String(value));
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function assertServerId(id) {
|
|
366
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
367
|
+
throw new TypeError("Server function id must be a non-empty string.");
|
|
368
|
+
}
|
|
369
|
+
}
|