diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts index 94be2100f57..b894eb28986 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts @@ -954,6 +954,7 @@ function applyEffect( case ValueKind.Primitive: { break; } + case ValueKind.MaybeFrozen: case ValueKind.Frozen: { sourceType = 'frozen'; break; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-destructure-from-prop-with-default-value.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-destructure-from-prop-with-default-value.expect.md new file mode 100644 index 00000000000..eec95683aa2 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-destructure-from-prop-with-default-value.expect.md @@ -0,0 +1,45 @@ + +## Input + +```javascript +export function useFormatRelativeTime(opts = {}) { + const {timeZone, minimal} = opts; + const format = useCallback(function formatWithUnit() {}, [minimal]); + // We previously recorded `{timeZone}` as capturing timeZone into the object, + // then assumed that dateTimeFormat() mutates that object, + // which in turn could mutate timeZone and the object it came from, + // which meanteans that the value `minimal` is derived from can change. + // + // The fix was to record a Capture from a maybefrozen value as an ImmutableCapture + // which doesn't propagate mutations + dateTimeFormat({timeZone}); + return format; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +export function useFormatRelativeTime(t0) { + const $ = _c(1); + const opts = t0 === undefined ? {} : t0; + const { timeZone, minimal } = opts; + let t1; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t1 = function formatWithUnit() {}; + $[0] = t1; + } else { + t1 = $[0]; + } + const format = t1; + + dateTimeFormat({ timeZone }); + return format; +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-destructure-from-prop-with-default-value.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-destructure-from-prop-with-default-value.js new file mode 100644 index 00000000000..dbcb7303778 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-destructure-from-prop-with-default-value.js @@ -0,0 +1,13 @@ +export function useFormatRelativeTime(opts = {}) { + const {timeZone, minimal} = opts; + const format = useCallback(function formatWithUnit() {}, [minimal]); + // We previously recorded `{timeZone}` as capturing timeZone into the object, + // then assumed that dateTimeFormat() mutates that object, + // which in turn could mutate timeZone and the object it came from, + // which meanteans that the value `minimal` is derived from can change. + // + // The fix was to record a Capture from a maybefrozen value as an ImmutableCapture + // which doesn't propagate mutations + dateTimeFormat({timeZone}); + return format; +} diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index cc0361e6b8d..49da30ccf4c 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -813,6 +813,12 @@ function createInitializedStreamChunk< value: T, controller: FlightStreamController, ): InitializedChunk { + if (__DEV__) { + // Retain a strong reference to the Response while we wait for chunks. + if (response._pendingChunks++ === 0) { + response._weakResponse.response = response; + } + } // We use the reason field to stash the controller since we already have that // field. It's a bit of a hack but efficient. // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors @@ -3075,7 +3081,6 @@ function resolveStream>( // We already resolved. We didn't expect to see this. return; } - releasePendingChunk(response, chunk); const resolveListeners = chunk.value; @@ -3375,6 +3380,14 @@ function stopStream( // We didn't expect not to have an existing stream; return; } + if (__DEV__) { + if (--response._pendingChunks === 0) { + // We're no longer waiting for any more chunks. We can release the strong + // reference to the response. We'll regain it if we ask for any more data + // later on. + response._weakResponse.response = null; + } + } const streamChunk: InitializedStreamChunk = (chunk: any); const controller = streamChunk.reason; controller.close(row === '' ? '"$undefined"' : row);