@async/framework 0.10.1 → 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 +36 -0
  2. package/README.md +23 -7
  3. package/browser.d.ts +4 -7
  4. package/browser.js +143 -116
  5. package/browser.min.js +1 -1
  6. package/browser.ts +143 -116
  7. package/browser.umd.js +143 -116
  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 -189
  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 -204
  44. package/src/loader.js +0 -765
  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 -571
  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 -357
  53. package/src/signals.js +0 -592
package/browser.umd.js CHANGED
@@ -329,10 +329,20 @@
329
329
  const resolved = resolveDescriptorUrl(type, id, descriptor, registryAssets);
330
330
  let modulePromise = moduleCache.get(resolved.moduleUrl);
331
331
  if (!modulePromise) {
332
- modulePromise = Promise.resolve(importModule(resolved.moduleUrl));
332
+ modulePromise = Promise.resolve().then(() => importModule(resolved.moduleUrl));
333
333
  moduleCache.set(resolved.moduleUrl, modulePromise);
334
334
  }
335
- const module = await modulePromise;
335
+ let module;
336
+ try {
337
+ module = await modulePromise;
338
+ } catch (cause) {
339
+ if (moduleCache.get(resolved.moduleUrl) === modulePromise) {
340
+ moduleCache.delete(resolved.moduleUrl);
341
+ }
342
+ throw new Error(`Lazy ${type} "${id}" failed to import ${resolved.moduleUrl}: ${errorMessage(cause)}`, {
343
+ cause
344
+ });
345
+ }
336
346
  const value = resolveExport(module, resolved.exportNames, type, id);
337
347
  exportCache.set(cacheKey, value);
338
348
  return value;
@@ -492,6 +502,10 @@
492
502
  return /^[A-Za-z][A-Za-z\d+.-]*:/.test(value);
493
503
  }
494
504
 
505
+ function errorMessage(error) {
506
+ return error instanceof Error ? error.message : String(error);
507
+ }
508
+
495
509
  function stableStringify(value) {
496
510
  if (!value || typeof value !== "object" || Array.isArray(value)) {
497
511
  return JSON.stringify(value);
@@ -820,15 +834,7 @@
820
834
 
821
835
  get(key) {
822
836
  assertKey(key);
823
- const entry = entries.get(key);
824
- if (!entry) {
825
- return undefined;
826
- }
827
- if (entry.expiresAt !== undefined && entry.expiresAt <= now()) {
828
- entries.delete(key);
829
- return undefined;
830
- }
831
- return entry.value;
837
+ return readEntry(key).value;
832
838
  },
833
839
 
834
840
  set(key, value, options = {}) {
@@ -846,9 +852,9 @@
846
852
  if (typeof fn !== "function") {
847
853
  throw new TypeError("cache.getOrSet(key, fn) requires a function.");
848
854
  }
849
- const cached = registryApi.get(key);
850
- if (cached !== undefined) {
851
- return cached;
855
+ const cached = readEntry(key);
856
+ if (cached.found) {
857
+ return cached.value;
852
858
  }
853
859
  if (pending.has(key)) {
854
860
  return pending.get(key);
@@ -899,8 +905,8 @@
899
905
  snapshot() {
900
906
  const snapshot = {};
901
907
  for (const [key] of entries) {
902
- const value = registryApi.get(key);
903
- if (value !== undefined) {
908
+ const { found, value } = readEntry(key);
909
+ if (found && value !== undefined) {
904
910
  snapshot[key] = value;
905
911
  }
906
912
  }
@@ -940,6 +946,18 @@
940
946
  const prefix = key.split(":")[0];
941
947
  return definitions.get(prefix);
942
948
  }
949
+
950
+ function readEntry(key) {
951
+ const entry = entries.get(key);
952
+ if (!entry) {
953
+ return { found: false, value: undefined };
954
+ }
955
+ if (entry.expiresAt !== undefined && entry.expiresAt <= now()) {
956
+ entries.delete(key);
957
+ return { found: false, value: undefined };
958
+ }
959
+ return { found: true, value: entry.value };
960
+ }
943
961
  }
944
962
 
945
963
  function normalizeDefinition(definition) {
@@ -2158,7 +2176,7 @@
2158
2176
 
2159
2177
  function createServerProxy({
2160
2178
  endpoint = "/__async/server",
2161
- fetch: fetchImpl = globalThis.fetch?.bind(globalThis),
2179
+ transport,
2162
2180
  signals,
2163
2181
  loader,
2164
2182
  router,
@@ -2166,8 +2184,8 @@
2166
2184
  scheduler,
2167
2185
  headers = {}
2168
2186
  } = {}) {
2169
- if (typeof fetchImpl !== "function") {
2170
- throw new TypeError("createServerProxy(...) requires fetch to be available.");
2187
+ if (typeof transport !== "function") {
2188
+ throw new TypeError("createServerProxy(...) requires a transport function.");
2171
2189
  }
2172
2190
 
2173
2191
  const defaults = { signals, loader, router, cache, scheduler };
@@ -2182,7 +2200,7 @@
2182
2200
  };
2183
2201
  assertJsonTransportable(body);
2184
2202
 
2185
- const response = await fetchImpl(joinEndpoint(endpoint, id), {
2203
+ const response = await transport(joinEndpoint(endpoint, id), {
2186
2204
  method: "POST",
2187
2205
  headers: {
2188
2206
  "content-type": "application/json",
@@ -2459,14 +2477,17 @@
2459
2477
  return output;
2460
2478
  }
2461
2479
 
2462
- function assertJsonTransportable(value, seen = new Set()) {
2480
+ function assertJsonTransportable(value, stack = new Set()) {
2481
+ if (typeof value === "bigint") {
2482
+ throw new Error("Server proxy JSON transport does not support BigInt values.");
2483
+ }
2463
2484
  if (value == null || typeof value !== "object") {
2464
2485
  return;
2465
2486
  }
2466
- if (seen.has(value)) {
2467
- return;
2487
+ if (stack.has(value)) {
2488
+ throw new Error("Server proxy JSON transport does not support circular values.");
2468
2489
  }
2469
- seen.add(value);
2490
+ stack.add(value);
2470
2491
 
2471
2492
  const tag = Object.prototype.toString.call(value);
2472
2493
  if (tag === "[object File]" || tag === "[object Blob]" || tag === "[object FormData]") {
@@ -2474,13 +2495,15 @@
2474
2495
  }
2475
2496
  if (Array.isArray(value)) {
2476
2497
  for (const item of value) {
2477
- assertJsonTransportable(item, seen);
2498
+ assertJsonTransportable(item, stack);
2478
2499
  }
2500
+ stack.delete(value);
2479
2501
  return;
2480
2502
  }
2481
2503
  for (const item of Object.values(value)) {
2482
- assertJsonTransportable(item, seen);
2504
+ assertJsonTransportable(item, stack);
2483
2505
  }
2506
+ stack.delete(value);
2484
2507
  }
2485
2508
 
2486
2509
  function joinEndpoint(endpoint, id) {
@@ -3125,6 +3148,7 @@
3125
3148
  return;
3126
3149
  }
3127
3150
  destroyed = true;
3151
+ markDestroyedScopes(rootNode);
3128
3152
  for (const cleanup of [...cleanups]) {
3129
3153
  runCleanup(cleanup);
3130
3154
  }
@@ -3575,6 +3599,12 @@
3575
3599
  });
3576
3600
  }
3577
3601
 
3602
+ function markDestroyedScopes(scope) {
3603
+ for (const element of elementsIn(scope)) {
3604
+ schedulerInstance.markScopeDestroyed(element);
3605
+ }
3606
+ }
3607
+
3578
3608
  return api;
3579
3609
  }
3580
3610
 
@@ -3954,6 +3984,8 @@
3954
3984
 
3955
3985
  const route = defineRoute;
3956
3986
 
3987
+ const routerModes = new Set(["csr", "spa", "ssr", "mpa"]);
3988
+
3957
3989
  function createRouteRegistry(initialMap = {}, options = {}) {
3958
3990
  const registryStore = options.registry ?? createRegistryStore();
3959
3991
  const type = options.type ?? "route";
@@ -4000,7 +4032,7 @@
4000
4032
  }
4001
4033
  const params = {};
4002
4034
  candidate.keys.forEach((key, index) => {
4003
- params[key] = decodeURIComponent(match[index + 1] ?? "");
4035
+ params[key] = safeDecodeURIComponent(match[index + 1] ?? "");
4004
4036
  });
4005
4037
  return {
4006
4038
  pattern: candidate.pattern,
@@ -4049,7 +4081,7 @@
4049
4081
  }
4050
4082
 
4051
4083
  function createRouter({
4052
- mode = "ssr-spa",
4084
+ mode = "ssr",
4053
4085
  root,
4054
4086
  boundary = "route",
4055
4087
  routes = createRouteRegistry(),
@@ -4059,11 +4091,10 @@
4059
4091
  server,
4060
4092
  cache,
4061
4093
  partials,
4062
- fetch: fetchImpl = globalThis.fetch?.bind(globalThis),
4063
- routeEndpoint = "/__async/route",
4064
4094
  attributes,
4065
4095
  scheduler
4066
4096
  } = {}) {
4097
+ assertRouterMode(mode);
4067
4098
  const documentRef = root?.ownerDocument ?? root ?? globalThis.document;
4068
4099
  const rootNode = root ?? documentRef;
4069
4100
  const signalRegistry = signals ?? loader?.signals ?? createSignalRegistry();
@@ -4115,11 +4146,11 @@
4115
4146
  }
4116
4147
  bindNavigation();
4117
4148
  if (mode === "csr") {
4118
- void api.navigate(currentUrl(), {
4149
+ handleNavigation(api.navigate(currentUrl(), {
4119
4150
  replace: true,
4120
4151
  initial: true,
4121
4152
  source: "client"
4122
- }).catch(() => {});
4153
+ }));
4123
4154
  return api;
4124
4155
  }
4125
4156
  updateStateFromLocation();
@@ -4132,16 +4163,13 @@
4132
4163
 
4133
4164
  prefetch(url) {
4134
4165
  assertActive();
4135
- if (mode === "ssr-spa" && typeof fetchImpl === "function") {
4136
- return fetchRoute(url, { prefetch: true });
4166
+ if (mode === "mpa" || mode === "ssr") {
4167
+ return Promise.resolve(null);
4137
4168
  }
4138
4169
  const matched = api.match(url);
4139
4170
  if (matched?.route?.partial && partials?.resolve?.(matched.route.partial)) {
4140
4171
  return partials.render(matched.route.partial, matched.params, contextFor(matched));
4141
4172
  }
4142
- if (typeof fetchImpl === "function") {
4143
- return fetchRoute(url, { prefetch: true });
4144
- }
4145
4173
  return Promise.resolve(null);
4146
4174
  },
4147
4175
 
@@ -4153,9 +4181,6 @@
4153
4181
  }
4154
4182
 
4155
4183
  const target = resolveUrl(url);
4156
- if (mode === "ssr-spa") {
4157
- return fetchRoutePartial(target, options);
4158
- }
4159
4184
  return renderLocalRoutePartial(target, options);
4160
4185
  },
4161
4186
 
@@ -4184,7 +4209,7 @@
4184
4209
  return;
4185
4210
  }
4186
4211
  event.preventDefault();
4187
- api.navigate(anchor.href);
4212
+ handleNavigation(api.navigate(anchor.href));
4188
4213
  };
4189
4214
  const submit = (event) => {
4190
4215
  const form = closest(event.target, "form");
@@ -4192,9 +4217,9 @@
4192
4217
  return;
4193
4218
  }
4194
4219
  event.preventDefault();
4195
- api.navigate(formActionUrl(form));
4220
+ handleNavigation(api.navigate(formActionUrl(form)));
4196
4221
  };
4197
- const popstate = () => api.navigate(currentUrl(), { history: false });
4222
+ const popstate = () => handleNavigation(api.navigate(currentUrl(), { history: false }));
4198
4223
 
4199
4224
  rootNode.addEventListener?.("click", click);
4200
4225
  rootNode.addEventListener?.("submit", submit);
@@ -4243,31 +4268,6 @@
4243
4268
  }
4244
4269
  }
4245
4270
 
4246
- async function fetchRoutePartial(target, options = {}) {
4247
- const matched = api.match(target);
4248
- const navigation = beginNavigation(target, matched);
4249
- setMatchedRouterState(target, matched, { pending: true, error: null });
4250
-
4251
- try {
4252
- const result = await fetchRoute(target.href, { signal: navigation.abort });
4253
- if (!isActiveNavigation(navigation)) {
4254
- return null;
4255
- }
4256
- await applyNavigationResult(result, target, options, navigation);
4257
- if (!isActiveNavigation(navigation)) {
4258
- return null;
4259
- }
4260
- setRouterState({ pending: false, error: null });
4261
- return result;
4262
- } catch (error) {
4263
- if (!isActiveNavigation(navigation)) {
4264
- return null;
4265
- }
4266
- setRouterState({ pending: false, error });
4267
- throw error;
4268
- }
4269
- }
4270
-
4271
4271
  async function applyNavigationResult(result, target, options, navigation) {
4272
4272
  if (!isActiveNavigation(navigation)) {
4273
4273
  return;
@@ -4298,29 +4298,6 @@
4298
4298
  documentRef.defaultView?.history?.pushState?.({}, "", target.href);
4299
4299
  }
4300
4300
 
4301
- async function fetchRoute(url, { prefetch = false, signal } = {}) {
4302
- if (typeof fetchImpl !== "function") {
4303
- throw new Error("Router navigation requires a partial registry or fetch.");
4304
- }
4305
- const response = await fetchImpl(`${routeEndpoint}?to=${encodeURIComponent(String(url))}`, {
4306
- headers: {
4307
- accept: "application/json, text/html"
4308
- },
4309
- signal
4310
- });
4311
- if (!response.ok) {
4312
- throw new Error(`Route "${url}" failed with ${response.status}.`);
4313
- }
4314
- if (prefetch) {
4315
- return response;
4316
- }
4317
- const type = response.headers.get("content-type") ?? "";
4318
- if (type.includes("application/json")) {
4319
- return response.json();
4320
- }
4321
- return { boundary, html: await response.text() };
4322
- }
4323
-
4324
4301
  function contextFor(matched, navigation) {
4325
4302
  return {
4326
4303
  params: matched.params,
@@ -4387,6 +4364,19 @@
4387
4364
  }
4388
4365
  }
4389
4366
 
4367
+ function handleNavigation(promise) {
4368
+ void promise.catch((error) => {
4369
+ if (destroyed) {
4370
+ return;
4371
+ }
4372
+ setRouterState({
4373
+ pending: false,
4374
+ error
4375
+ });
4376
+ dispatchAsyncError(rootNode, error);
4377
+ });
4378
+ }
4379
+
4390
4380
  function currentUrl() {
4391
4381
  return resolveUrl(documentRef.defaultView?.location?.href ?? "http://localhost/");
4392
4382
  }
@@ -4403,6 +4393,33 @@
4403
4393
  throw new Error("Router has been destroyed.");
4404
4394
  }
4405
4395
  }
4396
+
4397
+ function shouldIgnoreLink(event, anchor) {
4398
+ if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
4399
+ return true;
4400
+ }
4401
+ if (anchor.target || anchor.hasAttribute("download")) {
4402
+ return true;
4403
+ }
4404
+ const target = resolveUrl(anchor.href);
4405
+ const current = currentUrl();
4406
+ if (target.origin !== current.origin) {
4407
+ return true;
4408
+ }
4409
+ return isHashOnlyNavigation(target, current, anchor);
4410
+ }
4411
+
4412
+ function shouldIgnoreForm(form) {
4413
+ const method = String(form.method || "get").toLowerCase();
4414
+ return method !== "get" || resolveUrl(form.action).origin !== currentUrl().origin;
4415
+ }
4416
+
4417
+ function formActionUrl(form) {
4418
+ const url = resolveUrl(form.action || form.ownerDocument.defaultView.location.href);
4419
+ const formData = new form.ownerDocument.defaultView.FormData(form);
4420
+ url.search = new URLSearchParams(formData).toString();
4421
+ return url.href;
4422
+ }
4406
4423
  }
4407
4424
 
4408
4425
  function normalizeRoute(pattern, definition) {
@@ -4440,28 +4457,6 @@
4440
4457
  return { regex: new RegExp(`^${source}$`), keys };
4441
4458
  }
4442
4459
 
4443
- function shouldIgnoreLink(event, anchor) {
4444
- if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
4445
- return true;
4446
- }
4447
- if (anchor.target || anchor.hasAttribute("download")) {
4448
- return true;
4449
- }
4450
- return toUrl(anchor.href).origin !== toUrl(anchor.ownerDocument.defaultView.location.href).origin;
4451
- }
4452
-
4453
- function shouldIgnoreForm(form) {
4454
- const method = String(form.method || "get").toLowerCase();
4455
- return method !== "get" || toUrl(form.action).origin !== toUrl(form.ownerDocument.defaultView.location.href).origin;
4456
- }
4457
-
4458
- function formActionUrl(form) {
4459
- const url = toUrl(form.action || form.ownerDocument.defaultView.location.href);
4460
- const formData = new form.ownerDocument.defaultView.FormData(form);
4461
- url.search = new URLSearchParams(formData).toString();
4462
- return url.href;
4463
- }
4464
-
4465
4460
  function closest(target, selector) {
4466
4461
  return target?.closest?.(selector);
4467
4462
  }
@@ -4477,6 +4472,40 @@
4477
4472
  return Object.fromEntries(url.searchParams.entries());
4478
4473
  }
4479
4474
 
4475
+ function safeDecodeURIComponent(value) {
4476
+ try {
4477
+ return decodeURIComponent(value);
4478
+ } catch {
4479
+ return value;
4480
+ }
4481
+ }
4482
+
4483
+ function isHashOnlyNavigation(target, current, anchor) {
4484
+ if (target.origin !== current.origin || target.pathname !== current.pathname || target.search !== current.search) {
4485
+ return false;
4486
+ }
4487
+ return target.hash !== current.hash || anchor.getAttribute?.("href")?.startsWith("#") === true;
4488
+ }
4489
+
4490
+ function assertRouterMode(mode) {
4491
+ if (!routerModes.has(mode)) {
4492
+ throw new TypeError(`Unknown router mode "${mode}".`);
4493
+ }
4494
+ }
4495
+
4496
+ function dispatchAsyncError(element, error) {
4497
+ const EventCtor = element.ownerDocument?.defaultView?.CustomEvent ?? globalThis.CustomEvent;
4498
+ if (typeof EventCtor !== "function") {
4499
+ return;
4500
+ }
4501
+ element.dispatchEvent?.(
4502
+ new EventCtor("async:error", {
4503
+ bubbles: true,
4504
+ detail: { error }
4505
+ })
4506
+ );
4507
+ }
4508
+
4480
4509
  function escapeRegExp(value) {
4481
4510
  return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4482
4511
  }
@@ -4862,7 +4891,7 @@
4862
4891
  return;
4863
4892
  }
4864
4893
  router = router ?? createRouter({
4865
- mode: options.mode ?? "ssr-spa",
4894
+ mode: options.mode ?? "ssr",
4866
4895
  root,
4867
4896
  boundary: options.boundary ?? "route",
4868
4897
  routes,
@@ -4873,8 +4902,6 @@
4873
4902
  cache: browserCache,
4874
4903
  partials,
4875
4904
  scheduler,
4876
- fetch: options.fetch,
4877
- routeEndpoint: options.routeEndpoint,
4878
4905
  attributes
4879
4906
  });
4880
4907
  runtime.router = router;