Commit 9263e94b authored by nanahira's avatar nanahira

fix app-context load

parent 618ed3b9
This diff is collapsed.
import { dualizeAny, throwDualPending } from '../dual-object';
type PromiseState<T = any> =
| { status: 'pending'; value?: undefined; error?: undefined }
| { status: 'fulfilled'; value: T; error?: undefined }
| { status: 'rejected'; value?: undefined; error: any };
const promiseStates = new WeakMap<Promise<any>, PromiseState>();
export const isPromiseLike = (value: any): value is Promise<any> =>
!!value && typeof value.then === 'function';
export const trackPromise = <T>(promise: Promise<T>): PromiseState<T> => {
const existing = promiseStates.get(promise);
if (existing) return existing as PromiseState<T>;
const state = { status: 'pending' } as PromiseState<T>;
promiseStates.set(promise, state);
promise.then(
(value) => {
(state as any).status = 'fulfilled';
(state as any).value = value;
},
(error) => {
(state as any).status = 'rejected';
(state as any).error = error;
},
);
return state;
};
export const wrapMaybePromise = <T>(
value: T,
options?: { methodKeys?: Iterable<PropertyKey> },
): T => {
if (!isPromiseLike(value)) return value;
const promise = Promise.resolve(value);
const state = trackPromise(promise);
if (state.status === 'fulfilled') return state.value;
if (state.status === 'rejected') throw state.error;
return dualizeAny<T>(
() => {
const current = trackPromise(promise);
if (current.status === 'fulfilled') return current.value;
if (current.status === 'rejected') throw current.error;
throwDualPending();
},
() => promise,
{
// Intentionally hide strict method return type here.
asyncMethods: Array.from(options?.methodKeys ?? []) as any,
},
);
};
export const createAsyncMethod =
(inst: any, key: PropertyKey) =>
(...args: any[]) =>
Promise.resolve(inst).then((resolved) => {
const fn = resolved?.[key];
if (typeof fn !== 'function') {
throw new TypeError('Target method is not a function');
}
return fn.apply(resolved, args);
});
......@@ -42,18 +42,19 @@ class InitLogService {
}
describe('app-context runtime', () => {
test('provide + merge(method) binds this correctly', () => {
const ctx = createAppContext()
test('provide + merge(method) binds this correctly', async () => {
const ctx = await createAppContext()
.provide(CounterService, 1, { merge: ['inc'] })
.define();
.define()
.start();
const inc = ctx.inc;
expect(inc()).toBe(2);
expect(inc()).toBe(3);
});
test('provide getter on pending service: method call works and normal field throws', async () => {
const ctx = createAppContext()
test('async factory is resolved during start()', async () => {
const ctx = await createAppContext()
.provide(CounterService, 5, {
provide: 'counter',
useFactory: async (self, ...args: unknown[]) => {
......@@ -62,17 +63,16 @@ describe('app-context runtime', () => {
return new CounterService(self, initial as number);
},
})
.define();
.define()
.start();
const p = ctx.counter.ping(3);
expect(() => ctx.counter.value).toThrow(
new TypeError('Value is not ready yet. Please await it first.'),
);
await expect(p).resolves.toBe(8);
// After start(), all async services are resolved
expect(ctx.counter.value).toBe(5);
await expect(ctx.counter.ping(3)).resolves.toBe(8);
});
test('merge(property) set is queued before resolve and flushed after getAsync', async () => {
const ctx = createAppContext()
test('merge(property) can be set after start()', async () => {
const ctx = await createAppContext()
.provide(AsyncMutableService, {
merge: ['count'],
useFactory: async (self) => {
......@@ -80,12 +80,12 @@ describe('app-context runtime', () => {
return new AsyncMutableService(self);
},
})
.define();
.define()
.start();
// After start(), properties can be set directly
ctx.count = 42;
await expect(ctx.getAsync(AsyncMutableService)).resolves.toMatchObject({
count: 42,
});
expect(ctx.get(AsyncMutableService).count).toBe(42);
expect(ctx.count).toBe(42);
});
......@@ -93,10 +93,10 @@ describe('app-context runtime', () => {
const ctx1 = createAppContext()
.provide(CounterService, 7, { provide: 'counter', merge: ['inc'] })
.define();
const root = createAppContext().use(ctx1).define();
const root = await createAppContext().use(ctx1).define().start();
expect(root.counter.inc()).toBe(8);
await expect(root.getAsync(CounterService)).resolves.toMatchObject({
expect(root.get(CounterService)).toMatchObject({
value: 8,
});
});
......@@ -136,10 +136,11 @@ describe('app-context runtime', () => {
});
describe('app-context type checks', () => {
test('compile-time type assertions', () => {
const ctx = createAppContext()
test('compile-time type assertions', async () => {
const ctx = await createAppContext()
.provide(CounterService, 1, { provide: 'counter', merge: ['inc'] })
.define();
.define()
.start();
const n: number = ctx.counter.inc();
expect(n).toBe(2);
......@@ -158,11 +159,11 @@ describe('app-context type checks', () => {
expect(ok).toBe(true);
});
test('use merges context types', () => {
test('use merges context types', async () => {
const a = createAppContext()
.provide(CounterService, 1, { provide: 'counter' })
.define();
const b = createAppContext().use(a).define();
const b = await createAppContext().use(a).define().start();
const v: number = b.counter.value;
expect(v).toBe(1);
});
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment