@async/framework 0.10.2 → 0.11.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.
Files changed (53) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +23 -7
  3. package/browser.d.ts +4 -7
  4. package/browser.js +17 -66
  5. package/browser.min.js +1 -1
  6. package/browser.ts +17 -66
  7. package/browser.umd.js +17 -66
  8. package/browser.umd.min.js +1 -1
  9. package/{server.d.ts → framework.d.ts} +4 -7
  10. package/framework.ts +5946 -0
  11. package/package.json +25 -17
  12. package/server.js +5945 -0
  13. package/examples/cache/index.html +0 -16
  14. package/examples/cache/main.js +0 -47
  15. package/examples/components/index.html +0 -11
  16. package/examples/components/main.js +0 -26
  17. package/examples/counter/index.html +0 -15
  18. package/examples/counter/main.js +0 -17
  19. package/examples/partials/index.html +0 -15
  20. package/examples/partials/main.js +0 -43
  21. package/examples/product/index.html +0 -32
  22. package/examples/product/main.js +0 -24
  23. package/examples/router/index.html +0 -18
  24. package/examples/router/main.js +0 -52
  25. package/examples/server-call/index.html +0 -21
  26. package/examples/server-call/main.js +0 -22
  27. package/examples/ssr/index.html +0 -12
  28. package/examples/ssr/main.js +0 -89
  29. package/examples/streaming/index.html +0 -16
  30. package/examples/streaming/main.js +0 -30
  31. package/src/app.js +0 -802
  32. package/src/async-signal.js +0 -277
  33. package/src/attributes.js +0 -52
  34. package/src/boundary-receiver.js +0 -302
  35. package/src/browser.js +0 -18
  36. package/src/cache.js +0 -193
  37. package/src/component.js +0 -373
  38. package/src/delay.js +0 -30
  39. package/src/elements.js +0 -63
  40. package/src/handlers.js +0 -219
  41. package/src/html.js +0 -158
  42. package/src/index.js +0 -20
  43. package/src/lazy-registry.js +0 -218
  44. package/src/loader.js +0 -772
  45. package/src/partials.js +0 -133
  46. package/src/registry-store.js +0 -267
  47. package/src/request-context.js +0 -40
  48. package/src/router.js +0 -617
  49. package/src/scheduler.js +0 -300
  50. package/src/server-entry.js +0 -20
  51. package/src/server-registry.js +0 -97
  52. package/src/server.js +0 -362
  53. package/src/signals.js +0 -592
package/src/router.js DELETED
@@ -1,617 +0,0 @@
1
- import { Loader } from "./loader.js";
2
- import { createHandlerRegistry } from "./handlers.js";
3
- import { createScheduler } from "./scheduler.js";
4
- import { createSignalRegistry } from "./signals.js";
5
- import { applyServerResult } from "./server.js";
6
- import { createRegistryStore } from "./registry-store.js";
7
- import { normalizeAttributeConfig } from "./attributes.js";
8
-
9
- export function defineRoute(partial, options = {}) {
10
- return {
11
- ...options,
12
- partial
13
- };
14
- }
15
-
16
- export const route = defineRoute;
17
-
18
- export function createRouteRegistry(initialMap = {}, options = {}) {
19
- const registryStore = options.registry ?? createRegistryStore();
20
- const type = options.type ?? "route";
21
- const entries = registryStore._map(type);
22
- const routes = [];
23
-
24
- const registry = {
25
- registry: registryStore,
26
-
27
- register(pattern, definition) {
28
- assertPattern(pattern);
29
- if (routes.some((candidate) => candidate.pattern === pattern)) {
30
- throw new Error(`Route "${pattern}" is already registered.`);
31
- }
32
- const nextRoute = normalizeRoute(pattern, definition);
33
- entries.set(pattern, nextRoute.definition);
34
- routes.push(nextRoute);
35
- sortRoutes(routes);
36
- return nextRoute;
37
- },
38
-
39
- registerMany(map) {
40
- for (const [pattern, definition] of Object.entries(map ?? {})) {
41
- registry.register(pattern, definition);
42
- }
43
- return registry;
44
- },
45
-
46
- unregister(pattern) {
47
- assertPattern(pattern);
48
- const index = routes.findIndex((candidate) => candidate.pattern === pattern);
49
- if (index !== -1) {
50
- routes.splice(index, 1);
51
- }
52
- return entries.delete(pattern);
53
- },
54
-
55
- match(url) {
56
- const path = toUrl(url).pathname;
57
- for (const candidate of routes) {
58
- const match = candidate.regex.exec(path);
59
- if (!match) {
60
- continue;
61
- }
62
- const params = {};
63
- candidate.keys.forEach((key, index) => {
64
- params[key] = safeDecodeURIComponent(match[index + 1] ?? "");
65
- });
66
- return {
67
- pattern: candidate.pattern,
68
- params,
69
- route: candidate.definition
70
- };
71
- }
72
- return null;
73
- },
74
-
75
- entries() {
76
- return routes.map(({ pattern, definition }) => ({ pattern, route: definition }));
77
- },
78
-
79
- keys() {
80
- return [...entries.keys()];
81
- },
82
-
83
- inspect() {
84
- return registryStore.entries(type);
85
- },
86
-
87
- _adoptMany(map = {}) {
88
- for (const pattern of Object.keys(map ?? {})) {
89
- adoptRoute(pattern, entries.get(pattern));
90
- }
91
- return registry;
92
- }
93
- };
94
-
95
- for (const [pattern, definition] of entries) {
96
- adoptRoute(pattern, definition);
97
- }
98
- registry.registerMany(initialMap);
99
- return registry;
100
-
101
- function adoptRoute(pattern, definition) {
102
- if (routes.some((candidate) => candidate.pattern === pattern)) {
103
- return;
104
- }
105
- const nextRoute = normalizeRoute(pattern, definition);
106
- entries.set(pattern, nextRoute.definition);
107
- routes.push(nextRoute);
108
- sortRoutes(routes);
109
- }
110
- }
111
-
112
- export function createRouter({
113
- mode = "ssr-spa",
114
- root,
115
- boundary = "route",
116
- routes = createRouteRegistry(),
117
- loader,
118
- signals,
119
- handlers,
120
- server,
121
- cache,
122
- partials,
123
- fetch: fetchImpl = globalThis.fetch?.bind(globalThis),
124
- routeEndpoint = "/__async/route",
125
- attributes,
126
- scheduler
127
- } = {}) {
128
- const documentRef = root?.ownerDocument ?? root ?? globalThis.document;
129
- const rootNode = root ?? documentRef;
130
- const signalRegistry = signals ?? loader?.signals ?? createSignalRegistry();
131
- const handlerRegistry = handlers ?? loader?.handlers ?? createHandlerRegistry();
132
- const schedulerInstance = scheduler ?? loader?.scheduler ?? createScheduler();
133
- const ownsScheduler = !scheduler && !loader?.scheduler;
134
- const attributeConfig = normalizeAttributeConfig(attributes ?? loader?.attributes);
135
- const loaderInstance =
136
- loader ??
137
- Loader({
138
- root: rootNode,
139
- signals: signalRegistry,
140
- handlers: handlerRegistry,
141
- server,
142
- cache,
143
- scheduler: schedulerInstance,
144
- attributes: attributeConfig
145
- });
146
- const ownsLoader = !loader;
147
- const cleanups = new Set();
148
- let destroyed = false;
149
- let navigationVersion = 0;
150
- let activeNavigation;
151
-
152
- const api = {
153
- mode,
154
- root: rootNode,
155
- boundary,
156
- routes,
157
- loader: loaderInstance,
158
- signals: signalRegistry,
159
- handlers: handlerRegistry,
160
- server,
161
- cache,
162
- partials,
163
- scheduler: schedulerInstance,
164
- attributes: attributeConfig,
165
-
166
- start() {
167
- assertActive();
168
- loaderInstance.router = api;
169
- signalRegistry._setContext?.({ router: api, loader: loaderInstance, server, cache, scheduler: schedulerInstance });
170
- if (ownsLoader) {
171
- loaderInstance.start();
172
- }
173
- if (mode === "mpa" || mode === "ssr") {
174
- updateStateFromLocation();
175
- return api;
176
- }
177
- bindNavigation();
178
- if (mode === "csr") {
179
- handleNavigation(api.navigate(currentUrl(), {
180
- replace: true,
181
- initial: true,
182
- source: "client"
183
- }));
184
- return api;
185
- }
186
- updateStateFromLocation();
187
- return api;
188
- },
189
-
190
- match(url) {
191
- return routes.match(resolveUrl(url));
192
- },
193
-
194
- prefetch(url) {
195
- assertActive();
196
- if (mode === "ssr-spa" && typeof fetchImpl === "function") {
197
- return fetchRoute(url, { prefetch: true });
198
- }
199
- const matched = api.match(url);
200
- if (matched?.route?.partial && partials?.resolve?.(matched.route.partial)) {
201
- return partials.render(matched.route.partial, matched.params, contextFor(matched));
202
- }
203
- if (typeof fetchImpl === "function") {
204
- return fetchRoute(url, { prefetch: true });
205
- }
206
- return Promise.resolve(null);
207
- },
208
-
209
- async navigate(url, options = {}) {
210
- assertActive();
211
- if (mode === "mpa" || mode === "ssr") {
212
- documentRef.defaultView?.location?.assign?.(url);
213
- return null;
214
- }
215
-
216
- const target = resolveUrl(url);
217
- if (mode === "ssr-spa") {
218
- return fetchRoutePartial(target, options);
219
- }
220
- return renderLocalRoutePartial(target, options);
221
- },
222
-
223
- destroy() {
224
- if (destroyed) {
225
- return;
226
- }
227
- destroyed = true;
228
- activeNavigation?.controller.abort(new Error("Router has been destroyed."));
229
- for (const cleanup of cleanups) {
230
- cleanup();
231
- }
232
- cleanups.clear();
233
- if (ownsScheduler) {
234
- schedulerInstance.destroy();
235
- }
236
- }
237
- };
238
-
239
- return api;
240
-
241
- function bindNavigation() {
242
- const click = (event) => {
243
- const anchor = closest(event.target, "a[href]");
244
- if (!anchor || shouldIgnoreLink(event, anchor)) {
245
- return;
246
- }
247
- event.preventDefault();
248
- handleNavigation(api.navigate(anchor.href));
249
- };
250
- const submit = (event) => {
251
- const form = closest(event.target, "form");
252
- if (!form || shouldIgnoreForm(form)) {
253
- return;
254
- }
255
- event.preventDefault();
256
- handleNavigation(api.navigate(formActionUrl(form)));
257
- };
258
- const popstate = () => handleNavigation(api.navigate(currentUrl(), { history: false }));
259
-
260
- rootNode.addEventListener?.("click", click);
261
- rootNode.addEventListener?.("submit", submit);
262
- documentRef.defaultView?.addEventListener?.("popstate", popstate);
263
- cleanups.add(() => rootNode.removeEventListener?.("click", click));
264
- cleanups.add(() => rootNode.removeEventListener?.("submit", submit));
265
- cleanups.add(() => documentRef.defaultView?.removeEventListener?.("popstate", popstate));
266
- }
267
-
268
- async function renderLocalRoutePartial(target, options = {}) {
269
- const matched = api.match(target);
270
- if (!matched) {
271
- beginNavigation(target, null);
272
- setNoRouteError(target);
273
- return null;
274
- }
275
-
276
- const navigation = beginNavigation(target, matched);
277
- setMatchedRouterState(target, matched, { pending: true, error: null });
278
-
279
- try {
280
- if (!matched.route?.partial || !partials?.resolve?.(matched.route.partial)) {
281
- const error = new Error(`Route "${target.pathname}" does not have a registered partial.`);
282
- if (isActiveNavigation(navigation)) {
283
- setRouterState({ pending: false, error });
284
- }
285
- return null;
286
- }
287
-
288
- const result = await partials.render(matched.route.partial, matched.params, contextFor(matched, navigation));
289
- if (!isActiveNavigation(navigation)) {
290
- return null;
291
- }
292
- await applyNavigationResult(result, target, options, navigation);
293
- if (!isActiveNavigation(navigation)) {
294
- return null;
295
- }
296
- setRouterState({ pending: false, error: null });
297
- return result;
298
- } catch (error) {
299
- if (!isActiveNavigation(navigation)) {
300
- return null;
301
- }
302
- setRouterState({ pending: false, error });
303
- throw error;
304
- }
305
- }
306
-
307
- async function fetchRoutePartial(target, options = {}) {
308
- const matched = api.match(target);
309
- const navigation = beginNavigation(target, matched);
310
- setMatchedRouterState(target, matched, { pending: true, error: null });
311
-
312
- try {
313
- const result = await fetchRoute(target.href, { signal: navigation.abort });
314
- if (!isActiveNavigation(navigation)) {
315
- return null;
316
- }
317
- await applyNavigationResult(result, target, options, navigation);
318
- if (!isActiveNavigation(navigation)) {
319
- return null;
320
- }
321
- setRouterState({ pending: false, error: null });
322
- return result;
323
- } catch (error) {
324
- if (!isActiveNavigation(navigation)) {
325
- return null;
326
- }
327
- setRouterState({ pending: false, error });
328
- throw error;
329
- }
330
- }
331
-
332
- async function applyNavigationResult(result, target, options, navigation) {
333
- if (!isActiveNavigation(navigation)) {
334
- return;
335
- }
336
- await applyServerResult(result, {
337
- signals: signalRegistry,
338
- loader: loaderInstance,
339
- router: api,
340
- cache,
341
- scheduler: schedulerInstance,
342
- abort: navigation?.abort
343
- });
344
- await schedulerInstance.flush();
345
- if (!isActiveNavigation(navigation)) {
346
- return;
347
- }
348
- if (result?.html != null && !result.boundary && !result.redirect) {
349
- loaderInstance.swap(boundary, result.html);
350
- await schedulerInstance.flush();
351
- }
352
- if (result?.redirect || options.history === false) {
353
- return;
354
- }
355
- if (options.replace) {
356
- documentRef.defaultView?.history?.replaceState?.({}, "", target.href);
357
- return;
358
- }
359
- documentRef.defaultView?.history?.pushState?.({}, "", target.href);
360
- }
361
-
362
- async function fetchRoute(url, { prefetch = false, signal } = {}) {
363
- if (typeof fetchImpl !== "function") {
364
- throw new Error("Router navigation requires a partial registry or fetch.");
365
- }
366
- const response = await fetchImpl(`${routeEndpoint}?to=${encodeURIComponent(String(url))}`, {
367
- headers: {
368
- accept: "application/json, text/html"
369
- },
370
- signal
371
- });
372
- if (!response.ok) {
373
- throw new Error(`Route "${url}" failed with ${response.status}.`);
374
- }
375
- if (prefetch) {
376
- return response;
377
- }
378
- const type = response.headers.get("content-type") ?? "";
379
- if (type.includes("application/json")) {
380
- return response.json();
381
- }
382
- return { boundary, html: await response.text() };
383
- }
384
-
385
- function contextFor(matched, navigation) {
386
- return {
387
- params: matched.params,
388
- route: matched.route,
389
- router: api,
390
- signals: signalRegistry,
391
- handlers: handlerRegistry,
392
- loader: loaderInstance,
393
- server,
394
- cache,
395
- scheduler: schedulerInstance,
396
- abort: navigation?.abort
397
- };
398
- }
399
-
400
- function beginNavigation(target, matched) {
401
- activeNavigation?.controller.abort(new Error(`Router navigation superseded by ${target.pathname}${target.search}.`));
402
- const controller = new AbortController();
403
- const navigation = {
404
- id: ++navigationVersion,
405
- controller,
406
- abort: controller.signal,
407
- target,
408
- matched
409
- };
410
- activeNavigation = navigation;
411
- return navigation;
412
- }
413
-
414
- function isActiveNavigation(navigation) {
415
- return !destroyed && navigation && activeNavigation?.id === navigation.id && !navigation.abort.aborted;
416
- }
417
-
418
- function updateStateFromLocation() {
419
- const url = currentUrl();
420
- const matched = api.match(url);
421
- setMatchedRouterState(url, matched, { pending: false, error: null });
422
- }
423
-
424
- function setMatchedRouterState(url, matched, patch = {}) {
425
- signalRegistry.ensure("router", {});
426
- setRouterState({
427
- url: url.href,
428
- path: url.pathname,
429
- query: queryObject(url),
430
- params: matched?.params ?? {},
431
- route: matched?.route ?? null,
432
- ...patch
433
- });
434
- }
435
-
436
- function setNoRouteError(url) {
437
- const error = new Error(`No route matched ${url.pathname}${url.search}`);
438
- setMatchedRouterState(url, null, {
439
- pending: false,
440
- error
441
- });
442
- }
443
-
444
- function setRouterState(patch) {
445
- signalRegistry.ensure("router", {});
446
- for (const [key, value] of Object.entries(patch)) {
447
- signalRegistry.set(`router.${key}`, value);
448
- }
449
- }
450
-
451
- function handleNavigation(promise) {
452
- void promise.catch((error) => {
453
- if (destroyed) {
454
- return;
455
- }
456
- setRouterState({
457
- pending: false,
458
- error
459
- });
460
- dispatchAsyncError(rootNode, error);
461
- });
462
- }
463
-
464
- function currentUrl() {
465
- return resolveUrl(documentRef.defaultView?.location?.href ?? "http://localhost/");
466
- }
467
-
468
- function resolveUrl(url) {
469
- if (url instanceof URL) {
470
- return url;
471
- }
472
- return new URL(String(url), documentRef.defaultView?.location?.href ?? "http://localhost/");
473
- }
474
-
475
- function assertActive() {
476
- if (destroyed) {
477
- throw new Error("Router has been destroyed.");
478
- }
479
- }
480
-
481
- function shouldIgnoreLink(event, anchor) {
482
- if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
483
- return true;
484
- }
485
- if (anchor.target || anchor.hasAttribute("download")) {
486
- return true;
487
- }
488
- const target = resolveUrl(anchor.href);
489
- const current = currentUrl();
490
- if (target.origin !== current.origin) {
491
- return true;
492
- }
493
- return isHashOnlyNavigation(target, current, anchor);
494
- }
495
-
496
- function shouldIgnoreForm(form) {
497
- const method = String(form.method || "get").toLowerCase();
498
- return method !== "get" || resolveUrl(form.action).origin !== currentUrl().origin;
499
- }
500
-
501
- function formActionUrl(form) {
502
- const url = resolveUrl(form.action || form.ownerDocument.defaultView.location.href);
503
- const formData = new form.ownerDocument.defaultView.FormData(form);
504
- url.search = new URLSearchParams(formData).toString();
505
- return url.href;
506
- }
507
- }
508
-
509
- function normalizeRoute(pattern, definition) {
510
- const normalized = typeof definition === "string" ? defineRoute(definition) : definition;
511
- const { regex, keys } = compilePattern(pattern);
512
- return {
513
- pattern,
514
- regex,
515
- keys,
516
- score: routeScore(pattern),
517
- definition: normalized
518
- };
519
- }
520
-
521
- function compilePattern(pattern) {
522
- const keys = [];
523
- if (pattern === "*") {
524
- return { regex: /^.*$/, keys };
525
- }
526
- if (pattern === "/") {
527
- return { regex: /^\/$/, keys };
528
- }
529
-
530
- const source = pattern
531
- .split("/")
532
- .map((segment) => {
533
- if (segment.startsWith(":")) {
534
- keys.push(segment.slice(1));
535
- return "([^/]+)";
536
- }
537
- return escapeRegExp(segment);
538
- })
539
- .join("/");
540
-
541
- return { regex: new RegExp(`^${source}$`), keys };
542
- }
543
-
544
- function closest(target, selector) {
545
- return target?.closest?.(selector);
546
- }
547
-
548
- function toUrl(url) {
549
- if (url instanceof URL) {
550
- return url;
551
- }
552
- return new URL(String(url), globalThis.location?.href ?? "http://localhost/");
553
- }
554
-
555
- function queryObject(url) {
556
- return Object.fromEntries(url.searchParams.entries());
557
- }
558
-
559
- function safeDecodeURIComponent(value) {
560
- try {
561
- return decodeURIComponent(value);
562
- } catch {
563
- return value;
564
- }
565
- }
566
-
567
- function isHashOnlyNavigation(target, current, anchor) {
568
- if (target.origin !== current.origin || target.pathname !== current.pathname || target.search !== current.search) {
569
- return false;
570
- }
571
- return target.hash !== current.hash || anchor.getAttribute?.("href")?.startsWith("#") === true;
572
- }
573
-
574
- function dispatchAsyncError(element, error) {
575
- const EventCtor = element.ownerDocument?.defaultView?.CustomEvent ?? globalThis.CustomEvent;
576
- if (typeof EventCtor !== "function") {
577
- return;
578
- }
579
- element.dispatchEvent?.(
580
- new EventCtor("async:error", {
581
- bubbles: true,
582
- detail: { error }
583
- })
584
- );
585
- }
586
-
587
- function escapeRegExp(value) {
588
- return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
589
- }
590
-
591
- function sortRoutes(routes) {
592
- routes.sort((left, right) => right.score - left.score || right.pattern.length - left.pattern.length);
593
- }
594
-
595
- function routeScore(pattern) {
596
- if (pattern === "*") {
597
- return -1;
598
- }
599
- return pattern
600
- .split("/")
601
- .filter(Boolean)
602
- .reduce((score, segment) => {
603
- if (segment === "*") {
604
- return score;
605
- }
606
- if (segment.startsWith(":")) {
607
- return score + 2;
608
- }
609
- return score + 4;
610
- }, pattern === "/" ? 3 : 0);
611
- }
612
-
613
- function assertPattern(pattern) {
614
- if (typeof pattern !== "string" || (pattern !== "*" && !pattern.startsWith("/"))) {
615
- throw new TypeError("Route pattern must be a path string or \"*\".");
616
- }
617
- }