@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/framework.ts CHANGED
@@ -188,6 +188,24 @@ const __asyncSignalModule = (() => {
188
188
  };
189
189
  },
190
190
 
191
+ _restore(snapshot = {}) {
192
+ if (!isAsyncSignalSnapshot(snapshot)) {
193
+ return state.set(snapshot);
194
+ }
195
+ if (activeAbort && !activeAbort.aborted) {
196
+ activeAbort.cancel(new Error(`Async signal "${registeredId}" restored from snapshot.`));
197
+ }
198
+ value = snapshot.value;
199
+ loading = Boolean(snapshot.loading);
200
+ error = snapshot.error ?? null;
201
+ status = typeof snapshot.status === "string" ? snapshot.status : inferStatus({ value, loading, error });
202
+ if (Number.isFinite(snapshot.version)) {
203
+ version = snapshot.version;
204
+ }
205
+ notify();
206
+ return state;
207
+ },
208
+
191
209
  _bindRegistry(nextRegistry, nextId) {
192
210
  registry = nextRegistry;
193
211
  registeredId = nextId;
@@ -273,6 +291,27 @@ const __asyncSignalModule = (() => {
273
291
  return Boolean(value?.[asyncSignalKind]);
274
292
  }
275
293
 
294
+ function isAsyncSignalSnapshot(value) {
295
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
296
+ return false;
297
+ }
298
+ return Object.hasOwn(value, "value")
299
+ && (Object.hasOwn(value, "loading")
300
+ || Object.hasOwn(value, "error")
301
+ || Object.hasOwn(value, "status")
302
+ || Object.hasOwn(value, "version"));
303
+ }
304
+
305
+ function inferStatus({ value, loading, error }) {
306
+ if (loading) {
307
+ return "loading";
308
+ }
309
+ if (error) {
310
+ return "error";
311
+ }
312
+ return value === undefined ? "idle" : "ready";
313
+ }
314
+
276
315
  function attachCancel(signal, controller) {
277
316
  Object.defineProperty(signal, "cancel", {
278
317
  configurable: true,
@@ -1911,12 +1950,16 @@ const __componentModule = (() => {
1911
1950
  });
1912
1951
 
1913
1952
  const output = Component.call(context, props);
1953
+ if (output && typeof output.then === "function") {
1954
+ throw new TypeError(`Component "${componentName(Component)}" returned a Promise. Async components are not supported by synchronous renderComponent(). Use an async partial or handler instead.`);
1955
+ }
1914
1956
  const html = renderScopedTemplate(output);
1915
1957
 
1916
1958
  return {
1917
1959
  html,
1918
1960
  attach(target) {
1919
- for (const hook of attachHooks) {
1961
+ for (let index = 0; index < attachHooks.length; index += 1) {
1962
+ const hook = attachHooks[index];
1920
1963
  runtime.scheduler?.enqueue("lifecycle", () => {
1921
1964
  const cleanup = hook(target);
1922
1965
  if (typeof cleanup === "function") {
@@ -1924,7 +1967,7 @@ const __componentModule = (() => {
1924
1967
  }
1925
1968
  }, {
1926
1969
  scope,
1927
- key: `attach:${attachHooks.indexOf(hook)}`
1970
+ key: `attach:${index}`
1928
1971
  }) ?? runAttachHook(hook, target);
1929
1972
  }
1930
1973
  },
@@ -1932,8 +1975,12 @@ const __componentModule = (() => {
1932
1975
  this.attach(target);
1933
1976
  },
1934
1977
  visible(target, observeVisible) {
1935
- for (const hook of visibleHooks) {
1936
- const cleanup = observeVisible(target, () => {
1978
+ if (visibleHooks.length === 0) {
1979
+ return;
1980
+ }
1981
+ const cleanup = observeVisible(target, () => {
1982
+ for (let index = 0; index < visibleHooks.length; index += 1) {
1983
+ const hook = visibleHooks[index];
1937
1984
  runtime.scheduler?.enqueue("lifecycle", () => {
1938
1985
  const hookCleanup = hook(target);
1939
1986
  if (typeof hookCleanup === "function") {
@@ -1941,12 +1988,12 @@ const __componentModule = (() => {
1941
1988
  }
1942
1989
  }, {
1943
1990
  scope,
1944
- key: `visible:${visibleHooks.indexOf(hook)}`
1991
+ key: `visible:${index}`
1945
1992
  }) ?? runVisibleHook(hook, target);
1946
- });
1947
- if (typeof cleanup === "function") {
1948
- cleanups.push(cleanup);
1949
1993
  }
1994
+ });
1995
+ if (typeof cleanup === "function") {
1996
+ cleanups.push(cleanup);
1950
1997
  }
1951
1998
  },
1952
1999
  cleanup() {
@@ -2202,11 +2249,12 @@ const __serverModule = (() => {
2202
2249
  signal: context.abort
2203
2250
  });
2204
2251
 
2252
+ assertTransportResponse(id, response);
2205
2253
  if (!response.ok) {
2206
2254
  throw new Error(`Server function "${id}" failed with ${response.status}.`);
2207
2255
  }
2208
2256
 
2209
- const result = await readServerResponse(response);
2257
+ const result = await readServerResponse(id, response);
2210
2258
  await applyServerResult(result, runContext);
2211
2259
  return markAppliedServerValue(unwrapServerResult(result));
2212
2260
  }
@@ -2247,6 +2295,11 @@ const __serverModule = (() => {
2247
2295
  return result;
2248
2296
  }
2249
2297
 
2298
+ if (result.error) {
2299
+ markAppliedServerResult(result);
2300
+ throw toError(result.error);
2301
+ }
2302
+
2250
2303
  if (result.signals && context.signals) {
2251
2304
  for (const [path, value] of Object.entries(result.signals)) {
2252
2305
  context.signals.set?.(path, value);
@@ -2265,15 +2318,7 @@ const __serverModule = (() => {
2265
2318
  await context.router?.navigate?.(result.redirect);
2266
2319
  }
2267
2320
 
2268
- if (result.error) {
2269
- throw toError(result.error);
2270
- }
2271
-
2272
- Object.defineProperty(result, appliedServerResult, {
2273
- configurable: true,
2274
- enumerable: false,
2275
- value: true
2276
- });
2321
+ markAppliedServerResult(result);
2277
2322
 
2278
2323
  return result;
2279
2324
  }
@@ -2292,6 +2337,15 @@ const __serverModule = (() => {
2292
2337
  return value;
2293
2338
  }
2294
2339
 
2340
+ function markAppliedServerResult(result) {
2341
+ Object.defineProperty(result, appliedServerResult, {
2342
+ configurable: true,
2343
+ enumerable: false,
2344
+ value: true
2345
+ });
2346
+ return result;
2347
+ }
2348
+
2295
2349
  function defaultInput(context = {}) {
2296
2350
  const form = findForm(context);
2297
2351
  if (form) {
@@ -2369,10 +2423,37 @@ const __serverModule = (() => {
2369
2423
  return namespace([]);
2370
2424
  }
2371
2425
 
2372
- async function readServerResponse(response) {
2426
+ function assertTransportResponse(id, response) {
2427
+ if (!response || typeof response !== "object") {
2428
+ throw new Error(`Server function "${id}" transport returned an invalid response: expected a fetch Response-like object.`);
2429
+ }
2430
+ if (typeof response.ok !== "boolean") {
2431
+ throw new Error(`Server function "${id}" transport returned an invalid response: missing boolean ok.`);
2432
+ }
2433
+ if (!response.headers || typeof response.headers.get !== "function") {
2434
+ throw new Error(`Server function "${id}" transport returned an invalid response: missing headers.get(name).`);
2435
+ }
2436
+ }
2437
+
2438
+ async function readServerResponse(id, response) {
2439
+ if (response.status === 204) {
2440
+ return { value: undefined };
2441
+ }
2373
2442
  const type = response.headers.get("content-type") ?? "";
2374
2443
  if (type.includes("application/json")) {
2375
- return response.json();
2444
+ if (typeof response.json !== "function") {
2445
+ throw new Error(`Server function "${id}" transport returned an invalid response: missing json().`);
2446
+ }
2447
+ try {
2448
+ return await response.json();
2449
+ } catch (cause) {
2450
+ throw new Error(`Server function "${id}" returned invalid JSON: ${errorMessage(cause)}`, {
2451
+ cause
2452
+ });
2453
+ }
2454
+ }
2455
+ if (typeof response.text !== "function") {
2456
+ throw new Error(`Server function "${id}" transport returned an invalid response: missing text().`);
2376
2457
  }
2377
2458
  return { value: await response.text() };
2378
2459
  }
@@ -2485,6 +2566,9 @@ const __serverModule = (() => {
2485
2566
  if (tag === "[object File]" || tag === "[object Blob]" || tag === "[object FormData]") {
2486
2567
  throw new Error("Server proxy JSON transport does not support File, Blob, or FormData values yet.");
2487
2568
  }
2569
+ if (isUnsupportedJsonTransportObject(value, tag)) {
2570
+ throw new Error("Server proxy JSON transport does not support URLSearchParams, Headers, Request, Response, ReadableStream, ArrayBuffer, or typed array values yet.");
2571
+ }
2488
2572
  if (Array.isArray(value)) {
2489
2573
  for (const item of value) {
2490
2574
  assertJsonTransportable(item, stack);
@@ -2498,6 +2582,16 @@ const __serverModule = (() => {
2498
2582
  stack.delete(value);
2499
2583
  }
2500
2584
 
2585
+ function isUnsupportedJsonTransportObject(value, tag = Object.prototype.toString.call(value)) {
2586
+ return tag === "[object URLSearchParams]"
2587
+ || tag === "[object Headers]"
2588
+ || tag === "[object Request]"
2589
+ || tag === "[object Response]"
2590
+ || tag === "[object ReadableStream]"
2591
+ || tag === "[object ArrayBuffer]"
2592
+ || ArrayBuffer.isView(value);
2593
+ }
2594
+
2501
2595
  function joinEndpoint(endpoint, id) {
2502
2596
  return `${String(endpoint).replace(/\/$/, "")}/${encodeURIComponent(id)}`;
2503
2597
  }
@@ -2519,6 +2613,10 @@ const __serverModule = (() => {
2519
2613
  return new Error(String(value));
2520
2614
  }
2521
2615
 
2616
+ function errorMessage(error) {
2617
+ return error instanceof Error ? error.message : String(error);
2618
+ }
2619
+
2522
2620
  function assertServerId(id) {
2523
2621
  if (typeof id !== "string" || id.length === 0) {
2524
2622
  throw new TypeError("Server function id must be a non-empty string.");
@@ -2751,7 +2849,8 @@ const __schedulerModule = (() => {
2751
2849
  const phases = [...(options.phases ?? defaultPhases)];
2752
2850
  const queues = new Map(phases.map((phase) => [phase, []]));
2753
2851
  const keyedJobs = new Map();
2754
- const destroyedScopes = new Set();
2852
+ const destroyedObjectScopes = new WeakSet();
2853
+ const destroyedPrimitiveScopes = new Set();
2755
2854
  const objectScopeIds = new WeakMap();
2756
2855
  const onError = typeof options.onError === "function" ? options.onError : undefined;
2757
2856
  const maxDepth = options.maxDepth ?? 100;
@@ -2799,7 +2898,7 @@ const __schedulerModule = (() => {
2799
2898
  throw new TypeError("scheduler.enqueue(phase, fn) requires a function.");
2800
2899
  }
2801
2900
  const scope = options.scope;
2802
- if (scope !== undefined && destroyedScopes.has(scope)) {
2901
+ if (isScopeDestroyed(scope)) {
2803
2902
  return noop;
2804
2903
  }
2805
2904
 
@@ -2903,14 +3002,29 @@ const __schedulerModule = (() => {
2903
3002
 
2904
3003
  markScopeDestroyed(scope) {
2905
3004
  if (scope !== undefined) {
2906
- destroyedScopes.add(scope);
3005
+ if (isObjectScope(scope)) {
3006
+ destroyedObjectScopes.add(scope);
3007
+ } else {
3008
+ destroyedPrimitiveScopes.add(scope);
3009
+ }
2907
3010
  api.cancelScope(scope);
2908
3011
  }
2909
3012
  return api;
2910
3013
  },
2911
3014
 
3015
+ reviveScope(scope) {
3016
+ if (scope !== undefined) {
3017
+ if (isObjectScope(scope)) {
3018
+ destroyedObjectScopes.delete(scope);
3019
+ } else {
3020
+ destroyedPrimitiveScopes.delete(scope);
3021
+ }
3022
+ }
3023
+ return api;
3024
+ },
3025
+
2912
3026
  isScopeDestroyed(scope) {
2913
- return scope !== undefined && destroyedScopes.has(scope);
3027
+ return isScopeDestroyed(scope);
2914
3028
  },
2915
3029
 
2916
3030
  inspect() {
@@ -2922,7 +3036,7 @@ const __schedulerModule = (() => {
2922
3036
  strategy,
2923
3037
  phases: [...phases],
2924
3038
  pending: counts,
2925
- scopesDestroyed: destroyedScopes.size,
3039
+ scopesDestroyed: destroyedPrimitiveScopes.size,
2926
3040
  flushing,
2927
3041
  scheduled
2928
3042
  };
@@ -2937,7 +3051,7 @@ const __schedulerModule = (() => {
2937
3051
  queue.length = 0;
2938
3052
  }
2939
3053
  keyedJobs.clear();
2940
- destroyedScopes.clear();
3054
+ destroyedPrimitiveScopes.clear();
2941
3055
  }
2942
3056
  };
2943
3057
 
@@ -2977,7 +3091,7 @@ const __schedulerModule = (() => {
2977
3091
  if (job.key) {
2978
3092
  keyedJobs.delete(job.key);
2979
3093
  }
2980
- if (job.canceled || (job.scope !== undefined && destroyedScopes.has(job.scope))) {
3094
+ if (job.canceled || isScopeDestroyed(job.scope)) {
2981
3095
  continue;
2982
3096
  }
2983
3097
  try {
@@ -3034,6 +3148,20 @@ const __schedulerModule = (() => {
3034
3148
  }
3035
3149
  return String(scope);
3036
3150
  }
3151
+
3152
+ function isScopeDestroyed(scope) {
3153
+ if (scope === undefined) {
3154
+ return false;
3155
+ }
3156
+ if (isObjectScope(scope)) {
3157
+ return destroyedObjectScopes.has(scope);
3158
+ }
3159
+ return destroyedPrimitiveScopes.has(scope);
3160
+ }
3161
+ }
3162
+
3163
+ function isObjectScope(scope) {
3164
+ return (typeof scope === "object" && scope !== null) || typeof scope === "function";
3037
3165
  }
3038
3166
 
3039
3167
  function scheduleMicrotask(fn) {
@@ -3094,6 +3222,7 @@ const __loaderModule = (() => {
3094
3222
 
3095
3223
  scan(rootOrFragment = rootNode) {
3096
3224
  assertActive();
3225
+ reviveScopes(rootOrFragment);
3097
3226
  bindSignalAttributes(rootOrFragment);
3098
3227
  bindClassAttributes(rootOrFragment);
3099
3228
  bindEventAttributes(rootOrFragment);
@@ -3597,6 +3726,12 @@ const __loaderModule = (() => {
3597
3726
  }
3598
3727
  }
3599
3728
 
3729
+ function reviveScopes(scope) {
3730
+ for (const element of elementsIn(scope)) {
3731
+ schedulerInstance.reviveScope?.(element);
3732
+ }
3733
+ }
3734
+
3600
3735
  return api;
3601
3736
  }
3602
3737
 
@@ -4160,7 +4295,10 @@ const __routerModule = (() => {
4160
4295
  }
4161
4296
  const matched = api.match(url);
4162
4297
  if (matched?.route?.partial && partials?.resolve?.(matched.route.partial)) {
4163
- return partials.render(matched.route.partial, matched.params, contextFor(matched));
4298
+ return partials.render(matched.route.partial, matched.params, {
4299
+ ...contextFor(matched),
4300
+ prefetch: true
4301
+ });
4164
4302
  }
4165
4303
  return Promise.resolve(null);
4166
4304
  },
@@ -5196,6 +5334,13 @@ const __appModule = (() => {
5196
5334
  function setOrRegisterSignal(signals, path, value) {
5197
5335
  const id = String(path).split(".")[0];
5198
5336
  if (signals.has?.(id)) {
5337
+ if (path === id) {
5338
+ const entry = signals._entry?.(id);
5339
+ if (typeof entry?._restore === "function" && isAsyncSignalSnapshot(value)) {
5340
+ entry._restore(value);
5341
+ return;
5342
+ }
5343
+ }
5199
5344
  signals.set(path, value);
5200
5345
  return;
5201
5346
  }
@@ -5205,6 +5350,17 @@ const __appModule = (() => {
5205
5350
  }
5206
5351
  }
5207
5352
 
5353
+ function isAsyncSignalSnapshot(value) {
5354
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
5355
+ return false;
5356
+ }
5357
+ return Object.hasOwn(value, "value")
5358
+ && (Object.hasOwn(value, "loading")
5359
+ || Object.hasOwn(value, "error")
5360
+ || Object.hasOwn(value, "status")
5361
+ || Object.hasOwn(value, "version"));
5362
+ }
5363
+
5208
5364
  function attachServerCache(server, cache) {
5209
5365
  try {
5210
5366
  server.cache = cache;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@async/framework",
3
- "version": "0.11.0",
3
+ "version": "0.11.2",
4
4
  "description": "No-build Loader app runtime with browser and server entrypoints, signals, command events, route partials, cache split, SSR activation, and streaming boundaries.",
5
5
  "type": "module",
6
6
  "main": "./server.js",
@@ -115,8 +115,9 @@
115
115
  "pack:check": "pnpm run bundle:check && npm pack --dry-run --ignore-scripts",
116
116
  "pipeline:github:check": "async-pipeline github check",
117
117
  "pipeline:github:generate": "async-pipeline github generate",
118
- "pipeline:pages": "async-pipeline run pages",
118
+ "pipeline:pages": "async-pipeline run-task docs.site",
119
119
  "pipeline:publish": "async-pipeline run publish",
120
+ "pipeline:publish:github:release": "async-pipeline publish github release --package . --registry https://npm.pkg.github.com",
120
121
  "pipeline:publish:npm": "async-pipeline publish npm --package .",
121
122
  "pipeline:release-doctor": "async-pipeline run release-doctor",
122
123
  "pipeline:release:doctor": "async-pipeline release doctor --package .",
@@ -130,7 +131,7 @@
130
131
  "test": "node --test tests/*.test.js"
131
132
  },
132
133
  "devDependencies": {
133
- "@async/pipeline": "0.8.5",
134
+ "@async/pipeline": "0.9.1",
134
135
  "happy-dom": "20.10.5",
135
136
  "terser": "5.48.0"
136
137
  }