diff --git a/packages/react-client/src/ReactFlightReplyClient.js b/packages/react-client/src/ReactFlightReplyClient.js index 35c23ed074..049987e392 100644 --- a/packages/react-client/src/ReactFlightReplyClient.js +++ b/packages/react-client/src/ReactFlightReplyClient.js @@ -185,7 +185,7 @@ export function processReply( temporaryReferences: void | TemporaryReferenceSet, resolve: (string | FormData) => void, reject: (error: mixed) => void, -): void { +): (reason: mixed) => void { let nextPartId = 1; let pendingParts = 0; let formData: null | FormData = null; @@ -841,6 +841,19 @@ export function processReply( return JSON.stringify(model, resolveToJSON); } + function abort(reason: mixed): void { + if (pendingParts > 0) { + pendingParts = 0; // Don't resolve again later. + // Resolve with what we have so far, which may have holes at this point. + // They'll error when the stream completes on the server. + if (formData === null) { + resolve(json); + } else { + resolve(formData); + } + } + } + const json = serializeModel(root, 0); if (formData === null) { @@ -854,6 +867,8 @@ export function processReply( resolve(formData); } } + + return abort; } const boundCache: WeakMap< diff --git a/packages/react-native-renderer/src/ReactNativeTypes.js b/packages/react-native-renderer/src/ReactNativeTypes.js index 9692a1256a..03c03cfba0 100644 --- a/packages/react-native-renderer/src/ReactNativeTypes.js +++ b/packages/react-native-renderer/src/ReactNativeTypes.js @@ -112,31 +112,32 @@ export interface INativeMethods { measure(callback: MeasureOnSuccessCallback): void; measureInWindow(callback: MeasureInWindowOnSuccessCallback): void; measureLayout( - relativeToNativeNode: number | ElementRef>, + relativeToNativeNode: number | HostInstance, onSuccess: MeasureLayoutOnSuccessCallback, onFail?: () => void, ): void; setNativeProps(nativeProps: {...}): void; } -export type NativeMethods = $ReadOnly<{| +export type NativeMethods = $ReadOnly<{ blur(): void, focus(): void, measure(callback: MeasureOnSuccessCallback): void, measureInWindow(callback: MeasureInWindowOnSuccessCallback): void, measureLayout( - relativeToNativeNode: number | ElementRef>, + relativeToNativeNode: number | HostInstance, onSuccess: MeasureLayoutOnSuccessCallback, onFail?: () => void, ): void, setNativeProps(nativeProps: {...}): void, -|}>; +}>; // This validates that INativeMethods and NativeMethods stay in sync using Flow! declare const ensureNativeMethodsAreSynced: NativeMethods; (ensureNativeMethodsAreSynced: INativeMethods); -export type HostComponent = AbstractComponent>; +export type HostInstance = NativeMethods; +export type HostComponent = AbstractComponent; type SecretInternalsType = { computeComponentStackForErrorReporting(tag: number): string, @@ -209,7 +210,7 @@ export type RenderRootOptions = { export type ReactNativeType = { findHostInstance_DEPRECATED( componentOrHandle: ?(ElementRef | number), - ): ?ElementRef>, + ): ?HostInstance, findNodeHandle( componentOrHandle: ?(ElementRef | number), ): ?number, @@ -218,14 +219,11 @@ export type ReactNativeType = { child: PublicInstance | HostComponent, ): boolean, dispatchCommand( - handle: ElementRef>, + handle: HostInstance, command: string, args: Array, ): void, - sendAccessibilityEvent( - handle: ElementRef>, - eventType: string, - ): void, + sendAccessibilityEvent(handle: HostInstance, eventType: string): void, render( element: MixedElement, containerTag: number, @@ -247,20 +245,17 @@ type PublicTextInstance = mixed; export type ReactFabricType = { findHostInstance_DEPRECATED( componentOrHandle: ?(ElementRef | number), - ): ?ElementRef>, + ): ?HostInstance, findNodeHandle( componentOrHandle: ?(ElementRef | number), ): ?number, dispatchCommand( - handle: ElementRef>, + handle: HostInstance, command: string, args: Array, ): void, isChildPublicInstance(parent: PublicInstance, child: PublicInstance): boolean, - sendAccessibilityEvent( - handle: ElementRef>, - eventType: string, - ): void, + sendAccessibilityEvent(handle: HostInstance, eventType: string): void, render( element: MixedElement, containerTag: number, diff --git a/packages/react-server-dom-esm/src/client/ReactFlightDOMClientBrowser.js b/packages/react-server-dom-esm/src/client/ReactFlightDOMClientBrowser.js index 58a36e7023..abaa793c96 100644 --- a/packages/react-server-dom-esm/src/client/ReactFlightDOMClientBrowser.js +++ b/packages/react-server-dom-esm/src/client/ReactFlightDOMClientBrowser.js @@ -121,12 +121,12 @@ function createFromFetch( function encodeReply( value: ReactServerValue, - options?: {temporaryReferences?: TemporaryReferenceSet}, + options?: {temporaryReferences?: TemporaryReferenceSet, signal?: AbortSignal}, ): Promise< string | URLSearchParams | FormData, > /* We don't use URLSearchParams yet but maybe */ { return new Promise((resolve, reject) => { - processReply( + const abort = processReply( value, '', options && options.temporaryReferences @@ -135,6 +135,18 @@ function encodeReply( resolve, reject, ); + if (options && options.signal) { + const signal = options.signal; + if (signal.aborted) { + abort((signal: any).reason); + } else { + const listener = () => { + abort((signal: any).reason); + signal.removeEventListener('abort', listener); + }; + signal.addEventListener('abort', listener); + } + } }); } diff --git a/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientBrowser.js b/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientBrowser.js index 50a4a206ff..0d566a57ca 100644 --- a/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientBrowser.js +++ b/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientBrowser.js @@ -120,12 +120,12 @@ function createFromFetch( function encodeReply( value: ReactServerValue, - options?: {temporaryReferences?: TemporaryReferenceSet}, + options?: {temporaryReferences?: TemporaryReferenceSet, signal?: AbortSignal}, ): Promise< string | URLSearchParams | FormData, > /* We don't use URLSearchParams yet but maybe */ { return new Promise((resolve, reject) => { - processReply( + const abort = processReply( value, '', options && options.temporaryReferences @@ -134,6 +134,18 @@ function encodeReply( resolve, reject, ); + if (options && options.signal) { + const signal = options.signal; + if (signal.aborted) { + abort((signal: any).reason); + } else { + const listener = () => { + abort((signal: any).reason); + signal.removeEventListener('abort', listener); + }; + signal.addEventListener('abort', listener); + } + } }); } diff --git a/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientEdge.js b/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientEdge.js index 5b3a765783..956e014042 100644 --- a/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientEdge.js +++ b/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientEdge.js @@ -149,12 +149,12 @@ function createFromFetch( function encodeReply( value: ReactServerValue, - options?: {temporaryReferences?: TemporaryReferenceSet}, + options?: {temporaryReferences?: TemporaryReferenceSet, signal?: AbortSignal}, ): Promise< string | URLSearchParams | FormData, > /* We don't use URLSearchParams yet but maybe */ { return new Promise((resolve, reject) => { - processReply( + const abort = processReply( value, '', options && options.temporaryReferences @@ -163,6 +163,18 @@ function encodeReply( resolve, reject, ); + if (options && options.signal) { + const signal = options.signal; + if (signal.aborted) { + abort((signal: any).reason); + } else { + const listener = () => { + abort((signal: any).reason); + signal.removeEventListener('abort', listener); + }; + signal.addEventListener('abort', listener); + } + } }); } diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReply-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReply-test.js index 30aa539e5a..64a0616cac 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReply-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReply-test.js @@ -618,4 +618,20 @@ describe('ReactFlightDOMReply', () => { const root = await ReactServerDOMServer.decodeReply(body, webpackServerMap); expect(root.prop.obj).toBe(root.prop); }); + + it('can abort an unresolved model and get the partial result', async () => { + const promise = new Promise(r => {}); + const controller = new AbortController(); + const bodyPromise = ReactServerDOMClient.encodeReply( + {promise: promise, hello: 'world'}, + {signal: controller.signal}, + ); + controller.abort(); + + const result = await ReactServerDOMServer.decodeReply(await bodyPromise); + expect(result.hello).toBe('world'); + // TODO: await result.promise should reject at this point because the stream + // has closed but that's a bug in both ReactFlightReplyServer and ReactFlightClient. + // It just halts in this case. + }); }); diff --git a/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientBrowser.js b/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientBrowser.js index 50a4a206ff..0d566a57ca 100644 --- a/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientBrowser.js +++ b/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientBrowser.js @@ -120,12 +120,12 @@ function createFromFetch( function encodeReply( value: ReactServerValue, - options?: {temporaryReferences?: TemporaryReferenceSet}, + options?: {temporaryReferences?: TemporaryReferenceSet, signal?: AbortSignal}, ): Promise< string | URLSearchParams | FormData, > /* We don't use URLSearchParams yet but maybe */ { return new Promise((resolve, reject) => { - processReply( + const abort = processReply( value, '', options && options.temporaryReferences @@ -134,6 +134,18 @@ function encodeReply( resolve, reject, ); + if (options && options.signal) { + const signal = options.signal; + if (signal.aborted) { + abort((signal: any).reason); + } else { + const listener = () => { + abort((signal: any).reason); + signal.removeEventListener('abort', listener); + }; + signal.addEventListener('abort', listener); + } + } }); } diff --git a/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientEdge.js b/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientEdge.js index 5b3a765783..956e014042 100644 --- a/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientEdge.js +++ b/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientEdge.js @@ -149,12 +149,12 @@ function createFromFetch( function encodeReply( value: ReactServerValue, - options?: {temporaryReferences?: TemporaryReferenceSet}, + options?: {temporaryReferences?: TemporaryReferenceSet, signal?: AbortSignal}, ): Promise< string | URLSearchParams | FormData, > /* We don't use URLSearchParams yet but maybe */ { return new Promise((resolve, reject) => { - processReply( + const abort = processReply( value, '', options && options.temporaryReferences @@ -163,6 +163,18 @@ function encodeReply( resolve, reject, ); + if (options && options.signal) { + const signal = options.signal; + if (signal.aborted) { + abort((signal: any).reason); + } else { + const listener = () => { + abort((signal: any).reason); + signal.removeEventListener('abort', listener); + }; + signal.addEventListener('abort', listener); + } + } }); }