@async/framework 0.11.11 → 0.11.13

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,34 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.11.13 - 2026-06-18
4
+
5
+ - Validated server proxy arguments, default input payloads, and selected signal
6
+ values against an explicit JSON transport model before requests leave the
7
+ caller.
8
+ - Rejected values that `JSON.stringify` would silently corrupt, including
9
+ `undefined`, non-finite numbers, functions, symbols, sparse arrays, circular
10
+ structures, class instances, dates, maps, sets, buffers, streams, and web
11
+ platform request/body objects.
12
+ - Added path-aware regression coverage for invalid transport values and
13
+ documented the supported server-call JSON model.
14
+ - Bundle size from bundled TypeScript source: `browser.ts` 187,564 B raw /
15
+ 35,332 B gzip -> `browser.min.js` 80,009 B raw / 23,677 B gzip
16
+ (-107,555 B raw, -11,655 B gzip).
17
+
18
+ ## 0.11.12 - 2026-06-18
19
+
20
+ - Deferred boundary receiver sequence commits until patch effects complete so
21
+ failed DOM swaps, scheduler flushes, redirects, or missing capabilities no
22
+ longer make the same streamed sequence stale.
23
+ - Serialized same-boundary patch application to keep concurrent patches from
24
+ committing out of order while preserving stale rejection after successful
25
+ commits.
26
+ - Added regression coverage for retryable DOM, scheduler, redirect, and
27
+ capability failures plus idempotent partial-effect replay.
28
+ - Bundle size from bundled TypeScript source: `browser.ts` 186,064 B raw /
29
+ 35,039 B gzip -> `browser.min.js` 79,042 B raw / 23,445 B gzip
30
+ (-107,022 B raw, -11,594 B gzip).
31
+
3
32
  ## 0.11.11 - 2026-06-18
4
33
 
5
34
  - Serialized rejected async-signal snapshot errors to stable `name`, `message`,
package/README.md CHANGED
@@ -834,6 +834,14 @@ const server = createServerProxy({
834
834
  await server.cart.add("sku-1", 2);
835
835
  ```
836
836
 
837
+ Proxy requests validate their `args`, default `input`, and selected signal
838
+ values before transport runs. Supported values are `null`, booleans, strings,
839
+ finite numbers, dense arrays, and plain objects composed from those values.
840
+ Values that JSON would silently change or drop, such as `undefined`, functions,
841
+ symbols, `Map`, `Set`, `Date`, sparse arrays, class instances, non-finite
842
+ numbers, circular objects, file-like values, streams, buffers, and typed arrays
843
+ are rejected with a path to the invalid value.
844
+
837
845
  Server responses can include `value`, `signals`, `boundary`, `html`, `redirect`,
838
846
  or `error`. Signal patches are applied before boundary swaps and redirects.
839
847
  Namespace calls such as `server.cart.add(...)` return the unwrapped `value`.
package/browser.js CHANGED
@@ -2573,12 +2573,17 @@ const __serverModule = (() => {
2573
2573
  if (!element) {
2574
2574
  return {};
2575
2575
  }
2576
-
2577
- return {
2578
- value: "value" in element ? element.value : undefined,
2579
- checked: "checked" in element ? element.checked : undefined,
2580
- dataset: element.dataset ? { ...element.dataset } : {}
2581
- };
2576
+ const input = {};
2577
+ if ("value" in element) {
2578
+ input.value = element.value;
2579
+ }
2580
+ if ("checked" in element) {
2581
+ input.checked = element.checked;
2582
+ }
2583
+ if (element.dataset) {
2584
+ input.dataset = { ...element.dataset };
2585
+ }
2586
+ return input;
2582
2587
  }
2583
2588
 
2584
2589
  function createServerNamespace(run, root = {}, contextProvider = () => ({})) {
@@ -2765,38 +2770,76 @@ const __serverModule = (() => {
2765
2770
  return output;
2766
2771
  }
2767
2772
 
2768
- function assertJsonTransportable(value, stack = new Set()) {
2769
- if (typeof value === "bigint") {
2770
- throw new Error("Server proxy JSON transport does not support BigInt values.");
2773
+ function assertJsonTransportable(value, path = "$", stack = new Set()) {
2774
+ if (value === null) {
2775
+ return;
2776
+ }
2777
+
2778
+ const type = typeof value;
2779
+ if (type === "boolean" || type === "string") {
2780
+ return;
2771
2781
  }
2772
- if (value == null || typeof value !== "object") {
2782
+ if (type === "number") {
2783
+ if (!Number.isFinite(value)) {
2784
+ throw new Error(`Server proxy JSON transport does not support non-finite numbers at ${path}.`);
2785
+ }
2773
2786
  return;
2774
2787
  }
2788
+ if (type === "bigint") {
2789
+ throw new Error(`Server proxy JSON transport does not support BigInt values at ${path}.`);
2790
+ }
2791
+ if (type === "undefined") {
2792
+ throw new Error(`Server proxy JSON transport does not support undefined values at ${path}.`);
2793
+ }
2794
+ if (type === "function" || type === "symbol") {
2795
+ throw new Error(`Server proxy JSON transport does not support ${type} values at ${path}.`);
2796
+ }
2797
+ if (type !== "object") {
2798
+ throw new Error(`Server proxy JSON transport does not support ${type} values at ${path}.`);
2799
+ }
2800
+
2775
2801
  if (stack.has(value)) {
2776
- throw new Error("Server proxy JSON transport does not support circular values.");
2802
+ throw new Error(`Server proxy JSON transport does not support circular values at ${path}.`);
2777
2803
  }
2778
2804
  stack.add(value);
2779
2805
 
2780
2806
  const tag = Object.prototype.toString.call(value);
2781
2807
  if (tag === "[object File]" || tag === "[object Blob]" || tag === "[object FormData]") {
2782
- throw new Error("Server proxy JSON transport does not support File, Blob, or FormData values yet.");
2808
+ throw new Error(`Server proxy JSON transport does not support File, Blob, or FormData values yet at ${path}.`);
2783
2809
  }
2784
2810
  if (isUnsupportedJsonTransportObject(value, tag)) {
2785
- throw new Error("Server proxy JSON transport does not support URLSearchParams, Headers, Request, Response, ReadableStream, ArrayBuffer, or typed array values yet.");
2811
+ throw new Error(`Server proxy JSON transport does not support URLSearchParams, Headers, Request, Response, ReadableStream, ArrayBuffer, or typed array values yet at ${path}.`);
2786
2812
  }
2787
2813
  if (Array.isArray(value)) {
2788
- for (const item of value) {
2789
- assertJsonTransportable(item, stack);
2814
+ for (let index = 0; index < value.length; index += 1) {
2815
+ if (!Object.hasOwn(value, index)) {
2816
+ throw new Error(`Server proxy JSON transport does not support sparse arrays at ${path}[${index}].`);
2817
+ }
2818
+ assertJsonTransportable(value[index], `${path}[${index}]`, stack);
2790
2819
  }
2791
2820
  stack.delete(value);
2792
2821
  return;
2793
2822
  }
2794
- for (const item of Object.values(value)) {
2795
- assertJsonTransportable(item, stack);
2823
+
2824
+ if (!isPlainJsonObject(value)) {
2825
+ throw new Error(`Server proxy JSON transport only supports plain objects at ${path}.`);
2826
+ }
2827
+
2828
+ for (const [key, item] of Object.entries(value)) {
2829
+ assertJsonTransportable(item, propertyPath(path, key), stack);
2796
2830
  }
2797
2831
  stack.delete(value);
2798
2832
  }
2799
2833
 
2834
+ function isPlainJsonObject(value) {
2835
+ const prototype = Object.getPrototypeOf(value);
2836
+ return prototype === Object.prototype || prototype === null;
2837
+ }
2838
+
2839
+ function propertyPath(path, key) {
2840
+ return /^[A-Za-z_$][\w$]*$/.test(key) ? `${path}.${key}` : `${path}[${JSON.stringify(key)}]`;
2841
+ }
2842
+
2800
2843
  function isUnsupportedJsonTransportObject(value, tag = Object.prototype.toString.call(value)) {
2801
2844
  return tag === "[object URLSearchParams]"
2802
2845
  || tag === "[object Headers]"
@@ -5815,101 +5858,25 @@ const __boundaryReceiverModule = (() => {
5815
5858
 
5816
5859
  const normalized = validatePatch(patch);
5817
5860
  const record = boundaryRecord(normalized.boundary);
5818
- if (normalized.seq <= record.lastSeq) {
5819
- const result = {
5820
- status: "ignored-stale",
5821
- boundary: normalized.boundary,
5822
- seq: normalized.seq,
5823
- lastSeq: record.lastSeq
5824
- };
5825
- record.ignored += 1;
5826
- record.lastStatus = result.status;
5827
- remember(result);
5828
- onIgnore?.(result, patch);
5829
- return result;
5830
- }
5831
-
5832
- if (normalized.parentScope !== undefined && isScopeDestroyed(normalized.parentScope)) {
5833
- const result = {
5834
- status: "ignored-destroyed",
5835
- boundary: normalized.boundary,
5836
- seq: normalized.seq,
5837
- parentScope: normalized.parentScope
5838
- };
5839
- record.ignored += 1;
5840
- record.lastStatus = result.status;
5841
- remember(result);
5842
- onIgnore?.(result, patch);
5843
- return result;
5844
- }
5845
-
5846
- record.lastSeq = normalized.seq;
5847
-
5848
- if (Object.hasOwn(normalized, "error")) {
5849
- const error = toStableError(normalized.error);
5850
- const result = {
5851
- status: "errored",
5852
- boundary: normalized.boundary,
5853
- seq: normalized.seq,
5854
- error
5855
- };
5856
- record.errored += 1;
5857
- record.lastStatus = result.status;
5858
- remember(result);
5859
- onError?.(error, result, patch);
5860
- if (throwOnError) {
5861
- throw error;
5862
- }
5863
- return result;
5864
- }
5861
+ let releasePending;
5862
+ const previousPending = record.pending ?? Promise.resolve();
5863
+ const pending = new Promise((resolve) => {
5864
+ releasePending = resolve;
5865
+ });
5866
+ record.pending = pending;
5865
5867
 
5866
- if (normalized.signals) {
5867
- if (!signals || typeof signals.set !== "function") {
5868
- throw new Error("Boundary patch includes signals, but no signal registry is available.");
5869
- }
5870
- for (const [path, value] of Object.entries(normalized.signals)) {
5871
- signals.set(path, value);
5868
+ try {
5869
+ await previousPending;
5870
+ if (destroyed) {
5871
+ throw new Error("Boundary receiver has been destroyed.");
5872
5872
  }
5873
- }
5874
-
5875
- if (normalized.cache?.browser) {
5876
- if (!cache || typeof cache.restore !== "function") {
5877
- throw new Error("Boundary patch includes browser cache, but no cache registry is available.");
5873
+ return await applyBoundaryPatch(record, normalized, patch);
5874
+ } finally {
5875
+ releasePending();
5876
+ if (record.pending === pending) {
5877
+ record.pending = undefined;
5878
5878
  }
5879
- cache.restore(normalized.cache.browser);
5880
5879
  }
5881
-
5882
- if (normalized.html != null) {
5883
- loader.swap(normalized.boundary, normalized.html);
5884
- }
5885
-
5886
- await flushScheduler(scheduler, normalized.scope);
5887
-
5888
- if (normalized.redirect) {
5889
- await followRedirect(normalized.redirect, router, loader);
5890
- const result = {
5891
- status: "redirected",
5892
- boundary: normalized.boundary,
5893
- seq: normalized.seq,
5894
- redirect: normalized.redirect
5895
- };
5896
- record.applied += 1;
5897
- record.lastStatus = result.status;
5898
- remember(result);
5899
- onApply?.(result, patch);
5900
- return result;
5901
- }
5902
-
5903
- const result = {
5904
- status: "applied",
5905
- boundary: normalized.boundary,
5906
- seq: normalized.seq
5907
- };
5908
- record.applied += 1;
5909
- record.lastStatus = result.status;
5910
- remember(result);
5911
- onApply?.(result, patch);
5912
- return result;
5913
5880
  },
5914
5881
 
5915
5882
  inspect() {
@@ -5957,6 +5924,105 @@ const __boundaryReceiverModule = (() => {
5957
5924
 
5958
5925
  return receiver;
5959
5926
 
5927
+ async function applyBoundaryPatch(record, normalized, patch) {
5928
+ if (normalized.seq <= record.lastSeq) {
5929
+ const result = {
5930
+ status: "ignored-stale",
5931
+ boundary: normalized.boundary,
5932
+ seq: normalized.seq,
5933
+ lastSeq: record.lastSeq
5934
+ };
5935
+ record.ignored += 1;
5936
+ record.lastStatus = result.status;
5937
+ remember(result);
5938
+ onIgnore?.(result, patch);
5939
+ return result;
5940
+ }
5941
+
5942
+ if (normalized.parentScope !== undefined && isScopeDestroyed(normalized.parentScope)) {
5943
+ const result = {
5944
+ status: "ignored-destroyed",
5945
+ boundary: normalized.boundary,
5946
+ seq: normalized.seq,
5947
+ parentScope: normalized.parentScope
5948
+ };
5949
+ record.ignored += 1;
5950
+ record.lastStatus = result.status;
5951
+ remember(result);
5952
+ onIgnore?.(result, patch);
5953
+ return result;
5954
+ }
5955
+
5956
+ if (Object.hasOwn(normalized, "error")) {
5957
+ const error = toStableError(normalized.error);
5958
+ const result = {
5959
+ status: "errored",
5960
+ boundary: normalized.boundary,
5961
+ seq: normalized.seq,
5962
+ error
5963
+ };
5964
+ record.lastSeq = normalized.seq;
5965
+ record.errored += 1;
5966
+ record.lastStatus = result.status;
5967
+ remember(result);
5968
+ onError?.(error, result, patch);
5969
+ if (throwOnError) {
5970
+ throw error;
5971
+ }
5972
+ return result;
5973
+ }
5974
+
5975
+ if (normalized.signals) {
5976
+ if (!signals || typeof signals.set !== "function") {
5977
+ throw new Error("Boundary patch includes signals, but no signal registry is available.");
5978
+ }
5979
+ for (const [path, value] of Object.entries(normalized.signals)) {
5980
+ signals.set(path, value);
5981
+ }
5982
+ }
5983
+
5984
+ if (normalized.cache?.browser) {
5985
+ if (!cache || typeof cache.restore !== "function") {
5986
+ throw new Error("Boundary patch includes browser cache, but no cache registry is available.");
5987
+ }
5988
+ cache.restore(normalized.cache.browser);
5989
+ }
5990
+
5991
+ if (normalized.html != null) {
5992
+ loader.swap(normalized.boundary, normalized.html);
5993
+ }
5994
+
5995
+ await flushScheduler(scheduler, normalized.scope);
5996
+
5997
+ if (normalized.redirect) {
5998
+ const result = {
5999
+ status: "redirected",
6000
+ boundary: normalized.boundary,
6001
+ seq: normalized.seq,
6002
+ redirect: normalized.redirect
6003
+ };
6004
+ await followRedirect(normalized.redirect, router, loader);
6005
+ record.applied += 1;
6006
+ record.lastSeq = normalized.seq;
6007
+ record.lastStatus = result.status;
6008
+ remember(result);
6009
+ onApply?.(result, patch);
6010
+ return result;
6011
+ }
6012
+
6013
+ const result = {
6014
+ status: "applied",
6015
+ boundary: normalized.boundary,
6016
+ seq: normalized.seq
6017
+ };
6018
+ record.applied += 1;
6019
+ record.lastSeq = normalized.seq;
6020
+ record.lastStatus = result.status;
6021
+ remember(result);
6022
+ onApply?.(result, patch);
6023
+ return result;
6024
+ }
6025
+
5960
6026
  function boundaryRecord(boundary) {
5961
6027
  if (!boundaries.has(boundary)) {
5962
6028
  boundaries.set(boundary, {
@@ -5964,7 +6030,8 @@ const __boundaryReceiverModule = (() => {
5964
6030
  applied: 0,
5965
6031
  ignored: 0,
5966
6032
  errored: 0,
5967
- lastStatus: undefined
6033
+ lastStatus: undefined,
6034
+ pending: undefined
5968
6035
  });
5969
6036
  }
5970
6037
  return boundaries.get(boundary);