@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.ts CHANGED
@@ -319,10 +319,20 @@ const __lazyRegistryModule = (() => {
319
319
  const resolved = resolveDescriptorUrl(type, id, descriptor, registryAssets);
320
320
  let modulePromise = moduleCache.get(resolved.moduleUrl);
321
321
  if (!modulePromise) {
322
- modulePromise = Promise.resolve(importModule(resolved.moduleUrl));
322
+ modulePromise = Promise.resolve().then(() => importModule(resolved.moduleUrl));
323
323
  moduleCache.set(resolved.moduleUrl, modulePromise);
324
324
  }
325
- const module = await modulePromise;
325
+ let module;
326
+ try {
327
+ module = await modulePromise;
328
+ } catch (cause) {
329
+ if (moduleCache.get(resolved.moduleUrl) === modulePromise) {
330
+ moduleCache.delete(resolved.moduleUrl);
331
+ }
332
+ throw new Error(`Lazy ${type} "${id}" failed to import ${resolved.moduleUrl}: ${errorMessage(cause)}`, {
333
+ cause
334
+ });
335
+ }
326
336
  const value = resolveExport(module, resolved.exportNames, type, id);
327
337
  exportCache.set(cacheKey, value);
328
338
  return value;
@@ -482,6 +492,10 @@ const __lazyRegistryModule = (() => {
482
492
  return /^[A-Za-z][A-Za-z\d+.-]*:/.test(value);
483
493
  }
484
494
 
495
+ function errorMessage(error) {
496
+ return error instanceof Error ? error.message : String(error);
497
+ }
498
+
485
499
  function stableStringify(value) {
486
500
  if (!value || typeof value !== "object" || Array.isArray(value)) {
487
501
  return JSON.stringify(value);
@@ -810,15 +824,7 @@ const __cacheModule = (() => {
810
824
 
811
825
  get(key) {
812
826
  assertKey(key);
813
- const entry = entries.get(key);
814
- if (!entry) {
815
- return undefined;
816
- }
817
- if (entry.expiresAt !== undefined && entry.expiresAt <= now()) {
818
- entries.delete(key);
819
- return undefined;
820
- }
821
- return entry.value;
827
+ return readEntry(key).value;
822
828
  },
823
829
 
824
830
  set(key, value, options = {}) {
@@ -836,9 +842,9 @@ const __cacheModule = (() => {
836
842
  if (typeof fn !== "function") {
837
843
  throw new TypeError("cache.getOrSet(key, fn) requires a function.");
838
844
  }
839
- const cached = registryApi.get(key);
840
- if (cached !== undefined) {
841
- return cached;
845
+ const cached = readEntry(key);
846
+ if (cached.found) {
847
+ return cached.value;
842
848
  }
843
849
  if (pending.has(key)) {
844
850
  return pending.get(key);
@@ -889,8 +895,8 @@ const __cacheModule = (() => {
889
895
  snapshot() {
890
896
  const snapshot = {};
891
897
  for (const [key] of entries) {
892
- const value = registryApi.get(key);
893
- if (value !== undefined) {
898
+ const { found, value } = readEntry(key);
899
+ if (found && value !== undefined) {
894
900
  snapshot[key] = value;
895
901
  }
896
902
  }
@@ -930,6 +936,18 @@ const __cacheModule = (() => {
930
936
  const prefix = key.split(":")[0];
931
937
  return definitions.get(prefix);
932
938
  }
939
+
940
+ function readEntry(key) {
941
+ const entry = entries.get(key);
942
+ if (!entry) {
943
+ return { found: false, value: undefined };
944
+ }
945
+ if (entry.expiresAt !== undefined && entry.expiresAt <= now()) {
946
+ entries.delete(key);
947
+ return { found: false, value: undefined };
948
+ }
949
+ return { found: true, value: entry.value };
950
+ }
933
951
  }
934
952
 
935
953
  function normalizeDefinition(definition) {
@@ -2148,7 +2166,7 @@ const __serverModule = (() => {
2148
2166
 
2149
2167
  function createServerProxy({
2150
2168
  endpoint = "/__async/server",
2151
- fetch: fetchImpl = globalThis.fetch?.bind(globalThis),
2169
+ transport,
2152
2170
  signals,
2153
2171
  loader,
2154
2172
  router,
@@ -2156,8 +2174,8 @@ const __serverModule = (() => {
2156
2174
  scheduler,
2157
2175
  headers = {}
2158
2176
  } = {}) {
2159
- if (typeof fetchImpl !== "function") {
2160
- throw new TypeError("createServerProxy(...) requires fetch to be available.");
2177
+ if (typeof transport !== "function") {
2178
+ throw new TypeError("createServerProxy(...) requires a transport function.");
2161
2179
  }
2162
2180
 
2163
2181
  const defaults = { signals, loader, router, cache, scheduler };
@@ -2172,7 +2190,7 @@ const __serverModule = (() => {
2172
2190
  };
2173
2191
  assertJsonTransportable(body);
2174
2192
 
2175
- const response = await fetchImpl(joinEndpoint(endpoint, id), {
2193
+ const response = await transport(joinEndpoint(endpoint, id), {
2176
2194
  method: "POST",
2177
2195
  headers: {
2178
2196
  "content-type": "application/json",
@@ -2449,14 +2467,17 @@ const __serverModule = (() => {
2449
2467
  return output;
2450
2468
  }
2451
2469
 
2452
- function assertJsonTransportable(value, seen = new Set()) {
2470
+ function assertJsonTransportable(value, stack = new Set()) {
2471
+ if (typeof value === "bigint") {
2472
+ throw new Error("Server proxy JSON transport does not support BigInt values.");
2473
+ }
2453
2474
  if (value == null || typeof value !== "object") {
2454
2475
  return;
2455
2476
  }
2456
- if (seen.has(value)) {
2457
- return;
2477
+ if (stack.has(value)) {
2478
+ throw new Error("Server proxy JSON transport does not support circular values.");
2458
2479
  }
2459
- seen.add(value);
2480
+ stack.add(value);
2460
2481
 
2461
2482
  const tag = Object.prototype.toString.call(value);
2462
2483
  if (tag === "[object File]" || tag === "[object Blob]" || tag === "[object FormData]") {
@@ -2464,13 +2485,15 @@ const __serverModule = (() => {
2464
2485
  }
2465
2486
  if (Array.isArray(value)) {
2466
2487
  for (const item of value) {
2467
- assertJsonTransportable(item, seen);
2488
+ assertJsonTransportable(item, stack);
2468
2489
  }
2490
+ stack.delete(value);
2469
2491
  return;
2470
2492
  }
2471
2493
  for (const item of Object.values(value)) {
2472
- assertJsonTransportable(item, seen);
2494
+ assertJsonTransportable(item, stack);
2473
2495
  }
2496
+ stack.delete(value);
2474
2497
  }
2475
2498
 
2476
2499
  function joinEndpoint(endpoint, id) {
@@ -3115,6 +3138,7 @@ const __loaderModule = (() => {
3115
3138
  return;
3116
3139
  }
3117
3140
  destroyed = true;
3141
+ markDestroyedScopes(rootNode);
3118
3142
  for (const cleanup of [...cleanups]) {
3119
3143
  runCleanup(cleanup);
3120
3144
  }
@@ -3565,6 +3589,12 @@ const __loaderModule = (() => {
3565
3589
  });
3566
3590
  }
3567
3591
 
3592
+ function markDestroyedScopes(scope) {
3593
+ for (const element of elementsIn(scope)) {
3594
+ schedulerInstance.markScopeDestroyed(element);
3595
+ }
3596
+ }
3597
+
3568
3598
  return api;
3569
3599
  }
3570
3600
 
@@ -3944,6 +3974,8 @@ const __routerModule = (() => {
3944
3974
 
3945
3975
  const route = defineRoute;
3946
3976
 
3977
+ const routerModes = new Set(["csr", "spa", "ssr", "mpa"]);
3978
+
3947
3979
  function createRouteRegistry(initialMap = {}, options = {}) {
3948
3980
  const registryStore = options.registry ?? createRegistryStore();
3949
3981
  const type = options.type ?? "route";
@@ -3990,7 +4022,7 @@ const __routerModule = (() => {
3990
4022
  }
3991
4023
  const params = {};
3992
4024
  candidate.keys.forEach((key, index) => {
3993
- params[key] = decodeURIComponent(match[index + 1] ?? "");
4025
+ params[key] = safeDecodeURIComponent(match[index + 1] ?? "");
3994
4026
  });
3995
4027
  return {
3996
4028
  pattern: candidate.pattern,
@@ -4039,7 +4071,7 @@ const __routerModule = (() => {
4039
4071
  }
4040
4072
 
4041
4073
  function createRouter({
4042
- mode = "ssr-spa",
4074
+ mode = "ssr",
4043
4075
  root,
4044
4076
  boundary = "route",
4045
4077
  routes = createRouteRegistry(),
@@ -4049,11 +4081,10 @@ const __routerModule = (() => {
4049
4081
  server,
4050
4082
  cache,
4051
4083
  partials,
4052
- fetch: fetchImpl = globalThis.fetch?.bind(globalThis),
4053
- routeEndpoint = "/__async/route",
4054
4084
  attributes,
4055
4085
  scheduler
4056
4086
  } = {}) {
4087
+ assertRouterMode(mode);
4057
4088
  const documentRef = root?.ownerDocument ?? root ?? globalThis.document;
4058
4089
  const rootNode = root ?? documentRef;
4059
4090
  const signalRegistry = signals ?? loader?.signals ?? createSignalRegistry();
@@ -4105,11 +4136,11 @@ const __routerModule = (() => {
4105
4136
  }
4106
4137
  bindNavigation();
4107
4138
  if (mode === "csr") {
4108
- void api.navigate(currentUrl(), {
4139
+ handleNavigation(api.navigate(currentUrl(), {
4109
4140
  replace: true,
4110
4141
  initial: true,
4111
4142
  source: "client"
4112
- }).catch(() => {});
4143
+ }));
4113
4144
  return api;
4114
4145
  }
4115
4146
  updateStateFromLocation();
@@ -4122,16 +4153,13 @@ const __routerModule = (() => {
4122
4153
 
4123
4154
  prefetch(url) {
4124
4155
  assertActive();
4125
- if (mode === "ssr-spa" && typeof fetchImpl === "function") {
4126
- return fetchRoute(url, { prefetch: true });
4156
+ if (mode === "mpa" || mode === "ssr") {
4157
+ return Promise.resolve(null);
4127
4158
  }
4128
4159
  const matched = api.match(url);
4129
4160
  if (matched?.route?.partial && partials?.resolve?.(matched.route.partial)) {
4130
4161
  return partials.render(matched.route.partial, matched.params, contextFor(matched));
4131
4162
  }
4132
- if (typeof fetchImpl === "function") {
4133
- return fetchRoute(url, { prefetch: true });
4134
- }
4135
4163
  return Promise.resolve(null);
4136
4164
  },
4137
4165
 
@@ -4143,9 +4171,6 @@ const __routerModule = (() => {
4143
4171
  }
4144
4172
 
4145
4173
  const target = resolveUrl(url);
4146
- if (mode === "ssr-spa") {
4147
- return fetchRoutePartial(target, options);
4148
- }
4149
4174
  return renderLocalRoutePartial(target, options);
4150
4175
  },
4151
4176
 
@@ -4174,7 +4199,7 @@ const __routerModule = (() => {
4174
4199
  return;
4175
4200
  }
4176
4201
  event.preventDefault();
4177
- api.navigate(anchor.href);
4202
+ handleNavigation(api.navigate(anchor.href));
4178
4203
  };
4179
4204
  const submit = (event) => {
4180
4205
  const form = closest(event.target, "form");
@@ -4182,9 +4207,9 @@ const __routerModule = (() => {
4182
4207
  return;
4183
4208
  }
4184
4209
  event.preventDefault();
4185
- api.navigate(formActionUrl(form));
4210
+ handleNavigation(api.navigate(formActionUrl(form)));
4186
4211
  };
4187
- const popstate = () => api.navigate(currentUrl(), { history: false });
4212
+ const popstate = () => handleNavigation(api.navigate(currentUrl(), { history: false }));
4188
4213
 
4189
4214
  rootNode.addEventListener?.("click", click);
4190
4215
  rootNode.addEventListener?.("submit", submit);
@@ -4233,31 +4258,6 @@ const __routerModule = (() => {
4233
4258
  }
4234
4259
  }
4235
4260
 
4236
- async function fetchRoutePartial(target, options = {}) {
4237
- const matched = api.match(target);
4238
- const navigation = beginNavigation(target, matched);
4239
- setMatchedRouterState(target, matched, { pending: true, error: null });
4240
-
4241
- try {
4242
- const result = await fetchRoute(target.href, { signal: navigation.abort });
4243
- if (!isActiveNavigation(navigation)) {
4244
- return null;
4245
- }
4246
- await applyNavigationResult(result, target, options, navigation);
4247
- if (!isActiveNavigation(navigation)) {
4248
- return null;
4249
- }
4250
- setRouterState({ pending: false, error: null });
4251
- return result;
4252
- } catch (error) {
4253
- if (!isActiveNavigation(navigation)) {
4254
- return null;
4255
- }
4256
- setRouterState({ pending: false, error });
4257
- throw error;
4258
- }
4259
- }
4260
-
4261
4261
  async function applyNavigationResult(result, target, options, navigation) {
4262
4262
  if (!isActiveNavigation(navigation)) {
4263
4263
  return;
@@ -4288,29 +4288,6 @@ const __routerModule = (() => {
4288
4288
  documentRef.defaultView?.history?.pushState?.({}, "", target.href);
4289
4289
  }
4290
4290
 
4291
- async function fetchRoute(url, { prefetch = false, signal } = {}) {
4292
- if (typeof fetchImpl !== "function") {
4293
- throw new Error("Router navigation requires a partial registry or fetch.");
4294
- }
4295
- const response = await fetchImpl(`${routeEndpoint}?to=${encodeURIComponent(String(url))}`, {
4296
- headers: {
4297
- accept: "application/json, text/html"
4298
- },
4299
- signal
4300
- });
4301
- if (!response.ok) {
4302
- throw new Error(`Route "${url}" failed with ${response.status}.`);
4303
- }
4304
- if (prefetch) {
4305
- return response;
4306
- }
4307
- const type = response.headers.get("content-type") ?? "";
4308
- if (type.includes("application/json")) {
4309
- return response.json();
4310
- }
4311
- return { boundary, html: await response.text() };
4312
- }
4313
-
4314
4291
  function contextFor(matched, navigation) {
4315
4292
  return {
4316
4293
  params: matched.params,
@@ -4377,6 +4354,19 @@ const __routerModule = (() => {
4377
4354
  }
4378
4355
  }
4379
4356
 
4357
+ function handleNavigation(promise) {
4358
+ void promise.catch((error) => {
4359
+ if (destroyed) {
4360
+ return;
4361
+ }
4362
+ setRouterState({
4363
+ pending: false,
4364
+ error
4365
+ });
4366
+ dispatchAsyncError(rootNode, error);
4367
+ });
4368
+ }
4369
+
4380
4370
  function currentUrl() {
4381
4371
  return resolveUrl(documentRef.defaultView?.location?.href ?? "http://localhost/");
4382
4372
  }
@@ -4393,6 +4383,33 @@ const __routerModule = (() => {
4393
4383
  throw new Error("Router has been destroyed.");
4394
4384
  }
4395
4385
  }
4386
+
4387
+ function shouldIgnoreLink(event, anchor) {
4388
+ if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
4389
+ return true;
4390
+ }
4391
+ if (anchor.target || anchor.hasAttribute("download")) {
4392
+ return true;
4393
+ }
4394
+ const target = resolveUrl(anchor.href);
4395
+ const current = currentUrl();
4396
+ if (target.origin !== current.origin) {
4397
+ return true;
4398
+ }
4399
+ return isHashOnlyNavigation(target, current, anchor);
4400
+ }
4401
+
4402
+ function shouldIgnoreForm(form) {
4403
+ const method = String(form.method || "get").toLowerCase();
4404
+ return method !== "get" || resolveUrl(form.action).origin !== currentUrl().origin;
4405
+ }
4406
+
4407
+ function formActionUrl(form) {
4408
+ const url = resolveUrl(form.action || form.ownerDocument.defaultView.location.href);
4409
+ const formData = new form.ownerDocument.defaultView.FormData(form);
4410
+ url.search = new URLSearchParams(formData).toString();
4411
+ return url.href;
4412
+ }
4396
4413
  }
4397
4414
 
4398
4415
  function normalizeRoute(pattern, definition) {
@@ -4430,28 +4447,6 @@ const __routerModule = (() => {
4430
4447
  return { regex: new RegExp(`^${source}$`), keys };
4431
4448
  }
4432
4449
 
4433
- function shouldIgnoreLink(event, anchor) {
4434
- if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
4435
- return true;
4436
- }
4437
- if (anchor.target || anchor.hasAttribute("download")) {
4438
- return true;
4439
- }
4440
- return toUrl(anchor.href).origin !== toUrl(anchor.ownerDocument.defaultView.location.href).origin;
4441
- }
4442
-
4443
- function shouldIgnoreForm(form) {
4444
- const method = String(form.method || "get").toLowerCase();
4445
- return method !== "get" || toUrl(form.action).origin !== toUrl(form.ownerDocument.defaultView.location.href).origin;
4446
- }
4447
-
4448
- function formActionUrl(form) {
4449
- const url = toUrl(form.action || form.ownerDocument.defaultView.location.href);
4450
- const formData = new form.ownerDocument.defaultView.FormData(form);
4451
- url.search = new URLSearchParams(formData).toString();
4452
- return url.href;
4453
- }
4454
-
4455
4450
  function closest(target, selector) {
4456
4451
  return target?.closest?.(selector);
4457
4452
  }
@@ -4467,6 +4462,40 @@ const __routerModule = (() => {
4467
4462
  return Object.fromEntries(url.searchParams.entries());
4468
4463
  }
4469
4464
 
4465
+ function safeDecodeURIComponent(value) {
4466
+ try {
4467
+ return decodeURIComponent(value);
4468
+ } catch {
4469
+ return value;
4470
+ }
4471
+ }
4472
+
4473
+ function isHashOnlyNavigation(target, current, anchor) {
4474
+ if (target.origin !== current.origin || target.pathname !== current.pathname || target.search !== current.search) {
4475
+ return false;
4476
+ }
4477
+ return target.hash !== current.hash || anchor.getAttribute?.("href")?.startsWith("#") === true;
4478
+ }
4479
+
4480
+ function assertRouterMode(mode) {
4481
+ if (!routerModes.has(mode)) {
4482
+ throw new TypeError(`Unknown router mode "${mode}".`);
4483
+ }
4484
+ }
4485
+
4486
+ function dispatchAsyncError(element, error) {
4487
+ const EventCtor = element.ownerDocument?.defaultView?.CustomEvent ?? globalThis.CustomEvent;
4488
+ if (typeof EventCtor !== "function") {
4489
+ return;
4490
+ }
4491
+ element.dispatchEvent?.(
4492
+ new EventCtor("async:error", {
4493
+ bubbles: true,
4494
+ detail: { error }
4495
+ })
4496
+ );
4497
+ }
4498
+
4470
4499
  function escapeRegExp(value) {
4471
4500
  return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4472
4501
  }
@@ -4852,7 +4881,7 @@ const __appModule = (() => {
4852
4881
  return;
4853
4882
  }
4854
4883
  router = router ?? createRouter({
4855
- mode: options.mode ?? "ssr-spa",
4884
+ mode: options.mode ?? "ssr",
4856
4885
  root,
4857
4886
  boundary: options.boundary ?? "route",
4858
4887
  routes,
@@ -4863,8 +4892,6 @@ const __appModule = (() => {
4863
4892
  cache: browserCache,
4864
4893
  partials,
4865
4894
  scheduler,
4866
- fetch: options.fetch,
4867
- routeEndpoint: options.routeEndpoint,
4868
4895
  attributes
4869
4896
  });
4870
4897
  runtime.router = router;