@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/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.11.2 - 2026-06-18
4
+
5
+ - Published the post-`0.11.1` feedback-regression hardening now on `main`,
6
+ including scheduler scope revival, server error envelopes, async-signal SSR
7
+ snapshot restore, lazy component error handling, router prefetch context,
8
+ server proxy transport validation, and package export smoke coverage.
9
+ - Refreshed the generated Async Pipeline workflow and lock metadata to
10
+ `@async/pipeline` `0.9.1`.
11
+ - Ignored local `docs/goals/` GoalBuddy planning bundles.
12
+
13
+ ## 0.11.1 - 2026-06-17
14
+
15
+ - Removed the literal old global fetch identifier from published release notes
16
+ so package-wide text scans stay focused on runtime artifacts.
17
+ - Hardened scheduler scope revival, server error envelopes, async-signal SSR
18
+ snapshot restore, lazy component sync-rendering errors, router prefetch
19
+ context, server proxy transport validation, and package export smoke tests.
20
+ - Bundle size from bundled TypeScript source: `browser.ts` 177,243 B raw /
21
+ 33,354 B gzip -> `browser.min.js` 75,517 B raw / 22,516 B gzip
22
+ (-101,726 B raw, -10,838 B gzip).
23
+
3
24
  ## 0.11.0 - 2026-06-17
4
25
 
5
26
  - Removed the networked `ssr-spa` router mode and route-fragment fetching so
@@ -8,7 +29,7 @@
8
29
  - Changed browser navigation to render registered SPA partials locally in
9
30
  `spa` and `csr` modes while leaving same-origin document navigation alone in
10
31
  `ssr` and `mpa` modes.
11
- - Replaced the server proxy's implicit `globalThis.fetch` default with an
32
+ - Replaced the server proxy's implicit global fetch default with an
12
33
  explicit `transport` callback supplied by application code.
13
34
  - Published only generated runtime artifacts and declarations:
14
35
  `browser.*`, `server.js`, `framework.ts`, and `framework.d.ts`; source,
package/browser.js CHANGED
@@ -185,6 +185,24 @@ const __asyncSignalModule = (() => {
185
185
  };
186
186
  },
187
187
 
188
+ _restore(snapshot = {}) {
189
+ if (!isAsyncSignalSnapshot(snapshot)) {
190
+ return state.set(snapshot);
191
+ }
192
+ if (activeAbort && !activeAbort.aborted) {
193
+ activeAbort.cancel(new Error(`Async signal "${registeredId}" restored from snapshot.`));
194
+ }
195
+ value = snapshot.value;
196
+ loading = Boolean(snapshot.loading);
197
+ error = snapshot.error ?? null;
198
+ status = typeof snapshot.status === "string" ? snapshot.status : inferStatus({ value, loading, error });
199
+ if (Number.isFinite(snapshot.version)) {
200
+ version = snapshot.version;
201
+ }
202
+ notify();
203
+ return state;
204
+ },
205
+
188
206
  _bindRegistry(nextRegistry, nextId) {
189
207
  registry = nextRegistry;
190
208
  registeredId = nextId;
@@ -270,6 +288,27 @@ const __asyncSignalModule = (() => {
270
288
  return Boolean(value?.[asyncSignalKind]);
271
289
  }
272
290
 
291
+ function isAsyncSignalSnapshot(value) {
292
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
293
+ return false;
294
+ }
295
+ return Object.hasOwn(value, "value")
296
+ && (Object.hasOwn(value, "loading")
297
+ || Object.hasOwn(value, "error")
298
+ || Object.hasOwn(value, "status")
299
+ || Object.hasOwn(value, "version"));
300
+ }
301
+
302
+ function inferStatus({ value, loading, error }) {
303
+ if (loading) {
304
+ return "loading";
305
+ }
306
+ if (error) {
307
+ return "error";
308
+ }
309
+ return value === undefined ? "idle" : "ready";
310
+ }
311
+
273
312
  function attachCancel(signal, controller) {
274
313
  Object.defineProperty(signal, "cancel", {
275
314
  configurable: true,
@@ -1908,12 +1947,16 @@ const __componentModule = (() => {
1908
1947
  });
1909
1948
 
1910
1949
  const output = Component.call(context, props);
1950
+ if (output && typeof output.then === "function") {
1951
+ throw new TypeError(`Component "${componentName(Component)}" returned a Promise. Async components are not supported by synchronous renderComponent(). Use an async partial or handler instead.`);
1952
+ }
1911
1953
  const html = renderScopedTemplate(output);
1912
1954
 
1913
1955
  return {
1914
1956
  html,
1915
1957
  attach(target) {
1916
- for (const hook of attachHooks) {
1958
+ for (let index = 0; index < attachHooks.length; index += 1) {
1959
+ const hook = attachHooks[index];
1917
1960
  runtime.scheduler?.enqueue("lifecycle", () => {
1918
1961
  const cleanup = hook(target);
1919
1962
  if (typeof cleanup === "function") {
@@ -1921,7 +1964,7 @@ const __componentModule = (() => {
1921
1964
  }
1922
1965
  }, {
1923
1966
  scope,
1924
- key: `attach:${attachHooks.indexOf(hook)}`
1967
+ key: `attach:${index}`
1925
1968
  }) ?? runAttachHook(hook, target);
1926
1969
  }
1927
1970
  },
@@ -1929,8 +1972,12 @@ const __componentModule = (() => {
1929
1972
  this.attach(target);
1930
1973
  },
1931
1974
  visible(target, observeVisible) {
1932
- for (const hook of visibleHooks) {
1933
- const cleanup = observeVisible(target, () => {
1975
+ if (visibleHooks.length === 0) {
1976
+ return;
1977
+ }
1978
+ const cleanup = observeVisible(target, () => {
1979
+ for (let index = 0; index < visibleHooks.length; index += 1) {
1980
+ const hook = visibleHooks[index];
1934
1981
  runtime.scheduler?.enqueue("lifecycle", () => {
1935
1982
  const hookCleanup = hook(target);
1936
1983
  if (typeof hookCleanup === "function") {
@@ -1938,12 +1985,12 @@ const __componentModule = (() => {
1938
1985
  }
1939
1986
  }, {
1940
1987
  scope,
1941
- key: `visible:${visibleHooks.indexOf(hook)}`
1988
+ key: `visible:${index}`
1942
1989
  }) ?? runVisibleHook(hook, target);
1943
- });
1944
- if (typeof cleanup === "function") {
1945
- cleanups.push(cleanup);
1946
1990
  }
1991
+ });
1992
+ if (typeof cleanup === "function") {
1993
+ cleanups.push(cleanup);
1947
1994
  }
1948
1995
  },
1949
1996
  cleanup() {
@@ -2199,11 +2246,12 @@ const __serverModule = (() => {
2199
2246
  signal: context.abort
2200
2247
  });
2201
2248
 
2249
+ assertTransportResponse(id, response);
2202
2250
  if (!response.ok) {
2203
2251
  throw new Error(`Server function "${id}" failed with ${response.status}.`);
2204
2252
  }
2205
2253
 
2206
- const result = await readServerResponse(response);
2254
+ const result = await readServerResponse(id, response);
2207
2255
  await applyServerResult(result, runContext);
2208
2256
  return markAppliedServerValue(unwrapServerResult(result));
2209
2257
  }
@@ -2244,6 +2292,11 @@ const __serverModule = (() => {
2244
2292
  return result;
2245
2293
  }
2246
2294
 
2295
+ if (result.error) {
2296
+ markAppliedServerResult(result);
2297
+ throw toError(result.error);
2298
+ }
2299
+
2247
2300
  if (result.signals && context.signals) {
2248
2301
  for (const [path, value] of Object.entries(result.signals)) {
2249
2302
  context.signals.set?.(path, value);
@@ -2262,15 +2315,7 @@ const __serverModule = (() => {
2262
2315
  await context.router?.navigate?.(result.redirect);
2263
2316
  }
2264
2317
 
2265
- if (result.error) {
2266
- throw toError(result.error);
2267
- }
2268
-
2269
- Object.defineProperty(result, appliedServerResult, {
2270
- configurable: true,
2271
- enumerable: false,
2272
- value: true
2273
- });
2318
+ markAppliedServerResult(result);
2274
2319
 
2275
2320
  return result;
2276
2321
  }
@@ -2289,6 +2334,15 @@ const __serverModule = (() => {
2289
2334
  return value;
2290
2335
  }
2291
2336
 
2337
+ function markAppliedServerResult(result) {
2338
+ Object.defineProperty(result, appliedServerResult, {
2339
+ configurable: true,
2340
+ enumerable: false,
2341
+ value: true
2342
+ });
2343
+ return result;
2344
+ }
2345
+
2292
2346
  function defaultInput(context = {}) {
2293
2347
  const form = findForm(context);
2294
2348
  if (form) {
@@ -2366,10 +2420,37 @@ const __serverModule = (() => {
2366
2420
  return namespace([]);
2367
2421
  }
2368
2422
 
2369
- async function readServerResponse(response) {
2423
+ function assertTransportResponse(id, response) {
2424
+ if (!response || typeof response !== "object") {
2425
+ throw new Error(`Server function "${id}" transport returned an invalid response: expected a fetch Response-like object.`);
2426
+ }
2427
+ if (typeof response.ok !== "boolean") {
2428
+ throw new Error(`Server function "${id}" transport returned an invalid response: missing boolean ok.`);
2429
+ }
2430
+ if (!response.headers || typeof response.headers.get !== "function") {
2431
+ throw new Error(`Server function "${id}" transport returned an invalid response: missing headers.get(name).`);
2432
+ }
2433
+ }
2434
+
2435
+ async function readServerResponse(id, response) {
2436
+ if (response.status === 204) {
2437
+ return { value: undefined };
2438
+ }
2370
2439
  const type = response.headers.get("content-type") ?? "";
2371
2440
  if (type.includes("application/json")) {
2372
- return response.json();
2441
+ if (typeof response.json !== "function") {
2442
+ throw new Error(`Server function "${id}" transport returned an invalid response: missing json().`);
2443
+ }
2444
+ try {
2445
+ return await response.json();
2446
+ } catch (cause) {
2447
+ throw new Error(`Server function "${id}" returned invalid JSON: ${errorMessage(cause)}`, {
2448
+ cause
2449
+ });
2450
+ }
2451
+ }
2452
+ if (typeof response.text !== "function") {
2453
+ throw new Error(`Server function "${id}" transport returned an invalid response: missing text().`);
2373
2454
  }
2374
2455
  return { value: await response.text() };
2375
2456
  }
@@ -2482,6 +2563,9 @@ const __serverModule = (() => {
2482
2563
  if (tag === "[object File]" || tag === "[object Blob]" || tag === "[object FormData]") {
2483
2564
  throw new Error("Server proxy JSON transport does not support File, Blob, or FormData values yet.");
2484
2565
  }
2566
+ if (isUnsupportedJsonTransportObject(value, tag)) {
2567
+ throw new Error("Server proxy JSON transport does not support URLSearchParams, Headers, Request, Response, ReadableStream, ArrayBuffer, or typed array values yet.");
2568
+ }
2485
2569
  if (Array.isArray(value)) {
2486
2570
  for (const item of value) {
2487
2571
  assertJsonTransportable(item, stack);
@@ -2495,6 +2579,16 @@ const __serverModule = (() => {
2495
2579
  stack.delete(value);
2496
2580
  }
2497
2581
 
2582
+ function isUnsupportedJsonTransportObject(value, tag = Object.prototype.toString.call(value)) {
2583
+ return tag === "[object URLSearchParams]"
2584
+ || tag === "[object Headers]"
2585
+ || tag === "[object Request]"
2586
+ || tag === "[object Response]"
2587
+ || tag === "[object ReadableStream]"
2588
+ || tag === "[object ArrayBuffer]"
2589
+ || ArrayBuffer.isView(value);
2590
+ }
2591
+
2498
2592
  function joinEndpoint(endpoint, id) {
2499
2593
  return `${String(endpoint).replace(/\/$/, "")}/${encodeURIComponent(id)}`;
2500
2594
  }
@@ -2516,6 +2610,10 @@ const __serverModule = (() => {
2516
2610
  return new Error(String(value));
2517
2611
  }
2518
2612
 
2613
+ function errorMessage(error) {
2614
+ return error instanceof Error ? error.message : String(error);
2615
+ }
2616
+
2519
2617
  function assertServerId(id) {
2520
2618
  if (typeof id !== "string" || id.length === 0) {
2521
2619
  throw new TypeError("Server function id must be a non-empty string.");
@@ -2748,7 +2846,8 @@ const __schedulerModule = (() => {
2748
2846
  const phases = [...(options.phases ?? defaultPhases)];
2749
2847
  const queues = new Map(phases.map((phase) => [phase, []]));
2750
2848
  const keyedJobs = new Map();
2751
- const destroyedScopes = new Set();
2849
+ const destroyedObjectScopes = new WeakSet();
2850
+ const destroyedPrimitiveScopes = new Set();
2752
2851
  const objectScopeIds = new WeakMap();
2753
2852
  const onError = typeof options.onError === "function" ? options.onError : undefined;
2754
2853
  const maxDepth = options.maxDepth ?? 100;
@@ -2796,7 +2895,7 @@ const __schedulerModule = (() => {
2796
2895
  throw new TypeError("scheduler.enqueue(phase, fn) requires a function.");
2797
2896
  }
2798
2897
  const scope = options.scope;
2799
- if (scope !== undefined && destroyedScopes.has(scope)) {
2898
+ if (isScopeDestroyed(scope)) {
2800
2899
  return noop;
2801
2900
  }
2802
2901
 
@@ -2900,14 +2999,29 @@ const __schedulerModule = (() => {
2900
2999
 
2901
3000
  markScopeDestroyed(scope) {
2902
3001
  if (scope !== undefined) {
2903
- destroyedScopes.add(scope);
3002
+ if (isObjectScope(scope)) {
3003
+ destroyedObjectScopes.add(scope);
3004
+ } else {
3005
+ destroyedPrimitiveScopes.add(scope);
3006
+ }
2904
3007
  api.cancelScope(scope);
2905
3008
  }
2906
3009
  return api;
2907
3010
  },
2908
3011
 
3012
+ reviveScope(scope) {
3013
+ if (scope !== undefined) {
3014
+ if (isObjectScope(scope)) {
3015
+ destroyedObjectScopes.delete(scope);
3016
+ } else {
3017
+ destroyedPrimitiveScopes.delete(scope);
3018
+ }
3019
+ }
3020
+ return api;
3021
+ },
3022
+
2909
3023
  isScopeDestroyed(scope) {
2910
- return scope !== undefined && destroyedScopes.has(scope);
3024
+ return isScopeDestroyed(scope);
2911
3025
  },
2912
3026
 
2913
3027
  inspect() {
@@ -2919,7 +3033,7 @@ const __schedulerModule = (() => {
2919
3033
  strategy,
2920
3034
  phases: [...phases],
2921
3035
  pending: counts,
2922
- scopesDestroyed: destroyedScopes.size,
3036
+ scopesDestroyed: destroyedPrimitiveScopes.size,
2923
3037
  flushing,
2924
3038
  scheduled
2925
3039
  };
@@ -2934,7 +3048,7 @@ const __schedulerModule = (() => {
2934
3048
  queue.length = 0;
2935
3049
  }
2936
3050
  keyedJobs.clear();
2937
- destroyedScopes.clear();
3051
+ destroyedPrimitiveScopes.clear();
2938
3052
  }
2939
3053
  };
2940
3054
 
@@ -2974,7 +3088,7 @@ const __schedulerModule = (() => {
2974
3088
  if (job.key) {
2975
3089
  keyedJobs.delete(job.key);
2976
3090
  }
2977
- if (job.canceled || (job.scope !== undefined && destroyedScopes.has(job.scope))) {
3091
+ if (job.canceled || isScopeDestroyed(job.scope)) {
2978
3092
  continue;
2979
3093
  }
2980
3094
  try {
@@ -3031,6 +3145,20 @@ const __schedulerModule = (() => {
3031
3145
  }
3032
3146
  return String(scope);
3033
3147
  }
3148
+
3149
+ function isScopeDestroyed(scope) {
3150
+ if (scope === undefined) {
3151
+ return false;
3152
+ }
3153
+ if (isObjectScope(scope)) {
3154
+ return destroyedObjectScopes.has(scope);
3155
+ }
3156
+ return destroyedPrimitiveScopes.has(scope);
3157
+ }
3158
+ }
3159
+
3160
+ function isObjectScope(scope) {
3161
+ return (typeof scope === "object" && scope !== null) || typeof scope === "function";
3034
3162
  }
3035
3163
 
3036
3164
  function scheduleMicrotask(fn) {
@@ -3091,6 +3219,7 @@ const __loaderModule = (() => {
3091
3219
 
3092
3220
  scan(rootOrFragment = rootNode) {
3093
3221
  assertActive();
3222
+ reviveScopes(rootOrFragment);
3094
3223
  bindSignalAttributes(rootOrFragment);
3095
3224
  bindClassAttributes(rootOrFragment);
3096
3225
  bindEventAttributes(rootOrFragment);
@@ -3594,6 +3723,12 @@ const __loaderModule = (() => {
3594
3723
  }
3595
3724
  }
3596
3725
 
3726
+ function reviveScopes(scope) {
3727
+ for (const element of elementsIn(scope)) {
3728
+ schedulerInstance.reviveScope?.(element);
3729
+ }
3730
+ }
3731
+
3597
3732
  return api;
3598
3733
  }
3599
3734
 
@@ -4157,7 +4292,10 @@ const __routerModule = (() => {
4157
4292
  }
4158
4293
  const matched = api.match(url);
4159
4294
  if (matched?.route?.partial && partials?.resolve?.(matched.route.partial)) {
4160
- return partials.render(matched.route.partial, matched.params, contextFor(matched));
4295
+ return partials.render(matched.route.partial, matched.params, {
4296
+ ...contextFor(matched),
4297
+ prefetch: true
4298
+ });
4161
4299
  }
4162
4300
  return Promise.resolve(null);
4163
4301
  },
@@ -5193,6 +5331,13 @@ const __appModule = (() => {
5193
5331
  function setOrRegisterSignal(signals, path, value) {
5194
5332
  const id = String(path).split(".")[0];
5195
5333
  if (signals.has?.(id)) {
5334
+ if (path === id) {
5335
+ const entry = signals._entry?.(id);
5336
+ if (typeof entry?._restore === "function" && isAsyncSignalSnapshot(value)) {
5337
+ entry._restore(value);
5338
+ return;
5339
+ }
5340
+ }
5196
5341
  signals.set(path, value);
5197
5342
  return;
5198
5343
  }
@@ -5202,6 +5347,17 @@ const __appModule = (() => {
5202
5347
  }
5203
5348
  }
5204
5349
 
5350
+ function isAsyncSignalSnapshot(value) {
5351
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
5352
+ return false;
5353
+ }
5354
+ return Object.hasOwn(value, "value")
5355
+ && (Object.hasOwn(value, "loading")
5356
+ || Object.hasOwn(value, "error")
5357
+ || Object.hasOwn(value, "status")
5358
+ || Object.hasOwn(value, "version"));
5359
+ }
5360
+
5205
5361
  function attachServerCache(server, cache) {
5206
5362
  try {
5207
5363
  server.cache = cache;