Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(react-native): import "node:events" lazily #1858

Merged
merged 2 commits into from
Nov 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 65 additions & 39 deletions src/node/SetupServerApi.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { setMaxListeners, defaultMaxListeners } from 'node:events'
import { invariant } from 'outvariant'
import {
BatchInterceptor,
Expand All @@ -13,7 +12,7 @@ import { RequiredDeep } from '~/core/typeUtils'
import { mergeRight } from '~/core/utils/internal/mergeRight'
import { handleRequest } from '~/core/utils/handleRequest'
import { devUtils } from '~/core/utils/internal/devUtils'
import { SetupServer } from './glossary'
import { SetupServer, SetupServerInternalContext } from './glossary'
import { isNodeExceptionLike } from './utils/isNodeExceptionLike'

const DEFAULT_LISTEN_OPTIONS: RequiredDeep<SharedOptions> = {
Expand All @@ -24,6 +23,7 @@ export class SetupServerApi
extends SetupApi<LifeCycleEventsMap>
implements SetupServer
{
private context: SetupServerInternalContext
protected readonly interceptor: BatchInterceptor<
Array<Interceptor<HttpRequestEventMap>>,
HttpRequestEventMap
Expand All @@ -38,6 +38,7 @@ export class SetupServerApi
) {
super(...handlers)

this.context = this.createContext()
this.interceptor = new BatchInterceptor({
name: 'setup-server',
interceptors: interceptors.map((Interceptor) => new Interceptor()),
Expand All @@ -47,48 +48,22 @@ export class SetupServerApi
this.init()
}

private createContext(): SetupServerInternalContext {
return {
get nodeEvents() {
return import('node:events')
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this Promise should only ever fire once. I'm not sure if I'm structuring this correctly but the intention is to try to import this module once so each outgoing request (request listener) doesn't pend on this import, increasing the response time).

.then((events) => events)
.catch(() => undefined)
},
}
}

/**
* Subscribe to all requests that are using the interceptor object
*/
private init(): void {
this.interceptor.on('request', async ({ request, requestId }) => {
/**
* @note React Native doesn't have "node:events".
*/
if (typeof setMaxListeners === 'function') {
// Bump the maximum number of event listeners on the
// request's "AbortSignal". This prepares the request
// for each request handler cloning it at least once.
// Note that cloning a request automatically appends a
// new "abort" event listener to the parent request's
// "AbortController" so if the parent aborts, all the
// clones are automatically aborted.
try {
setMaxListeners(
Math.max(defaultMaxListeners, this.currentHandlers.length),
request.signal,
)
} catch (error: unknown) {
/**
* @note Mock environments (JSDOM, ...) are not able to implement an internal
* "kIsNodeEventTarget" Symbol that Node.js uses to identify Node.js `EventTarget`s.
* `setMaxListeners` throws an error for non-Node.js `EventTarget`s.
* At the same time, mock environments are also not able to implement the
* internal "events.maxEventTargetListenersWarned" Symbol, which results in
* "MaxListenersExceededWarning" not being printed by Node.js for those anyway.
* The main reason for using `setMaxListeners` is to suppress these warnings in Node.js,
* which won't be printed anyway if `setMaxListeners` fails.
*/
if (
!(
isNodeExceptionLike(error) &&
error.code === 'ERR_INVALID_ARG_TYPE'
)
) {
throw error
}
}
}
await this.setRequestAbortSignalMaxListeners(request)

const response = await handleRequest(
request,
Expand Down Expand Up @@ -150,4 +125,55 @@ export class SetupServerApi
public close(): void {
this.dispose()
}

/**
* Bump the maximum number of event listeners on the
* request's "AbortSignal". This prepares the request
* for each request handler cloning it at least once.
* Note that cloning a request automatically appends a
* new "abort" event listener to the parent request's
* "AbortController" so if the parent aborts, all the
* clones are automatically aborted.
*/
private async setRequestAbortSignalMaxListeners(
request: Request,
): Promise<void> {
const events = await this.context.nodeEvents

/**
* @note React Native doesn't support "node:events".
*/
if (typeof events === 'undefined') {
return
}

const { setMaxListeners, defaultMaxListeners } = events

if (typeof setMaxListeners !== 'function') {
return
}

try {
setMaxListeners(
Math.max(defaultMaxListeners, this.currentHandlers.length),
request.signal,
)
} catch (error: unknown) {
/**
* @note Mock environments (JSDOM, ...) are not able to implement an internal
* "kIsNodeEventTarget" Symbol that Node.js uses to identify Node.js `EventTarget`s.
* `setMaxListeners` throws an error for non-Node.js `EventTarget`s.
* At the same time, mock environments are also not able to implement the
* internal "events.maxEventTargetListenersWarned" Symbol, which results in
* "MaxListenersExceededWarning" not being printed by Node.js for those anyway.
* The main reason for using `setMaxListeners` is to suppress these warnings in Node.js,
* which won't be printed anyway if `setMaxListeners` fails.
*/
if (
!(isNodeExceptionLike(error) && error.code === 'ERR_INVALID_ARG_TYPE')
) {
throw error
}
}
}
}
4 changes: 4 additions & 0 deletions src/node/glossary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,7 @@ export interface SetupServer {
*/
events: LifeCycleEventEmitter<LifeCycleEventsMap>
}

export type SetupServerInternalContext = {
get nodeEvents(): Promise<typeof import('node:events') | undefined>
}