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 { ...@@ -42,18 +42,19 @@ class InitLogService {
} }
describe('app-context runtime', () => { describe('app-context runtime', () => {
test('provide + merge(method) binds this correctly', () => { test('provide + merge(method) binds this correctly', async () => {
const ctx = createAppContext() const ctx = await createAppContext()
.provide(CounterService, 1, { merge: ['inc'] }) .provide(CounterService, 1, { merge: ['inc'] })
.define(); .define()
.start();
const inc = ctx.inc; const inc = ctx.inc;
expect(inc()).toBe(2); expect(inc()).toBe(2);
expect(inc()).toBe(3); expect(inc()).toBe(3);
}); });
test('provide getter on pending service: method call works and normal field throws', async () => { test('async factory is resolved during start()', async () => {
const ctx = createAppContext() const ctx = await createAppContext()
.provide(CounterService, 5, { .provide(CounterService, 5, {
provide: 'counter', provide: 'counter',
useFactory: async (self, ...args: unknown[]) => { useFactory: async (self, ...args: unknown[]) => {
...@@ -62,17 +63,16 @@ describe('app-context runtime', () => { ...@@ -62,17 +63,16 @@ describe('app-context runtime', () => {
return new CounterService(self, initial as number); return new CounterService(self, initial as number);
}, },
}) })
.define(); .define()
.start();
const p = ctx.counter.ping(3); // After start(), all async services are resolved
expect(() => ctx.counter.value).toThrow( expect(ctx.counter.value).toBe(5);
new TypeError('Value is not ready yet. Please await it first.'), await expect(ctx.counter.ping(3)).resolves.toBe(8);
);
await expect(p).resolves.toBe(8);
}); });
test('merge(property) set is queued before resolve and flushed after getAsync', async () => { test('merge(property) can be set after start()', async () => {
const ctx = createAppContext() const ctx = await createAppContext()
.provide(AsyncMutableService, { .provide(AsyncMutableService, {
merge: ['count'], merge: ['count'],
useFactory: async (self) => { useFactory: async (self) => {
...@@ -80,12 +80,12 @@ describe('app-context runtime', () => { ...@@ -80,12 +80,12 @@ describe('app-context runtime', () => {
return new AsyncMutableService(self); return new AsyncMutableService(self);
}, },
}) })
.define(); .define()
.start();
// After start(), properties can be set directly
ctx.count = 42; ctx.count = 42;
await expect(ctx.getAsync(AsyncMutableService)).resolves.toMatchObject({ expect(ctx.get(AsyncMutableService).count).toBe(42);
count: 42,
});
expect(ctx.count).toBe(42); expect(ctx.count).toBe(42);
}); });
...@@ -93,10 +93,10 @@ describe('app-context runtime', () => { ...@@ -93,10 +93,10 @@ describe('app-context runtime', () => {
const ctx1 = createAppContext() const ctx1 = createAppContext()
.provide(CounterService, 7, { provide: 'counter', merge: ['inc'] }) .provide(CounterService, 7, { provide: 'counter', merge: ['inc'] })
.define(); .define();
const root = createAppContext().use(ctx1).define(); const root = await createAppContext().use(ctx1).define().start();
expect(root.counter.inc()).toBe(8); expect(root.counter.inc()).toBe(8);
await expect(root.getAsync(CounterService)).resolves.toMatchObject({ expect(root.get(CounterService)).toMatchObject({
value: 8, value: 8,
}); });
}); });
...@@ -136,10 +136,11 @@ describe('app-context runtime', () => { ...@@ -136,10 +136,11 @@ describe('app-context runtime', () => {
}); });
describe('app-context type checks', () => { describe('app-context type checks', () => {
test('compile-time type assertions', () => { test('compile-time type assertions', async () => {
const ctx = createAppContext() const ctx = await createAppContext()
.provide(CounterService, 1, { provide: 'counter', merge: ['inc'] }) .provide(CounterService, 1, { provide: 'counter', merge: ['inc'] })
.define(); .define()
.start();
const n: number = ctx.counter.inc(); const n: number = ctx.counter.inc();
expect(n).toBe(2); expect(n).toBe(2);
...@@ -158,11 +159,11 @@ describe('app-context type checks', () => { ...@@ -158,11 +159,11 @@ describe('app-context type checks', () => {
expect(ok).toBe(true); expect(ok).toBe(true);
}); });
test('use merges context types', () => { test('use merges context types', async () => {
const a = createAppContext() const a = createAppContext()
.provide(CounterService, 1, { provide: 'counter' }) .provide(CounterService, 1, { provide: 'counter' })
.define(); .define();
const b = createAppContext().use(a).define(); const b = await createAppContext().use(a).define().start();
const v: number = b.counter.value; const v: number = b.counter.value;
expect(v).toBe(1); 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