feat(core): resolve eager dependencies independently of their binding order
MODIFY
packages/core/src/context/context.factory.ts
packages/core/src/context/context.factory.ts
@@ -47,20 +47,31 @@ const isLazyDependency = (x: any): x is ContextLazyReader => {
export const createContext = () => M.empty;
-export const register = <T>(boundDependency: BoundDependency<T, ContextDependency>) => async (context: Context): Promise<Context> =>
+export const register = <T>(boundDependency: BoundDependency<T, ContextDependency>) => (context: Context): Context =>
M.insertAt(setoidContextToken)(
boundDependency.token,
- isEagerDependency(boundDependency.dependency)
- ? await Promise.resolve(boundDependency.dependency.eval(context))
- : boundDependency.dependency,
+ boundDependency.dependency,
)(context);
-export const registerAll = (boundDependencies: BoundDependency<any, any>[]) => async (context: Context): Promise<Context> =>
+export const registerAll = (boundDependencies: BoundDependency<any, any>[]) => (context: Context): Context =>
boundDependencies.reduce(
- async (ctx, dependency) => await register(dependency)(await ctx),
- Promise.resolve(context),
+ (ctx, dependency) => register(dependency)(ctx),
+ context,
);
+export const resolve = async (context: Context): Promise<Context> => {
+ for (const [token, dependency] of context) {
+ context.set(
+ token,
+ isEagerDependency(dependency)
+ ? await Promise.resolve(dependency.eval(context))
+ : dependency,
+ );
+ }
+
+ return context;
+}
+
export const lookup = (context: Context) => <T>(token: ContextToken<T>): O.Option<T> => pipe(
M.lookup(ordContextToken)(token, context),
O.map(dependency => {

MODIFY
packages/core/src/context/specs/context.factory.spec.ts
packages/core/src/context/specs/context.factory.spec.ts
import { isEmpty, size } from 'fp-ts/lib/Map';
import { pipe } from 'fp-ts/lib/pipeable';
+import { flow } from 'fp-ts/lib/function';
import * as R from 'fp-ts/lib/Reader';
import * as O from 'fp-ts/lib/Option';
import {
@@ -14,6 +15,7 @@ import {
Context,
ContextReader,
ContextReaderTag,
+ resolve,
} from '../context.factory';
import { createContextToken } from '../context.token.factory';
@@ -56,27 +58,14 @@ describe('#createContext', () => {
});
describe('#register', () => {
- test('registers bound sync reader', async () => {
+ test('registers bound reader', () => {
// given
const token = createContextToken();
const dependency = pipe(R.ask<Context>(), R.map(_ => 'test'));
const boundDependency = bindTo(token)(dependency);
// when
- const context = await register(boundDependency)(createContext());
-
- // then
- expect(lookup(context)(token)).toEqual(O.some('test'));
- });
-
- test('registers bound async readers', async () => {
- // given
- const token = createContextToken();
- const dependency = pipe(R.ask<Context>(), R.map(_ => Promise.resolve('test')));
- const boundDependency = bindEagerlyTo(token)(dependency);
-
- // when
- const context = await register(boundDependency)(createContext());
+ const context = register(boundDependency)(createContext());
// then
expect(lookup(context)(token)).toEqual(O.some('test'));
@@ -84,7 +73,7 @@ describe('#register', () => {
});
describe('#registerAll', () => {
- test('registers set of bound readers', async () => {
+ test('registers set of bound readers', () => {
// given
const token1 = createContextToken<string>();
const token2 = createContextToken<string>();
@@ -94,7 +83,7 @@ describe('#registerAll', () => {
const dependency3 = pipe(R.ask<Context>(), R.map(_ => 'test_3'));
// when
- const context = await registerAll([
+ const context = registerAll([
bindTo(token1)(dependency1),
bindTo(token2)(dependency2),
bindTo(token3)(dependency3),
@@ -106,6 +95,45 @@ describe('#registerAll', () => {
});
});
+describe('#resolve', () => {
+ test('resolves asynchronous eager reader', async () => {
+ // given
+ const token1 = createContextToken();
+ const token2 = createContextToken();
+
+ // reader dependency
+ const dependency1 = pipe(R.ask<Context>(), R.map(async _ => 'test_1'));
+
+ // factory function
+ const dependency2 = async () => 'test_2';
+
+ // when
+ const context = registerAll([
+ bindEagerlyTo(token1)(dependency1),
+ bindEagerlyTo(token2)(dependency2),
+ ])(createContext());
+ const resolvedContext = await resolve(context);
+
+ // then
+ expect(lookup(resolvedContext)(token1)).toEqual(O.some('test_1'));
+ expect(lookup(resolvedContext)(token2)).toEqual(O.some('test_2'));
+ });
+
+ test('resolves synchronous eager reader', async () => {
+ // given
+ const token = createContextToken();
+ const dependency = pipe(R.ask<Context>(), R.map(_ => 'test'));
+ const boundDependency = bindEagerlyTo(token)(dependency);
+
+ // when
+ const context = register(boundDependency)(createContext());
+ const resolvedContext = await resolve(context);
+
+ // then
+ expect(lookup(resolvedContext)(token)).toEqual(O.some('test'));
+ });
+});
+
describe('#reader', () => {
test('asks context for a lazy reader dependency', async () => {
// given
@@ -119,23 +147,29 @@ describe('#reader', () => {
));
// when ordered
- const context1 = await registerAll([
+ const context1 = await flow(
+ registerAll([
bindTo(token1)(dependency1),
bindTo(token2)(dependency2),
- ])(createContext());
+ ]),
+ resolve,
+ )(createContext());
// when reordered
- const context2 = await registerAll([
+ const context2 = await flow(
+ registerAll([
bindTo(token2)(dependency2),
bindTo(token1)(dependency1),
- ])(createContext());
+ ]),
+ resolve,
+ )(createContext());
// then
expect(lookup(context1)(token2)).toEqual(O.some('test_1_2'));
expect(lookup(context2)(token2)).toEqual(O.some('test_1_2'));
});
- test('asks context for a eager reader dependency', async () => {
+ test('asks context for a eager reader dependency (the binding order doesn\'t matter)', async () => {
// given
const executionOrder: number[] = [];
const spy1 = jest.fn(() => executionOrder.push(1));
@@ -161,11 +195,14 @@ describe('#reader', () => {
}));
// when
- const context = await registerAll([
- bindLazilyTo(token1)(dependency1),
+ const context = await flow(
+ registerAll([
bindEagerlyTo(token2)(dependency2),
+ bindLazilyTo(token1)(dependency1),
bindLazilyTo(token3)(dependency3),
- ])(createContext());
+ ]),
+ resolve,
+ )(createContext());
// then
expect(lookup(context)(token1)).toEqual(O.some('test'));
@@ -181,9 +218,12 @@ describe('#reader', () => {
const dependency = pipe(reader, R.map(() => { spy(); return 'test'; }));
// when
- const context = await registerAll([
+ const context = await flow(
+ registerAll([
bindTo(token)(dependency),
- ])(createContext());
+ ]),
+ resolve,
+ )(createContext());
// then
expect(lookup(context)(token)).toEqual(O.some('test'));

MODIFY
packages/core/src/context/specs/context.hook.spec.ts
packages/core/src/context/specs/context.hook.spec.ts
@@ -7,14 +7,14 @@ describe('#useContext', () => {
jest.spyOn(console, 'error').mockImplementation();
});
- test('injects bound dependency', async () => {
+ test('injects bound dependency', () => {
// given
const token1 = createContextToken('token_1');
const token2 = createContextToken('token_2');
const dependency1 = 'dependency_1';
const dependency2 = 'dependency_2';
- const context = await registerAll([
+ const context = registerAll([
bindTo(token1)(() => dependency1),
bindTo(token2)(() => dependency2),
])(createContext());
@@ -30,13 +30,13 @@ describe('#useContext', () => {
expect(resolvedDependency2).toEqual(dependency2);
});
- test('throws an error if dependency is not bound', async () => {
+ test('throws an error if dependency is not bound', () => {
// given
const token1 = createContextToken('token_1');
const token2 = createContextToken('token_2');
const dependency1 = 'dependency_1';
- const context = await registerAll([
+ const context = registerAll([
bindTo(token1)(() => dependency1),
])(createContext());

MODIFY
packages/core/src/http/server/http.server.ts
packages/core/src/http/server/http.server.ts
import * as http from 'http';
import * as https from 'https';
-import { createContext, lookup, registerAll, bindTo } from '../../context/context.factory';
+import { flow } from 'fp-ts/lib/function';
+import { createContext, lookup, registerAll, bindTo, resolve } from '../../context/context.factory';
import { createEffectContext } from '../../effects/effectsContext.factory';
import { insertIf, isTestingMetadataOn } from '../../+internal';
import { subscribeServerEvents } from './http.server.event.subscriber';
@@ -20,12 +21,15 @@ export const createServer = async (config: CreateServerConfig): Promise<Server>
const boundServer = bindTo(ServerClientToken)(() => server);
const boundServerRequestMetadataStorage = bindTo(ServerRequestMetadataStorageToken)(serverRequestMetadataStorage);
- const context = await registerAll([
+ const context = await flow(
+ registerAll([
boundServer,
boundServerEvent$,
...insertIf(isTestingMetadataOn(), boundServerRequestMetadataStorage),
...dependencies,
- ])(createContext());
+ ]),
+ resolve,
+ )(createContext());
const listener = httpListener(context);

MODIFY
packages/core/src/http/server/specs/http.server.listener.spec.ts
packages/core/src/http/server/specs/http.server.listener.spec.ts
@@ -11,8 +11,8 @@ describe('Http listener', () => {
let responseHandler;
let context: Context;
- beforeAll(async () => {
- context = await registerAll([
+ beforeAll(() => {
+ context = registerAll([
bindTo(ServerClientToken)(() => http.createServer()),
])(createContext());
});

MODIFY
packages/messaging/src/server/messaging.server.ts
packages/messaging/src/server/messaging.server.ts
import { Subject } from 'rxjs';
import { takeWhile, takeUntil, take } from 'rxjs/operators';
-import { createContext, registerAll, bindTo, createEffectContext, lookup, combineEffects } from '@marblejs/core';
+import { flow } from 'fp-ts/lib/function';
+import { createContext, registerAll, bindTo, createEffectContext, lookup, combineEffects, resolve } from '@marblejs/core';
import { provideTransportLayer } from '../transport/transport.provider';
import { statusLogger$ } from '../middlewares/messaging.statusLogger.middleware';
import { CreateMicroserviceConfig, Microservice } from './messaging.server.interface';
@@ -20,9 +21,17 @@ export const createMicroservice = async (config: CreateMicroserviceConfig): Prom
const transportLayer = provideTransportLayer(transport, options);
const boundTransportLayer = bindTo(TransportLayerToken)(() => transportLayer);
const boundServerEvents = bindTo(ServerEventsToken)(() => serverEventsSubject);
- const context = await registerAll([ boundTransportLayer, boundServerEvents, ...dependencies ])(createContext());
- const listener = messagingListener(context);
+ const context = await flow(
+ registerAll([
+ boundTransportLayer,
+ boundServerEvents,
+ ...dependencies,
+ ]),
+ resolve,
+ )(createContext());
+
+ const listener = messagingListener(context);
const serverEvent$ = serverEventsSubject.asObservable().pipe(takeWhile(e => !isCloseEvent(e)));
const ctx = createEffectContext({ ask: lookup(context), client: undefined });
const combinedEvents = event$ ? combineEffects(statusLogger$, event$) : statusLogger$;

feat(core): resolve eager dependencies independently of their binding order