Commit ada020c6 authored by nanahira's avatar nanahira

allow in-provider provide

parent 4c10330f
This diff is collapsed.
......@@ -33,6 +33,9 @@ type LoadEntry = {
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> {
private __current: Cur;
private __required: Req;
......@@ -42,18 +45,90 @@ export class AppContextCore<Cur = Empty, Req = Empty> {
private objectSteps: ObjectStep[] = [];
started = false;
private starting = false;
private startingEntries: LoadEntry[] | null = null;
private createdRecords: Set<ProvideRecord> | null = null;
private createdEntries: Map<ProvideRecord, LoadEntry> | null = null;
private settleCursor = 0;
private recordEntries = new Map<ProvideRecord, LoadEntry>();
private pendingInitQueue: LoadEntry[] = [];
private pendingInitSet = new Set<LoadEntry>();
private initializedEntries = new Set<LoadEntry>();
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 {
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))
) {
if (record.classRef === key && !this.recordEntries.has(record)) {
return record;
}
}
......@@ -61,7 +136,7 @@ export class AppContextCore<Cur = Empty, Req = Empty> {
}
private createEntryFromRecord(record: ProvideRecord): Awaitable<LoadEntry> {
const existing = this.createdEntries?.get(record);
const existing = this.recordEntries.get(record);
if (existing) {
return existing;
}
......@@ -78,11 +153,10 @@ export class AppContextCore<Cur = Empty, Req = Empty> {
classRef: record.classRef,
inst: record.factory(this),
};
this.createdRecords?.add(record);
this.createdEntries?.set(record, entry);
this.recordEntries.set(record, entry);
this.registry.set(record.classRef, entry);
this.startingEntries?.push(entry);
if (entry.inst?.then && typeof entry.inst.then === 'function') {
this.enqueueInit(entry);
if (isPromiseLike(entry.inst)) {
return entry.inst.then((resolvedInst: any) => {
entry.inst = resolvedInst;
return entry;
......@@ -102,6 +176,8 @@ export class AppContextCore<Cur = Empty, Req = Empty> {
cls: C,
...args: AppProvideArgs<Cur, Req, C, P, M>
): AppProvidedMerged<Cur, Req, C, P, M> {
this.assertSettlementHealthy();
const last = args[args.length - 1] as any;
const hasOptions =
!!last &&
......@@ -199,6 +275,8 @@ export class AppContextCore<Cur = Empty, Req = Empty> {
| AppServiceClass<Cur, Req, any, R>
| (() => AppServiceClass<Cur, Req, any, R>),
): R {
this.assertSettlementHealthy();
let key = cls as unknown as AnyClass;
if (
!this.registry.has(key) &&
......@@ -208,7 +286,7 @@ export class AppContextCore<Cur = Empty, Req = Empty> {
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);
if (record) {
this.createEntryFromRecord(record);
......@@ -234,17 +312,14 @@ export class AppContextCore<Cur = Empty, Req = Empty> {
use<const Ctxes extends AppContext<any, any>[]>(
...ctxes: Ctxes
): AppContextUsed<Cur, Req, Ctxes> {
this.assertSettlementHealthy();
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)) {
// Started contexts are merged by instance directly (registry copy),
// and should not queue reconstruction/re-init on target context.
if (Array.isArray(other?.provideRecords) && !other?.started) {
this.provideRecords.push(...other.provideRecords);
}
......@@ -271,6 +346,10 @@ export class AppContextCore<Cur = Empty, Req = Empty> {
}
define(): AppContext<Cur, Req> {
this.assertSettlementHealthy();
if (this.started && !this.starting) {
this.triggerSettlement();
}
return this as any;
}
......@@ -279,35 +358,13 @@ export class AppContextCore<Cur = Empty, Req = Empty> {
return this as any;
}
const startedEntries: LoadEntry[] = [];
const preloadedKeys = new Set(this.registry.keys());
this.assertSettlementHealthy();
this.starting = true;
this.startingEntries = startedEntries;
this.createdRecords = new Set();
this.createdEntries = new Map();
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;
}
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();
}
}
await this.scheduleSettlement();
this.started = true;
return this as any;
} finally {
this.createdEntries = null;
this.createdRecords = null;
this.startingEntries = null;
this.starting = false;
}
}
......
This diff is collapsed.
......@@ -164,15 +164,16 @@ describe('app-context runtime', () => {
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 unstarted = createAppContext()
.provide(CounterService, 1, { provide: 'counter' })
.define();
expect(() => started.use(unstarted)).toThrow(
'Cannot use an unstarted context into a started context.',
);
started.use(unstarted).define();
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 () => {
......
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