@async/framework 0.11.10 → 0.11.12

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,32 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.11.12 - 2026-06-18
4
+
5
+ - Deferred boundary receiver sequence commits until patch effects complete so
6
+ failed DOM swaps, scheduler flushes, redirects, or missing capabilities no
7
+ longer make the same streamed sequence stale.
8
+ - Serialized same-boundary patch application to keep concurrent patches from
9
+ committing out of order while preserving stale rejection after successful
10
+ commits.
11
+ - Added regression coverage for retryable DOM, scheduler, redirect, and
12
+ capability failures plus idempotent partial-effect replay.
13
+ - Bundle size from bundled TypeScript source: `browser.ts` 186,064 B raw /
14
+ 35,039 B gzip -> `browser.min.js` 79,042 B raw / 23,445 B gzip
15
+ (-107,022 B raw, -11,594 B gzip).
16
+
17
+ ## 0.11.11 - 2026-06-18
18
+
19
+ - Serialized rejected async-signal snapshot errors to stable `name`, `message`,
20
+ and `code` records so JSON SSR snapshots preserve documented
21
+ `$error.message` bindings during browser activation.
22
+ - Normalized non-Error rejections into readable error records while omitting
23
+ arbitrary error object properties from snapshots by default.
24
+ - Added regression coverage for JSON snapshot round-trips, error code
25
+ preservation, non-Error rejection normalization, and SSR activation bindings.
26
+ - Bundle size from bundled TypeScript source: `browser.ts` 185,440 B raw /
27
+ 34,884 B gzip -> `browser.min.js` 78,754 B raw / 23,362 B gzip
28
+ (-106,686 B raw, -11,522 B gzip).
29
+
3
30
  ## 0.11.10 - 2026-06-18
4
31
 
5
32
  - Routed automatic microtask scheduler flush failures through an explicit error
package/browser.js CHANGED
@@ -131,9 +131,9 @@ const __asyncSignalModule = (() => {
131
131
 
132
132
  snapshot() {
133
133
  return {
134
- value,
134
+ value: value === undefined && error !== null ? null : value,
135
135
  loading,
136
- error,
136
+ error: serializeAsyncError(error),
137
137
  status,
138
138
  version
139
139
  };
@@ -150,7 +150,7 @@ const __asyncSignalModule = (() => {
150
150
  cancelCurrentRun(new Error(`Async signal "${registeredId}" restored from snapshot.`));
151
151
  value = snapshot.value;
152
152
  loading = Boolean(snapshot.loading);
153
- error = snapshot.error ?? null;
153
+ error = restoreAsyncError(snapshot.error);
154
154
  status = typeof snapshot.status === "string" ? snapshot.status : inferStatus({ value, loading, error });
155
155
  if (Number.isFinite(snapshot.version)) {
156
156
  version = snapshot.version;
@@ -402,6 +402,51 @@ const __asyncSignalModule = (() => {
402
402
  return value === undefined ? "idle" : "ready";
403
403
  }
404
404
 
405
+ function serializeAsyncError(value) {
406
+ if (value == null) {
407
+ return null;
408
+ }
409
+
410
+ const record = {
411
+ name: readErrorName(value),
412
+ message: readErrorMessage(value)
413
+ };
414
+ const code = readErrorCode(value);
415
+ if (code !== undefined) {
416
+ record.code = code;
417
+ }
418
+ return record;
419
+ }
420
+
421
+ function restoreAsyncError(value) {
422
+ return serializeAsyncError(value);
423
+ }
424
+
425
+ function readErrorName(value) {
426
+ if (value && typeof value === "object" && typeof value.name === "string" && value.name.length > 0) {
427
+ return value.name;
428
+ }
429
+ return "Error";
430
+ }
431
+
432
+ function readErrorMessage(value) {
433
+ if (value instanceof Error) {
434
+ return value.message;
435
+ }
436
+ if (value && typeof value === "object" && typeof value.message === "string") {
437
+ return value.message;
438
+ }
439
+ return String(value);
440
+ }
441
+
442
+ function readErrorCode(value) {
443
+ if (!value || typeof value !== "object" || !Object.hasOwn(value, "code")) {
444
+ return undefined;
445
+ }
446
+ const code = value.code;
447
+ return typeof code === "string" || typeof code === "number" ? code : undefined;
448
+ }
449
+
405
450
  function attachCancel(signal, controller, onCancel) {
406
451
  Object.defineProperty(signal, "cancel", {
407
452
  configurable: true,
@@ -5770,101 +5815,25 @@ const __boundaryReceiverModule = (() => {
5770
5815
 
5771
5816
  const normalized = validatePatch(patch);
5772
5817
  const record = boundaryRecord(normalized.boundary);
5773
- if (normalized.seq <= record.lastSeq) {
5774
- const result = {
5775
- status: "ignored-stale",
5776
- boundary: normalized.boundary,
5777
- seq: normalized.seq,
5778
- lastSeq: record.lastSeq
5779
- };
5780
- record.ignored += 1;
5781
- record.lastStatus = result.status;
5782
- remember(result);
5783
- onIgnore?.(result, patch);
5784
- return result;
5785
- }
5786
-
5787
- if (normalized.parentScope !== undefined && isScopeDestroyed(normalized.parentScope)) {
5788
- const result = {
5789
- status: "ignored-destroyed",
5790
- boundary: normalized.boundary,
5791
- seq: normalized.seq,
5792
- parentScope: normalized.parentScope
5793
- };
5794
- record.ignored += 1;
5795
- record.lastStatus = result.status;
5796
- remember(result);
5797
- onIgnore?.(result, patch);
5798
- return result;
5799
- }
5800
-
5801
- record.lastSeq = normalized.seq;
5802
-
5803
- if (Object.hasOwn(normalized, "error")) {
5804
- const error = toStableError(normalized.error);
5805
- const result = {
5806
- status: "errored",
5807
- boundary: normalized.boundary,
5808
- seq: normalized.seq,
5809
- error
5810
- };
5811
- record.errored += 1;
5812
- record.lastStatus = result.status;
5813
- remember(result);
5814
- onError?.(error, result, patch);
5815
- if (throwOnError) {
5816
- throw error;
5817
- }
5818
- return result;
5819
- }
5818
+ let releasePending;
5819
+ const previousPending = record.pending ?? Promise.resolve();
5820
+ const pending = new Promise((resolve) => {
5821
+ releasePending = resolve;
5822
+ });
5823
+ record.pending = pending;
5820
5824
 
5821
- if (normalized.signals) {
5822
- if (!signals || typeof signals.set !== "function") {
5823
- throw new Error("Boundary patch includes signals, but no signal registry is available.");
5824
- }
5825
- for (const [path, value] of Object.entries(normalized.signals)) {
5826
- signals.set(path, value);
5825
+ try {
5826
+ await previousPending;
5827
+ if (destroyed) {
5828
+ throw new Error("Boundary receiver has been destroyed.");
5827
5829
  }
5828
- }
5829
-
5830
- if (normalized.cache?.browser) {
5831
- if (!cache || typeof cache.restore !== "function") {
5832
- throw new Error("Boundary patch includes browser cache, but no cache registry is available.");
5830
+ return await applyBoundaryPatch(record, normalized, patch);
5831
+ } finally {
5832
+ releasePending();
5833
+ if (record.pending === pending) {
5834
+ record.pending = undefined;
5833
5835
  }
5834
- cache.restore(normalized.cache.browser);
5835
- }
5836
-
5837
- if (normalized.html != null) {
5838
- loader.swap(normalized.boundary, normalized.html);
5839
5836
  }
5840
-
5841
- await flushScheduler(scheduler, normalized.scope);
5842
-
5843
- if (normalized.redirect) {
5844
- await followRedirect(normalized.redirect, router, loader);
5845
- const result = {
5846
- status: "redirected",
5847
- boundary: normalized.boundary,
5848
- seq: normalized.seq,
5849
- redirect: normalized.redirect
5850
- };
5851
- record.applied += 1;
5852
- record.lastStatus = result.status;
5853
- remember(result);
5854
- onApply?.(result, patch);
5855
- return result;
5856
- }
5857
-
5858
- const result = {
5859
- status: "applied",
5860
- boundary: normalized.boundary,
5861
- seq: normalized.seq
5862
- };
5863
- record.applied += 1;
5864
- record.lastStatus = result.status;
5865
- remember(result);
5866
- onApply?.(result, patch);
5867
- return result;
5868
5837
  },
5869
5838
 
5870
5839
  inspect() {
@@ -5912,6 +5881,105 @@ const __boundaryReceiverModule = (() => {
5912
5881
 
5913
5882
  return receiver;
5914
5883
 
5884
+ async function applyBoundaryPatch(record, normalized, patch) {
5885
+ if (normalized.seq <= record.lastSeq) {
5886
+ const result = {
5887
+ status: "ignored-stale",
5888
+ boundary: normalized.boundary,
5889
+ seq: normalized.seq,
5890
+ lastSeq: record.lastSeq
5891
+ };
5892
+ record.ignored += 1;
5893
+ record.lastStatus = result.status;
5894
+ remember(result);
5895
+ onIgnore?.(result, patch);
5896
+ return result;
5897
+ }
5898
+
5899
+ if (normalized.parentScope !== undefined && isScopeDestroyed(normalized.parentScope)) {
5900
+ const result = {
5901
+ status: "ignored-destroyed",
5902
+ boundary: normalized.boundary,
5903
+ seq: normalized.seq,
5904
+ parentScope: normalized.parentScope
5905
+ };
5906
+ record.ignored += 1;
5907
+ record.lastStatus = result.status;
5908
+ remember(result);
5909
+ onIgnore?.(result, patch);
5910
+ return result;
5911
+ }
5912
+
5913
+ if (Object.hasOwn(normalized, "error")) {
5914
+ const error = toStableError(normalized.error);
5915
+ const result = {
5916
+ status: "errored",
5917
+ boundary: normalized.boundary,
5918
+ seq: normalized.seq,
5919
+ error
5920
+ };
5921
+ record.lastSeq = normalized.seq;
5922
+ record.errored += 1;
5923
+ record.lastStatus = result.status;
5924
+ remember(result);
5925
+ onError?.(error, result, patch);
5926
+ if (throwOnError) {
5927
+ throw error;
5928
+ }
5929
+ return result;
5930
+ }
5931
+
5932
+ if (normalized.signals) {
5933
+ if (!signals || typeof signals.set !== "function") {
5934
+ throw new Error("Boundary patch includes signals, but no signal registry is available.");
5935
+ }
5936
+ for (const [path, value] of Object.entries(normalized.signals)) {
5937
+ signals.set(path, value);
5938
+ }
5939
+ }
5940
+
5941
+ if (normalized.cache?.browser) {
5942
+ if (!cache || typeof cache.restore !== "function") {
5943
+ throw new Error("Boundary patch includes browser cache, but no cache registry is available.");
5944
+ }
5945
+ cache.restore(normalized.cache.browser);
5946
+ }
5947
+
5948
+ if (normalized.html != null) {
5949
+ loader.swap(normalized.boundary, normalized.html);
5950
+ }
5951
+
5952
+ await flushScheduler(scheduler, normalized.scope);
5953
+
5954
+ if (normalized.redirect) {
5955
+ const result = {
5956
+ status: "redirected",
5957
+ boundary: normalized.boundary,
5958
+ seq: normalized.seq,
5959
+ redirect: normalized.redirect
5960
+ };
5961
+ await followRedirect(normalized.redirect, router, loader);
5962
+ record.applied += 1;
5963
+ record.lastSeq = normalized.seq;
5964
+ record.lastStatus = result.status;
5965
+ remember(result);
5966
+ onApply?.(result, patch);
5967
+ return result;
5968
+ }
5969
+
5970
+ const result = {
5971
+ status: "applied",
5972
+ boundary: normalized.boundary,
5973
+ seq: normalized.seq
5974
+ };
5975
+ record.applied += 1;
5976
+ record.lastSeq = normalized.seq;
5977
+ record.lastStatus = result.status;
5978
+ remember(result);
5979
+ onApply?.(result, patch);
5980
+ return result;
5981
+ }
5982
+
5915
5983
  function boundaryRecord(boundary) {
5916
5984
  if (!boundaries.has(boundary)) {
5917
5985
  boundaries.set(boundary, {
@@ -5919,7 +5987,8 @@ const __boundaryReceiverModule = (() => {
5919
5987
  applied: 0,
5920
5988
  ignored: 0,
5921
5989
  errored: 0,
5922
- lastStatus: undefined
5990
+ lastStatus: undefined,
5991
+ pending: undefined
5923
5992
  });
5924
5993
  }
5925
5994
  return boundaries.get(boundary);