Commit baa9e715 authored by nanahira's avatar nanahira

fix

parent 8b1eceb6
......@@ -23,7 +23,7 @@ const getMethodDescriptor = (cls: AnyClass, key: PropertyKey) => {
type ProvideRecord = {
classRef: AnyClass;
factory: () => any;
factory: (ctx: AppContextCore<any, any>) => any;
};
type LoadEntry = {
......@@ -39,9 +39,8 @@ export class AppContextCore<Cur = Empty, Req = Empty> {
private provideRecords: ProvideRecord[] = [];
private registry = new Map<string | AnyClass, LoadEntry>();
private loadSeq: LoadEntry[] = [];
private objectSteps: ObjectStep[] = [];
private started = false;
started = false;
provide<
C extends AppServiceClass<Cur, Req>,
......@@ -71,12 +70,12 @@ export class AppContextCore<Cur = Empty, Req = Empty> {
const classRef = cls as unknown as AnyClass;
// Create factory function that will be called during start()
const factory = () =>
// Create factory function that will be called during start() with the target ctx.
const factory = (ctx: AppContextCore<any, any>) =>
options?.useValue ??
(options?.useFactory
? options.useFactory(this as any, ..._args)
: new (options?.useClass ?? cls)(this as any, ..._args));
? options.useFactory(ctx as any, ..._args)
: new (options?.useClass ?? cls)(ctx as any, ..._args));
// Record the provide configuration
this.provideRecords.push({
......@@ -182,6 +181,12 @@ export class AppContextCore<Cur = Empty, Req = Empty> {
for (const ctx of ctxes) {
const other = ctx as any as AppContextCore<any, any>;
if (this.started && !other?.started) {
throw new Error(
'Cannot use an unstarted context into a started context.',
);
}
// Copy provide records
if (Array.isArray(other?.provideRecords)) {
this.provideRecords.push(...other.provideRecords);
......@@ -195,11 +200,9 @@ export class AppContextCore<Cur = Empty, Req = Empty> {
}
}
// If the other context has already started, copy its loaded entries
// If the other context has already started, copy loaded entries only.
// They should remain initialized by the source context and must not be re-initialized.
if (other?.started) {
if (Array.isArray(other?.loadSeq)) {
this.loadSeq.push(...other.loadSeq);
}
if (other?.registry instanceof Map) {
for (const [key, value] of other.registry.entries()) {
this.registry.set(key, value);
......@@ -220,26 +223,33 @@ export class AppContextCore<Cur = Empty, Req = Empty> {
return this as any;
}
const startedEntries: LoadEntry[] = [];
const preloadedKeys = new Set(this.registry.keys());
// Create all instances
for (const record of this.provideRecords) {
const inst = record.factory();
if (preloadedKeys.has(record.classRef)) {
continue;
}
const inst = record.factory(this);
const entry: LoadEntry = {
classRef: record.classRef,
inst,
};
this.registry.set(record.classRef, entry);
this.loadSeq.push(entry);
startedEntries.push(entry);
}
// Resolve all promises
for (const entry of this.loadSeq) {
// Resolve all promises created in this start().
for (const entry of startedEntries) {
if (entry.inst && typeof entry.inst.then === 'function') {
entry.inst = await entry.inst;
}
}
// Call init on all instances
for (const entry of this.loadSeq) {
// Init only instances created in this start().
for (const entry of startedEntries) {
const inst = entry.inst;
if (inst && typeof inst.init === 'function') {
await inst.init();
......
......@@ -41,6 +41,28 @@ class InitLogService {
}
}
class InitCounterService {
constructor(
public ctx: AppContext,
private counter: { value: number },
) {}
async init() {
this.counter.value += 1;
}
}
class NeedsMergedMethodService {
value: number;
constructor(public ctx: AppContext) {
if (typeof (ctx as any).inc !== 'function') {
throw new Error('missing merged method: inc');
}
this.value = (ctx as any).inc();
}
}
describe('app-context runtime', () => {
test('provide + merge(method) binds this correctly', async () => {
const ctx = await createAppContext()
......@@ -133,6 +155,43 @@ describe('app-context runtime', () => {
expect(logs).toContain('init:B');
expect(logs.indexOf('init:A')).toBeLessThan(logs.indexOf('init:B'));
});
test('started context cannot use unstarted context', async () => {
const started = await createAppContext().define().start();
const unstarted = createAppContext()
.provide(CounterService, 1, { provide: 'counter' })
.define();
expect(() => started.use(unstarted)).toThrow(
'Cannot use an unstarted context into a started context.',
);
});
test('using started context does not re-init imported providers', async () => {
const counter = { value: 0 };
const startedChild = await createAppContext()
.provide(InitCounterService, counter)
.define()
.start();
expect(counter.value).toBe(1);
const root = await createAppContext().use(startedChild).define().start();
expect(counter.value).toBe(1);
expect(root.get(InitCounterService)).toBe(startedChild.get(InitCounterService));
});
test('provider from used context can access merged members on target context', async () => {
const parent = createAppContext()
.provide(CounterService, 10, { merge: ['inc'] })
.define();
const child = createAppContext()
.provide(NeedsMergedMethodService, { provide: 'needsMerged' })
.define();
const root = await createAppContext().use(parent).use(child).define().start();
expect(root.needsMerged.value).toBe(11);
});
});
describe('app-context type checks', () => {
......
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