@async/framework 0.11.0 → 0.11.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/browser.umd.js CHANGED
@@ -196,6 +196,24 @@
196
196
  };
197
197
  },
198
198
 
199
+ _restore(snapshot = {}) {
200
+ if (!isAsyncSignalSnapshot(snapshot)) {
201
+ return state.set(snapshot);
202
+ }
203
+ if (activeAbort && !activeAbort.aborted) {
204
+ activeAbort.cancel(new Error(`Async signal "${registeredId}" restored from snapshot.`));
205
+ }
206
+ value = snapshot.value;
207
+ loading = Boolean(snapshot.loading);
208
+ error = snapshot.error ?? null;
209
+ status = typeof snapshot.status === "string" ? snapshot.status : inferStatus({ value, loading, error });
210
+ if (Number.isFinite(snapshot.version)) {
211
+ version = snapshot.version;
212
+ }
213
+ notify();
214
+ return state;
215
+ },
216
+
199
217
  _bindRegistry(nextRegistry, nextId) {
200
218
  registry = nextRegistry;
201
219
  registeredId = nextId;
@@ -281,6 +299,27 @@
281
299
  return Boolean(value?.[asyncSignalKind]);
282
300
  }
283
301
 
302
+ function isAsyncSignalSnapshot(value) {
303
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
304
+ return false;
305
+ }
306
+ return Object.hasOwn(value, "value")
307
+ && (Object.hasOwn(value, "loading")
308
+ || Object.hasOwn(value, "error")
309
+ || Object.hasOwn(value, "status")
310
+ || Object.hasOwn(value, "version"));
311
+ }
312
+
313
+ function inferStatus({ value, loading, error }) {
314
+ if (loading) {
315
+ return "loading";
316
+ }
317
+ if (error) {
318
+ return "error";
319
+ }
320
+ return value === undefined ? "idle" : "ready";
321
+ }
322
+
284
323
  function attachCancel(signal, controller) {
285
324
  Object.defineProperty(signal, "cancel", {
286
325
  configurable: true,
@@ -1919,12 +1958,16 @@
1919
1958
  });
1920
1959
 
1921
1960
  const output = Component.call(context, props);
1961
+ if (output && typeof output.then === "function") {
1962
+ throw new TypeError(`Component "${componentName(Component)}" returned a Promise. Async components are not supported by synchronous renderComponent(). Use an async partial or handler instead.`);
1963
+ }
1922
1964
  const html = renderScopedTemplate(output);
1923
1965
 
1924
1966
  return {
1925
1967
  html,
1926
1968
  attach(target) {
1927
- for (const hook of attachHooks) {
1969
+ for (let index = 0; index < attachHooks.length; index += 1) {
1970
+ const hook = attachHooks[index];
1928
1971
  runtime.scheduler?.enqueue("lifecycle", () => {
1929
1972
  const cleanup = hook(target);
1930
1973
  if (typeof cleanup === "function") {
@@ -1932,7 +1975,7 @@
1932
1975
  }
1933
1976
  }, {
1934
1977
  scope,
1935
- key: `attach:${attachHooks.indexOf(hook)}`
1978
+ key: `attach:${index}`
1936
1979
  }) ?? runAttachHook(hook, target);
1937
1980
  }
1938
1981
  },
@@ -1940,8 +1983,12 @@
1940
1983
  this.attach(target);
1941
1984
  },
1942
1985
  visible(target, observeVisible) {
1943
- for (const hook of visibleHooks) {
1944
- const cleanup = observeVisible(target, () => {
1986
+ if (visibleHooks.length === 0) {
1987
+ return;
1988
+ }
1989
+ const cleanup = observeVisible(target, () => {
1990
+ for (let index = 0; index < visibleHooks.length; index += 1) {
1991
+ const hook = visibleHooks[index];
1945
1992
  runtime.scheduler?.enqueue("lifecycle", () => {
1946
1993
  const hookCleanup = hook(target);
1947
1994
  if (typeof hookCleanup === "function") {
@@ -1949,12 +1996,12 @@
1949
1996
  }
1950
1997
  }, {
1951
1998
  scope,
1952
- key: `visible:${visibleHooks.indexOf(hook)}`
1999
+ key: `visible:${index}`
1953
2000
  }) ?? runVisibleHook(hook, target);
1954
- });
1955
- if (typeof cleanup === "function") {
1956
- cleanups.push(cleanup);
1957
2001
  }
2002
+ });
2003
+ if (typeof cleanup === "function") {
2004
+ cleanups.push(cleanup);
1958
2005
  }
1959
2006
  },
1960
2007
  cleanup() {
@@ -2210,11 +2257,12 @@
2210
2257
  signal: context.abort
2211
2258
  });
2212
2259
 
2260
+ assertTransportResponse(id, response);
2213
2261
  if (!response.ok) {
2214
2262
  throw new Error(`Server function "${id}" failed with ${response.status}.`);
2215
2263
  }
2216
2264
 
2217
- const result = await readServerResponse(response);
2265
+ const result = await readServerResponse(id, response);
2218
2266
  await applyServerResult(result, runContext);
2219
2267
  return markAppliedServerValue(unwrapServerResult(result));
2220
2268
  }
@@ -2255,6 +2303,11 @@
2255
2303
  return result;
2256
2304
  }
2257
2305
 
2306
+ if (result.error) {
2307
+ markAppliedServerResult(result);
2308
+ throw toError(result.error);
2309
+ }
2310
+
2258
2311
  if (result.signals && context.signals) {
2259
2312
  for (const [path, value] of Object.entries(result.signals)) {
2260
2313
  context.signals.set?.(path, value);
@@ -2273,15 +2326,7 @@
2273
2326
  await context.router?.navigate?.(result.redirect);
2274
2327
  }
2275
2328
 
2276
- if (result.error) {
2277
- throw toError(result.error);
2278
- }
2279
-
2280
- Object.defineProperty(result, appliedServerResult, {
2281
- configurable: true,
2282
- enumerable: false,
2283
- value: true
2284
- });
2329
+ markAppliedServerResult(result);
2285
2330
 
2286
2331
  return result;
2287
2332
  }
@@ -2300,6 +2345,15 @@
2300
2345
  return value;
2301
2346
  }
2302
2347
 
2348
+ function markAppliedServerResult(result) {
2349
+ Object.defineProperty(result, appliedServerResult, {
2350
+ configurable: true,
2351
+ enumerable: false,
2352
+ value: true
2353
+ });
2354
+ return result;
2355
+ }
2356
+
2303
2357
  function defaultInput(context = {}) {
2304
2358
  const form = findForm(context);
2305
2359
  if (form) {
@@ -2377,10 +2431,37 @@
2377
2431
  return namespace([]);
2378
2432
  }
2379
2433
 
2380
- async function readServerResponse(response) {
2434
+ function assertTransportResponse(id, response) {
2435
+ if (!response || typeof response !== "object") {
2436
+ throw new Error(`Server function "${id}" transport returned an invalid response: expected a fetch Response-like object.`);
2437
+ }
2438
+ if (typeof response.ok !== "boolean") {
2439
+ throw new Error(`Server function "${id}" transport returned an invalid response: missing boolean ok.`);
2440
+ }
2441
+ if (!response.headers || typeof response.headers.get !== "function") {
2442
+ throw new Error(`Server function "${id}" transport returned an invalid response: missing headers.get(name).`);
2443
+ }
2444
+ }
2445
+
2446
+ async function readServerResponse(id, response) {
2447
+ if (response.status === 204) {
2448
+ return { value: undefined };
2449
+ }
2381
2450
  const type = response.headers.get("content-type") ?? "";
2382
2451
  if (type.includes("application/json")) {
2383
- return response.json();
2452
+ if (typeof response.json !== "function") {
2453
+ throw new Error(`Server function "${id}" transport returned an invalid response: missing json().`);
2454
+ }
2455
+ try {
2456
+ return await response.json();
2457
+ } catch (cause) {
2458
+ throw new Error(`Server function "${id}" returned invalid JSON: ${errorMessage(cause)}`, {
2459
+ cause
2460
+ });
2461
+ }
2462
+ }
2463
+ if (typeof response.text !== "function") {
2464
+ throw new Error(`Server function "${id}" transport returned an invalid response: missing text().`);
2384
2465
  }
2385
2466
  return { value: await response.text() };
2386
2467
  }
@@ -2493,6 +2574,9 @@
2493
2574
  if (tag === "[object File]" || tag === "[object Blob]" || tag === "[object FormData]") {
2494
2575
  throw new Error("Server proxy JSON transport does not support File, Blob, or FormData values yet.");
2495
2576
  }
2577
+ if (isUnsupportedJsonTransportObject(value, tag)) {
2578
+ throw new Error("Server proxy JSON transport does not support URLSearchParams, Headers, Request, Response, ReadableStream, ArrayBuffer, or typed array values yet.");
2579
+ }
2496
2580
  if (Array.isArray(value)) {
2497
2581
  for (const item of value) {
2498
2582
  assertJsonTransportable(item, stack);
@@ -2506,6 +2590,16 @@
2506
2590
  stack.delete(value);
2507
2591
  }
2508
2592
 
2593
+ function isUnsupportedJsonTransportObject(value, tag = Object.prototype.toString.call(value)) {
2594
+ return tag === "[object URLSearchParams]"
2595
+ || tag === "[object Headers]"
2596
+ || tag === "[object Request]"
2597
+ || tag === "[object Response]"
2598
+ || tag === "[object ReadableStream]"
2599
+ || tag === "[object ArrayBuffer]"
2600
+ || ArrayBuffer.isView(value);
2601
+ }
2602
+
2509
2603
  function joinEndpoint(endpoint, id) {
2510
2604
  return `${String(endpoint).replace(/\/$/, "")}/${encodeURIComponent(id)}`;
2511
2605
  }
@@ -2527,6 +2621,10 @@
2527
2621
  return new Error(String(value));
2528
2622
  }
2529
2623
 
2624
+ function errorMessage(error) {
2625
+ return error instanceof Error ? error.message : String(error);
2626
+ }
2627
+
2530
2628
  function assertServerId(id) {
2531
2629
  if (typeof id !== "string" || id.length === 0) {
2532
2630
  throw new TypeError("Server function id must be a non-empty string.");
@@ -2759,7 +2857,8 @@
2759
2857
  const phases = [...(options.phases ?? defaultPhases)];
2760
2858
  const queues = new Map(phases.map((phase) => [phase, []]));
2761
2859
  const keyedJobs = new Map();
2762
- const destroyedScopes = new Set();
2860
+ const destroyedObjectScopes = new WeakSet();
2861
+ const destroyedPrimitiveScopes = new Set();
2763
2862
  const objectScopeIds = new WeakMap();
2764
2863
  const onError = typeof options.onError === "function" ? options.onError : undefined;
2765
2864
  const maxDepth = options.maxDepth ?? 100;
@@ -2807,7 +2906,7 @@
2807
2906
  throw new TypeError("scheduler.enqueue(phase, fn) requires a function.");
2808
2907
  }
2809
2908
  const scope = options.scope;
2810
- if (scope !== undefined && destroyedScopes.has(scope)) {
2909
+ if (isScopeDestroyed(scope)) {
2811
2910
  return noop;
2812
2911
  }
2813
2912
 
@@ -2911,14 +3010,29 @@
2911
3010
 
2912
3011
  markScopeDestroyed(scope) {
2913
3012
  if (scope !== undefined) {
2914
- destroyedScopes.add(scope);
3013
+ if (isObjectScope(scope)) {
3014
+ destroyedObjectScopes.add(scope);
3015
+ } else {
3016
+ destroyedPrimitiveScopes.add(scope);
3017
+ }
2915
3018
  api.cancelScope(scope);
2916
3019
  }
2917
3020
  return api;
2918
3021
  },
2919
3022
 
3023
+ reviveScope(scope) {
3024
+ if (scope !== undefined) {
3025
+ if (isObjectScope(scope)) {
3026
+ destroyedObjectScopes.delete(scope);
3027
+ } else {
3028
+ destroyedPrimitiveScopes.delete(scope);
3029
+ }
3030
+ }
3031
+ return api;
3032
+ },
3033
+
2920
3034
  isScopeDestroyed(scope) {
2921
- return scope !== undefined && destroyedScopes.has(scope);
3035
+ return isScopeDestroyed(scope);
2922
3036
  },
2923
3037
 
2924
3038
  inspect() {
@@ -2930,7 +3044,7 @@
2930
3044
  strategy,
2931
3045
  phases: [...phases],
2932
3046
  pending: counts,
2933
- scopesDestroyed: destroyedScopes.size,
3047
+ scopesDestroyed: destroyedPrimitiveScopes.size,
2934
3048
  flushing,
2935
3049
  scheduled
2936
3050
  };
@@ -2945,7 +3059,7 @@
2945
3059
  queue.length = 0;
2946
3060
  }
2947
3061
  keyedJobs.clear();
2948
- destroyedScopes.clear();
3062
+ destroyedPrimitiveScopes.clear();
2949
3063
  }
2950
3064
  };
2951
3065
 
@@ -2985,7 +3099,7 @@
2985
3099
  if (job.key) {
2986
3100
  keyedJobs.delete(job.key);
2987
3101
  }
2988
- if (job.canceled || (job.scope !== undefined && destroyedScopes.has(job.scope))) {
3102
+ if (job.canceled || isScopeDestroyed(job.scope)) {
2989
3103
  continue;
2990
3104
  }
2991
3105
  try {
@@ -3042,6 +3156,20 @@
3042
3156
  }
3043
3157
  return String(scope);
3044
3158
  }
3159
+
3160
+ function isScopeDestroyed(scope) {
3161
+ if (scope === undefined) {
3162
+ return false;
3163
+ }
3164
+ if (isObjectScope(scope)) {
3165
+ return destroyedObjectScopes.has(scope);
3166
+ }
3167
+ return destroyedPrimitiveScopes.has(scope);
3168
+ }
3169
+ }
3170
+
3171
+ function isObjectScope(scope) {
3172
+ return (typeof scope === "object" && scope !== null) || typeof scope === "function";
3045
3173
  }
3046
3174
 
3047
3175
  function scheduleMicrotask(fn) {
@@ -3102,6 +3230,7 @@
3102
3230
 
3103
3231
  scan(rootOrFragment = rootNode) {
3104
3232
  assertActive();
3233
+ reviveScopes(rootOrFragment);
3105
3234
  bindSignalAttributes(rootOrFragment);
3106
3235
  bindClassAttributes(rootOrFragment);
3107
3236
  bindEventAttributes(rootOrFragment);
@@ -3605,6 +3734,12 @@
3605
3734
  }
3606
3735
  }
3607
3736
 
3737
+ function reviveScopes(scope) {
3738
+ for (const element of elementsIn(scope)) {
3739
+ schedulerInstance.reviveScope?.(element);
3740
+ }
3741
+ }
3742
+
3608
3743
  return api;
3609
3744
  }
3610
3745
 
@@ -4168,7 +4303,10 @@
4168
4303
  }
4169
4304
  const matched = api.match(url);
4170
4305
  if (matched?.route?.partial && partials?.resolve?.(matched.route.partial)) {
4171
- return partials.render(matched.route.partial, matched.params, contextFor(matched));
4306
+ return partials.render(matched.route.partial, matched.params, {
4307
+ ...contextFor(matched),
4308
+ prefetch: true
4309
+ });
4172
4310
  }
4173
4311
  return Promise.resolve(null);
4174
4312
  },
@@ -5204,6 +5342,13 @@
5204
5342
  function setOrRegisterSignal(signals, path, value) {
5205
5343
  const id = String(path).split(".")[0];
5206
5344
  if (signals.has?.(id)) {
5345
+ if (path === id) {
5346
+ const entry = signals._entry?.(id);
5347
+ if (typeof entry?._restore === "function" && isAsyncSignalSnapshot(value)) {
5348
+ entry._restore(value);
5349
+ return;
5350
+ }
5351
+ }
5207
5352
  signals.set(path, value);
5208
5353
  return;
5209
5354
  }
@@ -5213,6 +5358,17 @@
5213
5358
  }
5214
5359
  }
5215
5360
 
5361
+ function isAsyncSignalSnapshot(value) {
5362
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
5363
+ return false;
5364
+ }
5365
+ return Object.hasOwn(value, "value")
5366
+ && (Object.hasOwn(value, "loading")
5367
+ || Object.hasOwn(value, "error")
5368
+ || Object.hasOwn(value, "status")
5369
+ || Object.hasOwn(value, "version"));
5370
+ }
5371
+
5216
5372
  function attachServerCache(server, cache) {
5217
5373
  try {
5218
5374
  server.cache = cache;