Commit ada020c6 authored by nanahira's avatar nanahira

allow in-provider provide

parent 4c10330f
This diff is collapsed.
...@@ -33,6 +33,9 @@ type LoadEntry = { ...@@ -33,6 +33,9 @@ type LoadEntry = {
type ObjectStep = (ctx: AppContextCore<any, any>) => void; type ObjectStep = (ctx: AppContextCore<any, any>) => void;
const isPromiseLike = (value: any): value is Promise<any> =>
!!value && typeof value.then === 'function';
export class AppContextCore<Cur = Empty, Req = Empty> { export class AppContextCore<Cur = Empty, Req = Empty> {
private __current: Cur; private __current: Cur;
private __required: Req; private __required: Req;
...@@ -42,18 +45,90 @@ export class AppContextCore<Cur = Empty, Req = Empty> { ...@@ -42,18 +45,90 @@ export class AppContextCore<Cur = Empty, Req = Empty> {
private objectSteps: ObjectStep[] = []; private objectSteps: ObjectStep[] = [];
started = false; started = false;
private starting = false; private starting = false;
private startingEntries: LoadEntry[] | null = null; private settleCursor = 0;
private createdRecords: Set<ProvideRecord> | null = null; private recordEntries = new Map<ProvideRecord, LoadEntry>();
private createdEntries: Map<ProvideRecord, LoadEntry> | null = null; private pendingInitQueue: LoadEntry[] = [];
private pendingInitSet = new Set<LoadEntry>();
private initializedEntries = new Set<LoadEntry>();
private loadingRecords = new Set<ProvideRecord>(); private loadingRecords = new Set<ProvideRecord>();
private settlementChain: Promise<void> = Promise.resolve();
private settlementError: unknown = null;
private settling = false;
private assertSettlementHealthy() {
if (this.settlementError) {
throw this.settlementError;
}
}
private enqueueInit(entry: LoadEntry) {
if (this.initializedEntries.has(entry) || this.pendingInitSet.has(entry)) {
return;
}
this.pendingInitSet.add(entry);
this.pendingInitQueue.push(entry);
}
private async constructPendingRecords() {
while (this.settleCursor < this.provideRecords.length) {
const record = this.provideRecords[this.settleCursor];
this.settleCursor += 1;
await this.createEntryFromRecord(record);
}
}
private async settlePending() {
await this.constructPendingRecords();
while (this.pendingInitQueue.length > 0) {
const entry = this.pendingInitQueue.shift()!;
this.pendingInitSet.delete(entry);
if (this.initializedEntries.has(entry)) {
continue;
}
const inst = await entry.inst;
entry.inst = inst;
if (inst && typeof inst.init === 'function') {
await inst.init();
}
this.initializedEntries.add(entry);
// Providers added during this init must settle and init immediately
// after current init, before previous remaining init queue items.
const remaining = this.pendingInitQueue.splice(0);
await this.constructPendingRecords();
this.pendingInitQueue.push(...remaining);
}
}
private scheduleSettlement() {
const run = async () => {
this.assertSettlementHealthy();
this.settling = true;
try {
await this.settlePending();
} catch (error) {
this.settlementError = error;
throw error;
} finally {
this.settling = false;
}
};
const task = this.settlementChain.then(run, run);
this.settlementChain = task.catch(() => undefined);
return task;
}
private triggerSettlement() {
void this.scheduleSettlement();
}
private findProvideRecord(key: AnyClass): ProvideRecord | undefined { private findProvideRecord(key: AnyClass): ProvideRecord | undefined {
for (let i = 0; i < this.provideRecords.length; i += 1) { for (let i = 0; i < this.provideRecords.length; i += 1) {
const record = this.provideRecords[i]; const record = this.provideRecords[i];
if ( if (record.classRef === key && !this.recordEntries.has(record)) {
record.classRef === key &&
(!this.createdRecords || !this.createdRecords.has(record))
) {
return record; return record;
} }
} }
...@@ -61,7 +136,7 @@ export class AppContextCore<Cur = Empty, Req = Empty> { ...@@ -61,7 +136,7 @@ export class AppContextCore<Cur = Empty, Req = Empty> {
} }
private createEntryFromRecord(record: ProvideRecord): Awaitable<LoadEntry> { private createEntryFromRecord(record: ProvideRecord): Awaitable<LoadEntry> {
const existing = this.createdEntries?.get(record); const existing = this.recordEntries.get(record);
if (existing) { if (existing) {
return existing; return existing;
} }
...@@ -78,11 +153,10 @@ export class AppContextCore<Cur = Empty, Req = Empty> { ...@@ -78,11 +153,10 @@ export class AppContextCore<Cur = Empty, Req = Empty> {
classRef: record.classRef, classRef: record.classRef,
inst: record.factory(this), inst: record.factory(this),
}; };
this.createdRecords?.add(record); this.recordEntries.set(record, entry);
this.createdEntries?.set(record, entry);
this.registry.set(record.classRef, entry); this.registry.set(record.classRef, entry);
this.startingEntries?.push(entry); this.enqueueInit(entry);
if (entry.inst?.then && typeof entry.inst.then === 'function') { if (isPromiseLike(entry.inst)) {
return entry.inst.then((resolvedInst: any) => { return entry.inst.then((resolvedInst: any) => {
entry.inst = resolvedInst; entry.inst = resolvedInst;
return entry; return entry;
...@@ -102,6 +176,8 @@ export class AppContextCore<Cur = Empty, Req = Empty> { ...@@ -102,6 +176,8 @@ export class AppContextCore<Cur = Empty, Req = Empty> {
cls: C, cls: C,
...args: AppProvideArgs<Cur, Req, C, P, M> ...args: AppProvideArgs<Cur, Req, C, P, M>
): AppProvidedMerged<Cur, Req, C, P, M> { ): AppProvidedMerged<Cur, Req, C, P, M> {
this.assertSettlementHealthy();
const last = args[args.length - 1] as any; const last = args[args.length - 1] as any;
const hasOptions = const hasOptions =
!!last && !!last &&
...@@ -199,6 +275,8 @@ export class AppContextCore<Cur = Empty, Req = Empty> { ...@@ -199,6 +275,8 @@ export class AppContextCore<Cur = Empty, Req = Empty> {
| AppServiceClass<Cur, Req, any, R> | AppServiceClass<Cur, Req, any, R>
| (() => AppServiceClass<Cur, Req, any, R>), | (() => AppServiceClass<Cur, Req, any, R>),
): R { ): R {
this.assertSettlementHealthy();
let key = cls as unknown as AnyClass; let key = cls as unknown as AnyClass;
if ( if (
!this.registry.has(key) && !this.registry.has(key) &&
...@@ -208,7 +286,7 @@ export class AppContextCore<Cur = Empty, Req = Empty> { ...@@ -208,7 +286,7 @@ 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) { if (!this.registry.has(key) && (this.starting || this.settling)) {
const record = this.findProvideRecord(key); const record = this.findProvideRecord(key);
if (record) { if (record) {
this.createEntryFromRecord(record); this.createEntryFromRecord(record);
...@@ -234,17 +312,14 @@ export class AppContextCore<Cur = Empty, Req = Empty> { ...@@ -234,17 +312,14 @@ export class AppContextCore<Cur = Empty, Req = Empty> {
use<const Ctxes extends AppContext<any, any>[]>( use<const Ctxes extends AppContext<any, any>[]>(
...ctxes: Ctxes ...ctxes: Ctxes
): AppContextUsed<Cur, Req, Ctxes> { ): AppContextUsed<Cur, Req, Ctxes> {
this.assertSettlementHealthy();
for (const ctx of ctxes) { for (const ctx of ctxes) {
const other = ctx as any as AppContextCore<any, any>; const other = ctx as any as AppContextCore<any, any>;
if (this.started && !other?.started) { // Started contexts are merged by instance directly (registry copy),
throw new Error( // and should not queue reconstruction/re-init on target context.
'Cannot use an unstarted context into a started context.', if (Array.isArray(other?.provideRecords) && !other?.started) {
);
}
// Copy provide records
if (Array.isArray(other?.provideRecords)) {
this.provideRecords.push(...other.provideRecords); this.provideRecords.push(...other.provideRecords);
} }
...@@ -271,6 +346,10 @@ export class AppContextCore<Cur = Empty, Req = Empty> { ...@@ -271,6 +346,10 @@ export class AppContextCore<Cur = Empty, Req = Empty> {
} }
define(): AppContext<Cur, Req> { define(): AppContext<Cur, Req> {
this.assertSettlementHealthy();
if (this.started && !this.starting) {
this.triggerSettlement();
}
return this as any; return this as any;
} }
...@@ -279,35 +358,13 @@ export class AppContextCore<Cur = Empty, Req = Empty> { ...@@ -279,35 +358,13 @@ export class AppContextCore<Cur = Empty, Req = Empty> {
return this as any; return this as any;
} }
const startedEntries: LoadEntry[] = []; this.assertSettlementHealthy();
const preloadedKeys = new Set(this.registry.keys());
this.starting = true; this.starting = true;
this.startingEntries = startedEntries;
this.createdRecords = new Set();
this.createdEntries = new Map();
try { try {
// Create all instances. Dependencies requested via get() during construction await this.scheduleSettlement();
// can be created on-demand from remaining provide records.
for (const record of this.provideRecords) {
if (preloadedKeys.has(record.classRef)) {
continue;
}
await this.createEntryFromRecord(record);
}
// 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();
}
}
this.started = true; this.started = true;
return this as any; return this as any;
} finally { } finally {
this.createdEntries = null;
this.createdRecords = null;
this.startingEntries = null;
this.starting = false; this.starting = false;
} }
} }
......
This diff is collapsed.
...@@ -164,15 +164,16 @@ describe('app-context runtime', () => { ...@@ -164,15 +164,16 @@ describe('app-context runtime', () => {
expect(logs.indexOf('init:A')).toBeLessThan(logs.indexOf('init:B')); expect(logs.indexOf('init:A')).toBeLessThan(logs.indexOf('init:B'));
}); });
test('started context cannot use unstarted context', async () => { test('started context can use unstarted context and settle on define()', async () => {
const started = await createAppContext().define().start(); const started = await createAppContext().define().start();
const unstarted = createAppContext() const unstarted = createAppContext()
.provide(CounterService, 1, { provide: 'counter' }) .provide(CounterService, 1, { provide: 'counter' })
.define(); .define();
expect(() => started.use(unstarted)).toThrow( started.use(unstarted).define();
'Cannot use an unstarted context into a started context.', await delay(0);
); expect(started.get(CounterService).value).toBe(1);
expect((started as any).counter.value).toBe(1);
}); });
test('using started context does not re-init imported providers', async () => { test('using started context does not re-init imported providers', async () => {
......
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