@async/framework 0.10.0 → 0.10.2

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/cache.js CHANGED
@@ -47,15 +47,7 @@ export function createCacheRegistry(initialMap = {}, { now = () => Date.now(), r
47
47
 
48
48
  get(key) {
49
49
  assertKey(key);
50
- const entry = entries.get(key);
51
- if (!entry) {
52
- return undefined;
53
- }
54
- if (entry.expiresAt !== undefined && entry.expiresAt <= now()) {
55
- entries.delete(key);
56
- return undefined;
57
- }
58
- return entry.value;
50
+ return readEntry(key).value;
59
51
  },
60
52
 
61
53
  set(key, value, options = {}) {
@@ -73,9 +65,9 @@ export function createCacheRegistry(initialMap = {}, { now = () => Date.now(), r
73
65
  if (typeof fn !== "function") {
74
66
  throw new TypeError("cache.getOrSet(key, fn) requires a function.");
75
67
  }
76
- const cached = registryApi.get(key);
77
- if (cached !== undefined) {
78
- return cached;
68
+ const cached = readEntry(key);
69
+ if (cached.found) {
70
+ return cached.value;
79
71
  }
80
72
  if (pending.has(key)) {
81
73
  return pending.get(key);
@@ -126,8 +118,8 @@ export function createCacheRegistry(initialMap = {}, { now = () => Date.now(), r
126
118
  snapshot() {
127
119
  const snapshot = {};
128
120
  for (const [key] of entries) {
129
- const value = registryApi.get(key);
130
- if (value !== undefined) {
121
+ const { found, value } = readEntry(key);
122
+ if (found && value !== undefined) {
131
123
  snapshot[key] = value;
132
124
  }
133
125
  }
@@ -167,6 +159,18 @@ export function createCacheRegistry(initialMap = {}, { now = () => Date.now(), r
167
159
  const prefix = key.split(":")[0];
168
160
  return definitions.get(prefix);
169
161
  }
162
+
163
+ function readEntry(key) {
164
+ const entry = entries.get(key);
165
+ if (!entry) {
166
+ return { found: false, value: undefined };
167
+ }
168
+ if (entry.expiresAt !== undefined && entry.expiresAt <= now()) {
169
+ entries.delete(key);
170
+ return { found: false, value: undefined };
171
+ }
172
+ return { found: true, value: entry.value };
173
+ }
170
174
  }
171
175
 
172
176
  function normalizeDefinition(definition) {
@@ -33,10 +33,20 @@ export function createLazyRegistry(options = {}) {
33
33
  const resolved = resolveDescriptorUrl(type, id, descriptor, registryAssets);
34
34
  let modulePromise = moduleCache.get(resolved.moduleUrl);
35
35
  if (!modulePromise) {
36
- modulePromise = Promise.resolve(importModule(resolved.moduleUrl));
36
+ modulePromise = Promise.resolve().then(() => importModule(resolved.moduleUrl));
37
37
  moduleCache.set(resolved.moduleUrl, modulePromise);
38
38
  }
39
- const module = await modulePromise;
39
+ let module;
40
+ try {
41
+ module = await modulePromise;
42
+ } catch (cause) {
43
+ if (moduleCache.get(resolved.moduleUrl) === modulePromise) {
44
+ moduleCache.delete(resolved.moduleUrl);
45
+ }
46
+ throw new Error(`Lazy ${type} "${id}" failed to import ${resolved.moduleUrl}: ${errorMessage(cause)}`, {
47
+ cause
48
+ });
49
+ }
40
50
  const value = resolveExport(module, resolved.exportNames, type, id);
41
51
  exportCache.set(cacheKey, value);
42
52
  return value;
@@ -196,6 +206,10 @@ function isAbsoluteUrl(value) {
196
206
  return /^[A-Za-z][A-Za-z\d+.-]*:/.test(value);
197
207
  }
198
208
 
209
+ function errorMessage(error) {
210
+ return error instanceof Error ? error.message : String(error);
211
+ }
212
+
199
213
  function stableStringify(value) {
200
214
  if (!value || typeof value !== "object" || Array.isArray(value)) {
201
215
  return JSON.stringify(value);
package/src/loader.js CHANGED
@@ -90,6 +90,7 @@ export function Loader({ root, signals, handlers, server, router, cache, attribu
90
90
  return;
91
91
  }
92
92
  destroyed = true;
93
+ markDestroyedScopes(rootNode);
93
94
  for (const cleanup of [...cleanups]) {
94
95
  runCleanup(cleanup);
95
96
  }
@@ -540,6 +541,12 @@ export function Loader({ root, signals, handlers, server, router, cache, attribu
540
541
  });
541
542
  }
542
543
 
544
+ function markDestroyedScopes(scope) {
545
+ for (const element of elementsIn(scope)) {
546
+ schedulerInstance.markScopeDestroyed(element);
547
+ }
548
+ }
549
+
543
550
  return api;
544
551
  }
545
552
 
package/src/router.js CHANGED
@@ -61,7 +61,7 @@ export function createRouteRegistry(initialMap = {}, options = {}) {
61
61
  }
62
62
  const params = {};
63
63
  candidate.keys.forEach((key, index) => {
64
- params[key] = decodeURIComponent(match[index + 1] ?? "");
64
+ params[key] = safeDecodeURIComponent(match[index + 1] ?? "");
65
65
  });
66
66
  return {
67
67
  pattern: candidate.pattern,
@@ -176,11 +176,11 @@ export function createRouter({
176
176
  }
177
177
  bindNavigation();
178
178
  if (mode === "csr") {
179
- void api.navigate(currentUrl(), {
179
+ handleNavigation(api.navigate(currentUrl(), {
180
180
  replace: true,
181
181
  initial: true,
182
182
  source: "client"
183
- }).catch(() => {});
183
+ }));
184
184
  return api;
185
185
  }
186
186
  updateStateFromLocation();
@@ -245,7 +245,7 @@ export function createRouter({
245
245
  return;
246
246
  }
247
247
  event.preventDefault();
248
- api.navigate(anchor.href);
248
+ handleNavigation(api.navigate(anchor.href));
249
249
  };
250
250
  const submit = (event) => {
251
251
  const form = closest(event.target, "form");
@@ -253,9 +253,9 @@ export function createRouter({
253
253
  return;
254
254
  }
255
255
  event.preventDefault();
256
- api.navigate(formActionUrl(form));
256
+ handleNavigation(api.navigate(formActionUrl(form)));
257
257
  };
258
- const popstate = () => api.navigate(currentUrl(), { history: false });
258
+ const popstate = () => handleNavigation(api.navigate(currentUrl(), { history: false }));
259
259
 
260
260
  rootNode.addEventListener?.("click", click);
261
261
  rootNode.addEventListener?.("submit", submit);
@@ -448,6 +448,19 @@ export function createRouter({
448
448
  }
449
449
  }
450
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
+
451
464
  function currentUrl() {
452
465
  return resolveUrl(documentRef.defaultView?.location?.href ?? "http://localhost/");
453
466
  }
@@ -464,6 +477,33 @@ export function createRouter({
464
477
  throw new Error("Router has been destroyed.");
465
478
  }
466
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
+ }
467
507
  }
468
508
 
469
509
  function normalizeRoute(pattern, definition) {
@@ -501,28 +541,6 @@ function compilePattern(pattern) {
501
541
  return { regex: new RegExp(`^${source}$`), keys };
502
542
  }
503
543
 
504
- function shouldIgnoreLink(event, anchor) {
505
- if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
506
- return true;
507
- }
508
- if (anchor.target || anchor.hasAttribute("download")) {
509
- return true;
510
- }
511
- return toUrl(anchor.href).origin !== toUrl(anchor.ownerDocument.defaultView.location.href).origin;
512
- }
513
-
514
- function shouldIgnoreForm(form) {
515
- const method = String(form.method || "get").toLowerCase();
516
- return method !== "get" || toUrl(form.action).origin !== toUrl(form.ownerDocument.defaultView.location.href).origin;
517
- }
518
-
519
- function formActionUrl(form) {
520
- const url = toUrl(form.action || form.ownerDocument.defaultView.location.href);
521
- const formData = new form.ownerDocument.defaultView.FormData(form);
522
- url.search = new URLSearchParams(formData).toString();
523
- return url.href;
524
- }
525
-
526
544
  function closest(target, selector) {
527
545
  return target?.closest?.(selector);
528
546
  }
@@ -538,6 +556,34 @@ function queryObject(url) {
538
556
  return Object.fromEntries(url.searchParams.entries());
539
557
  }
540
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
+
541
587
  function escapeRegExp(value) {
542
588
  return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
543
589
  }
package/src/server.js CHANGED
@@ -305,14 +305,17 @@ function formDataToObject(formData) {
305
305
  return output;
306
306
  }
307
307
 
308
- function assertJsonTransportable(value, seen = new Set()) {
308
+ function assertJsonTransportable(value, stack = new Set()) {
309
+ if (typeof value === "bigint") {
310
+ throw new Error("Server proxy JSON transport does not support BigInt values.");
311
+ }
309
312
  if (value == null || typeof value !== "object") {
310
313
  return;
311
314
  }
312
- if (seen.has(value)) {
313
- return;
315
+ if (stack.has(value)) {
316
+ throw new Error("Server proxy JSON transport does not support circular values.");
314
317
  }
315
- seen.add(value);
318
+ stack.add(value);
316
319
 
317
320
  const tag = Object.prototype.toString.call(value);
318
321
  if (tag === "[object File]" || tag === "[object Blob]" || tag === "[object FormData]") {
@@ -320,13 +323,15 @@ function assertJsonTransportable(value, seen = new Set()) {
320
323
  }
321
324
  if (Array.isArray(value)) {
322
325
  for (const item of value) {
323
- assertJsonTransportable(item, seen);
326
+ assertJsonTransportable(item, stack);
324
327
  }
328
+ stack.delete(value);
325
329
  return;
326
330
  }
327
331
  for (const item of Object.values(value)) {
328
- assertJsonTransportable(item, seen);
332
+ assertJsonTransportable(item, stack);
329
333
  }
334
+ stack.delete(value);
330
335
  }
331
336
 
332
337
  function joinEndpoint(endpoint, id) {