@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 +29 -0
- package/README.md +8 -0
- package/browser.js +176 -109
- package/browser.min.js +1 -1
- package/browser.ts +176 -109
- package/browser.umd.js +176 -109
- package/browser.umd.min.js +1 -1
- package/framework.ts +176 -109
- package/package.json +1 -1
- package/server.js +176 -109
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
|
-
|
|
2578
|
-
value
|
|
2579
|
-
|
|
2580
|
-
|
|
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 (
|
|
2770
|
-
|
|
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 (
|
|
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(
|
|
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(
|
|
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(
|
|
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 (
|
|
2789
|
-
|
|
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
|
-
|
|
2795
|
-
|
|
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
|
-
|
|
5819
|
-
|
|
5820
|
-
|
|
5821
|
-
|
|
5822
|
-
|
|
5823
|
-
|
|
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
|
-
|
|
5867
|
-
|
|
5868
|
-
|
|
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
|
-
|
|
5876
|
-
if (
|
|
5877
|
-
|
|
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);
|