Commit 633652d6 authored by nanahira's avatar nanahira

missing

parent bfd03193
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);
});
import { createAppContext, AppContext } from '../src/app-context';
const delay = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
type Equal<A, B> =
(<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2
? true
: false;
type Expect<T extends true> = T;
class CounterService {
constructor(
public ctx: AppContext,
public value = 1,
) {}
inc() {
this.value += 1;
return this.value;
}
async ping(add: number) {
return this.value + add;
}
}
class AsyncMutableService {
constructor(public ctx: AppContext) {}
count = 0;
}
class InitLogService {
constructor(
public ctx: AppContext,
private name: string,
private logs: string[],
) {}
async init() {
this.logs.push(`init:${this.name}`);
}
}
describe('app-context runtime', () => {
test('provide + merge(method) binds this correctly', () => {
const ctx = createAppContext()
.provide(CounterService, 1, { merge: ['inc'] })
.define();
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()
.provide(CounterService, 5, {
provide: 'counter',
useFactory: async (self, ...args: unknown[]) => {
const initial = args[args.length - 1];
await delay(20);
return new CounterService(self, initial as number);
},
})
.define();
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);
});
test('merge(property) set is queued before resolve and flushed after getAsync', async () => {
const ctx = createAppContext()
.provide(AsyncMutableService, {
merge: ['count'],
useFactory: async (self) => {
await delay(20);
return new AsyncMutableService(self);
},
})
.define();
ctx.count = 42;
await expect(ctx.getAsync(AsyncMutableService)).resolves.toMatchObject({
count: 42,
});
expect(ctx.count).toBe(42);
});
test('use replays object definition steps and merges registry', async () => {
const ctx1 = createAppContext()
.provide(CounterService, 7, { provide: 'counter', merge: ['inc'] })
.define();
const root = createAppContext().use(ctx1).define();
expect(root.counter.inc()).toBe(8);
await expect(root.getAsync(CounterService)).resolves.toMatchObject({
value: 8,
});
});
test('start resolves async provides and runs init in registration order', async () => {
const logs: string[] = [];
const ctx1 = createAppContext()
.provide(InitLogService, 'A', logs, {
useFactory: async (self, ...args: unknown[]) => {
const [name, output] = args.length >= 2 ? args : [args[0], logs];
const resolvedName = String(name);
const resolvedOutput = output as string[];
await delay(20);
resolvedOutput.push(`resolve:${resolvedName}`);
return new InitLogService(self, resolvedName, resolvedOutput);
},
})
.define();
const ctx2 = createAppContext()
.provide(InitLogService, 'B', logs, {
useFactory: async (self, ...args: unknown[]) => {
const [name, output] = args.length >= 2 ? args : [args[0], logs];
const resolvedName = String(name);
const resolvedOutput = output as string[];
await delay(5);
resolvedOutput.push(`resolve:${resolvedName}`);
return new InitLogService(self, resolvedName, resolvedOutput);
},
})
.define();
await createAppContext().use(ctx1).use(ctx2).define().start();
expect(logs).toContain('init:A');
expect(logs).toContain('init:B');
expect(logs.indexOf('init:A')).toBeLessThan(logs.indexOf('init:B'));
});
});
describe('app-context type checks', () => {
test('compile-time type assertions', () => {
const ctx = createAppContext()
.provide(CounterService, 1, { provide: 'counter', merge: ['inc'] })
.define();
const n: number = ctx.counter.inc();
expect(n).toBe(2);
type _counter = Expect<Equal<typeof ctx.counter, CounterService>>;
type _inc = Expect<Equal<typeof ctx.inc, () => number>>;
const ok: _counter | _inc = true;
expect(ok).toBe(true);
});
test('start return type is never when requirement not fulfilled', () => {
const reqCtx = createAppContext<{ foo: number }>().define();
type Ret = Awaited<ReturnType<typeof reqCtx.start>>;
type _ret = Expect<Equal<Ret, never>>;
const ok: _ret = true;
expect(ok).toBe(true);
});
test('use merges context types', () => {
const a = createAppContext()
.provide(CounterService, 1, { provide: 'counter' })
.define();
const b = createAppContext().use(a).define();
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