Skip to content

Commit

Permalink
feat: import modules sequentially when loaded by jest+esm to prevent …
Browse files Browse the repository at this point in the history
  • Loading branch information
ephys committed Oct 19, 2021
1 parent f517cbd commit 32f53f0
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 38 deletions.
21 changes: 9 additions & 12 deletions packages/core/src/dependency-injector.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import assert from 'assert';
import { awaitAllEntries, isPlainObject, mapObject } from '@stilt/util';
import { awaitMapAllEntries, isPlainObject, FORCE_SEQUENTIAL_MODULE_IMPORT } from '@stilt/util';
import type { Factory } from './factory';
import { isFactory } from './factory.js';
import type { TOptionalLazy } from './lazy';
Expand Down Expand Up @@ -65,6 +65,7 @@ export default class DependencyInjector {
// dependencies: { myService: lazy(() => MyService) }
| { [key: string]: TOptionalLazy<TInstantiable<T>> },
): Promise<T | T[] | { [key: string]: T }> {
// @ts-expect-error
return this._getInstances(moduleFactory, []);
}

Expand All @@ -88,21 +89,17 @@ export default class DependencyInjector {
return this._getInstance<T>(moduleFactory, dependencyChain);
}

if (Array.isArray(moduleFactory)) {
return Promise.all(
moduleFactory.map(async ClassItem => this._getInstance(ClassItem, dependencyChain)),
);
}

if (typeof moduleFactory === 'object' && isPlainObject(moduleFactory)) {
return awaitAllEntries(mapObject(moduleFactory, async (aClass: TOptionalLazy<TInstantiable<T>>, key: string) => {
if (Array.isArray(moduleFactory) || typeof moduleFactory === 'object' && isPlainObject(moduleFactory)) {
// @ts-expect-error
return awaitMapAllEntries(moduleFactory, async (ClassItem, key) => {
try {
return await this._getInstance<T>(aClass, dependencyChain);
// @ts-expect-error
return await this._getInstance<T>(ClassItem, dependencyChain);
} catch (e) {
// TODO: use .causedBy
// TODO: use { cause: e }
throw new Error(`Failed to build ${key}: \n ${e.message}`);
}
}));
}, FORCE_SEQUENTIAL_MODULE_IMPORT);
}

assert(typeof moduleFactory === 'function');
Expand Down
4 changes: 2 additions & 2 deletions packages/graphql/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import path from 'path';
import type { InjectableIdentifier, TRunnable } from '@stilt/core';
import { App, factory, isRunnable, runnable } from '@stilt/core';
import { StiltHttp } from '@stilt/http';
import { asyncGlob, coalesce } from '@stilt/util';
import { asyncGlob, awaitMapAllEntries, coalesce, FORCE_SEQUENTIAL_MODULE_IMPORT } from '@stilt/util';
import type {
GraphQLNamedType,
Source, DocumentNode,
Expand Down Expand Up @@ -207,7 +207,7 @@ export class StiltGraphQl {
* - A GraphQL type (eg. a GraphQL enum, any export)
*/

const resolverExports = await Promise.all(resolverFiles.map(readOrRequireFile));
const resolverExports = await awaitMapAllEntries(resolverFiles, readOrRequireFile, FORCE_SEQUENTIAL_MODULE_IMPORT);

const resolverInstancePromises = [];
for (const resolverExport of resolverExports) {
Expand Down
34 changes: 15 additions & 19 deletions packages/rest/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { App, factory, isRunnable, runnable } from '@stilt/core';
import type { Class } from '@stilt/core/types/typing';
import { StiltHttp } from '@stilt/http';
import { wrapControllerWithInjectors } from '@stilt/http/dist/controllerInjectors.js';
import { asyncGlob } from '@stilt/util';
import { asyncGlob, awaitMapAllEntries, FORCE_SEQUENTIAL_MODULE_IMPORT } from '@stilt/util';
import { getRoutingMetadata } from './HttpMethodsDecorators.js';
import { IsRestError } from './RestError.js';

Expand Down Expand Up @@ -145,28 +145,24 @@ export class StiltRest {
private static async loadControllers(app: App, schemaGlob: string) {
const controllers = await asyncGlob(schemaGlob);

const apiClasses = (await Promise.all(
Object.values(controllers).map(async controllerPath => {
const controllerModule = await import(controllerPath);
const controllerClass = controllerModule.default;
const apiClasses = (await awaitMapAllEntries(Object.values(controllers), async controllerPath => {
const controllerModule = await import(controllerPath);
const controllerClass = controllerModule.default;

if (controllerClass == null || (typeof controllerClass !== 'function' && typeof controllerClass !== 'object')) {
return null;
}
if (controllerClass == null || (typeof controllerClass !== 'function' && typeof controllerClass !== 'object')) {
return null;
}

return controllerClass;
}),
)).filter(controllerClass => controllerClass != null);
return controllerClass;
}, FORCE_SEQUENTIAL_MODULE_IMPORT)).filter(controllerClass => controllerClass != null);

const apiInstances = await Promise.all(
apiClasses.map(async resolverClass => {
if (typeof resolverClass === 'function') {
return app.instantiate(resolverClass);
}
const apiInstances = (await awaitMapAllEntries(apiClasses, async resolverClass => {
if (typeof resolverClass === 'function') {
return app.instantiate(resolverClass);
}

return null;
}),
);
return null;
}, FORCE_SEQUENTIAL_MODULE_IMPORT)).filter(controllerInstance => controllerInstance != null);

const routeHandlers = [...apiClasses, ...apiInstances];

Expand Down
82 changes: 77 additions & 5 deletions packages/util/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export function hasOwnProperty<X extends {}, Y extends PropertyKey>(
return Object.prototype.hasOwnProperty.call(obj, propertyKey);
}

export function isPlainObject(obj: any): obj is Object {
export function isPlainObject(obj: any): obj is object {
const proto = Object.getPrototypeOf(obj);

return proto == null || proto === Object.prototype;
Expand Down Expand Up @@ -56,9 +56,16 @@ export function coalesce<T>(...args: T[]): T {
}

type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

export async function awaitAllEntries<T extends { [key: string]: any },
>(obj: T): Promise<{ [P in keyof T]: UnwrapPromise<T[P]> }> {
type MaybePromise<T> = Promise<T> | T;

// eslint-disable-next-line max-len
export async function awaitAllEntries<In, T extends { [key: string]: MaybePromise<In> }>(obj: T): Promise<{ [P in keyof T]: UnwrapPromise<T[P]> }>;
export async function awaitAllEntries<In>(obj: Array<MaybePromise<In>>): Promise<In[]>;
// eslint-disable-next-line max-len
export async function awaitAllEntries<In, T extends ({ [key: string]: MaybePromise<In> })>(obj: T | Array<MaybePromise<In>>): Promise<{ [P in keyof T]: UnwrapPromise<T[P]> } | In[]> {
if (Array.isArray(obj)) {
return Promise.all(obj);
}

const values = await Promise.all(Object.values(obj));
const keys = Object.keys(obj);
Expand All @@ -73,7 +80,7 @@ export async function awaitAllEntries<T extends { [key: string]: any },
return resolvedObject;
}

export function mapObject<In, Out, T extends { [key: string]: In }>(
export function mapObject<In, Out, T>(
obj: T,
callback: (value: In, key: string) => Out,
): { [P in keyof T]: Out } {
Expand All @@ -88,6 +95,71 @@ export function mapObject<In, Out, T extends { [key: string]: In }>(
return newObject;
}

export function mapEntries<In, Out, T extends { [key: string]: In }>(
obj: T,
callback: (value: In, key: string | number) => Out
): { [P in keyof T]: Out };

export function mapEntries<In, Out>(
obj: In[],
callback: (value: In, key: string | number) => Out
): Out[];

export function mapEntries<In, Out, T extends ({ [key: string]: In })>(
obj: T | In[],
callback: (value: In, key: string | number) => Out): { [P in keyof T]: Out } | Out[] {
// process.env.JEST_WORKER_ID
if (Array.isArray(obj)) {
return obj.map((value, key) => callback(value, key));
}

return mapObject(obj, callback);
}

// eslint-disable-next-line max-len
export async function awaitMapAllEntries<In, Out, T extends { [key: string]: In }>(obj: T, callback: (value: In, key: string | number) => MaybePromise<Out>, sequential?: boolean): Promise<{ [P in keyof T]: Out }>;
// eslint-disable-next-line max-len
export async function awaitMapAllEntries<In, Out>(obj: In[], callback: (value: In, key: string | number) => MaybePromise<Out>, sequential?: boolean): Promise<Out[]>;
// eslint-disable-next-line max-len
export async function awaitMapAllEntries<In, Out, T extends ({ [key: string]: In })>(
obj: T | In[],
callback: (value: In, key: string | number) => MaybePromise<Out>,
sequential?: boolean): Promise<{ [P in keyof T]: Out } | Out[]> {

// `sequential` option is a workaround due to a bug when using Jest with native ES Modules
// See https://github.com/facebook/jest/issues/11434
if (sequential) {
if (Array.isArray(obj)) {
const out = [];

for (let i = 0; i < obj.length; i++){
// eslint-disable-next-line no-await-in-loop
out.push(await callback(obj[i], i));
}

return out;
}

const out = {};
for (const key of Object.keys(obj)) {
// eslint-disable-next-line no-await-in-loop
out[key] = await callback(obj[key], key);
}

// @ts-expect-error
return out;
}

// @ts-expect-error
const mapped = mapEntries(obj, callback);
const out = awaitAllEntries(mapped);

// @ts-expect-error
return out;
}

export function assertIsFunction(item: any): asserts item is Function {
assert(typeof item === 'function');
}

export const FORCE_SEQUENTIAL_MODULE_IMPORT = process.env.JEST_WORKER_ID != null;

0 comments on commit 32f53f0

Please sign in to comment.