Commit 9d5ee363 authored by nanahira's avatar nanahira

fix context

parent ad4c365d
...@@ -41,6 +41,52 @@ export class AppContextCore<Cur = Empty, Req = Empty> { ...@@ -41,6 +41,52 @@ export class AppContextCore<Cur = Empty, Req = Empty> {
private registry = new Map<string | AnyClass, LoadEntry>(); private registry = new Map<string | AnyClass, LoadEntry>();
private objectSteps: ObjectStep[] = []; private objectSteps: ObjectStep[] = [];
started = false; started = false;
private starting = false;
private startingEntries: LoadEntry[] | null = null;
private createdRecords: Set<ProvideRecord> | null = null;
private createdEntries: Map<ProvideRecord, LoadEntry> | null = null;
private loadingRecords = new Set<ProvideRecord>();
private findProvideRecord(key: AnyClass): ProvideRecord | undefined {
for (let i = 0; i < this.provideRecords.length; i += 1) {
const record = this.provideRecords[i];
if (
record.classRef === key &&
(!this.createdRecords || !this.createdRecords.has(record))
) {
return record;
}
}
return undefined;
}
private createEntryFromRecord(record: ProvideRecord): LoadEntry {
const existing = this.createdEntries?.get(record);
if (existing) {
return existing;
}
if (this.loadingRecords.has(record)) {
throw new Error(
`Circular dependency detected while providing: ${record.classRef?.name ?? 'UnknownService'}`,
);
}
this.loadingRecords.add(record);
try {
const entry: LoadEntry = {
classRef: record.classRef,
inst: record.factory(this),
};
this.createdRecords?.add(record);
this.createdEntries?.set(record, entry);
this.registry.set(record.classRef, entry);
this.startingEntries?.push(entry);
return entry;
} finally {
this.loadingRecords.delete(record);
}
}
provide< provide<
C extends AppServiceClass<Cur, Req>, C extends AppServiceClass<Cur, Req>,
...@@ -156,6 +202,13 @@ export class AppContextCore<Cur = Empty, Req = Empty> { ...@@ -156,6 +202,13 @@ export class AppContextCore<Cur = Empty, Req = Empty> {
key = (cls as () => AppServiceClass<Cur, Req, any, R>)() as AnyClass; key = (cls as () => AppServiceClass<Cur, Req, any, R>)() as AnyClass;
} }
if (!this.registry.has(key) && this.starting) {
const record = this.findProvideRecord(key);
if (record) {
this.createEntryFromRecord(record);
}
}
if (!this.registry.has(key)) { if (!this.registry.has(key)) {
throw new Error(`Service not provided: ${key?.name ?? cls.name}`); throw new Error(`Service not provided: ${key?.name ?? cls.name}`);
} }
...@@ -225,39 +278,42 @@ export class AppContextCore<Cur = Empty, Req = Empty> { ...@@ -225,39 +278,42 @@ export class AppContextCore<Cur = Empty, Req = Empty> {
const startedEntries: LoadEntry[] = []; const startedEntries: LoadEntry[] = [];
const preloadedKeys = new Set(this.registry.keys()); const preloadedKeys = new Set(this.registry.keys());
this.starting = true;
// Create all instances this.startingEntries = startedEntries;
for (const record of this.provideRecords) { this.createdRecords = new Set();
if (preloadedKeys.has(record.classRef)) { this.createdEntries = new Map();
continue; try {
// Create all instances. Dependencies requested via get() during construction
// can be created on-demand from remaining provide records.
for (const record of this.provideRecords) {
if (preloadedKeys.has(record.classRef)) {
continue;
}
this.createEntryFromRecord(record);
} }
const inst = record.factory(this); // Resolve all promises created in this start().
const entry: LoadEntry = { for (const entry of startedEntries) {
classRef: record.classRef, if (entry.inst && typeof entry.inst.then === 'function') {
inst, entry.inst = await entry.inst;
}; }
this.registry.set(record.classRef, entry);
startedEntries.push(entry);
}
// 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;
} }
}
// Init only instances created in this start(). // Init only instances created in this start().
for (const entry of startedEntries) { for (const entry of startedEntries) {
const inst = entry.inst; const inst = entry.inst;
if (inst && typeof inst.init === 'function') { if (inst && typeof inst.init === 'function') {
await inst.init(); await inst.init();
}
} }
this.started = true;
return this as any;
} finally {
this.createdEntries = null;
this.createdRecords = null;
this.startingEntries = null;
this.starting = false;
} }
this.started = true;
return this as any;
} }
} }
......
...@@ -63,6 +63,14 @@ class NeedsMergedMethodService { ...@@ -63,6 +63,14 @@ class NeedsMergedMethodService {
} }
} }
class NeedsCounterService {
counter: CounterService;
constructor(public ctx: AppContext) {
this.counter = ctx.get(CounterService);
}
}
describe('app-context runtime', () => { describe('app-context runtime', () => {
test('provide + merge(method) binds this correctly', async () => { test('provide + merge(method) binds this correctly', async () => {
const ctx = await createAppContext() const ctx = await createAppContext()
...@@ -192,6 +200,32 @@ describe('app-context runtime', () => { ...@@ -192,6 +200,32 @@ describe('app-context runtime', () => {
const root = await createAppContext().use(parent).use(child).define().start(); const root = await createAppContext().use(parent).use(child).define().start();
expect(root.needsMerged.value).toBe(11); expect(root.needsMerged.value).toBe(11);
}); });
test('provider in earlier used context can get provider from later used context', async () => {
const a = createAppContext()
.provide(NeedsCounterService, { provide: 'needsCounter' })
.define();
const b = createAppContext()
.provide(CounterService, 21, { provide: 'counter' })
.define();
const root = await createAppContext().use(a).use(b).define().start();
expect(root.needsCounter.counter).toBe(root.counter);
expect(root.needsCounter.counter.value).toBe(21);
});
test('provider in later used context can get provider from earlier used context', async () => {
const a = createAppContext()
.provide(CounterService, 34, { provide: 'counter' })
.define();
const b = createAppContext()
.provide(NeedsCounterService, { provide: 'needsCounter' })
.define();
const root = await createAppContext().use(a).use(b).define().start();
expect(root.needsCounter.counter).toBe(root.counter);
expect(root.needsCounter.counter.value).toBe(34);
});
}); });
describe('app-context type checks', () => { 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