Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 24 additions & 8 deletions packages/core/src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class UserProvider extends BaseService {
describe('Instantiation', () => {
it('#getSingleton should instantiate a singleton class once', () => {
let app = new App();
app.provideSingleton(Database);
let db1 = app.getSingleton(Database);
let db2 = app.getSingleton(Database);
expect(db1.id).toEqual(db2.id);
Expand All @@ -34,6 +35,8 @@ describe('Instantiation', () => {
it('#getSingleton should instantiate a singleton factory once', () => {
let app = new App();
let factoryFunc = () => app.getSingleton(Database);
app.provideSingleton(Database);
app.provideSingleton(factoryFunc);
let db1 = app.getSingleton(factoryFunc);
let db2 = app.getSingleton(factoryFunc);
expect(db1.id).toEqual(db2.id);
Expand All @@ -42,21 +45,23 @@ describe('Instantiation', () => {
it('#load should force a singleton to instantitate', () => {
let app = new App();
let dbDidInit: boolean = false;
app.load(
class MyDB extends Database {
constructor(app: App) {
super(app);
dbDidInit = true;
}
},
);
app.provideSingleton(Database);
class MyDB extends Database {
constructor(app: App) {
super(app);
dbDidInit = true;
}
}
app.provideSingleton(MyDB);
app.load(MyDB);
expect(dbDidInit).toBe(true);
});
});

describe('Overrides', () => {
it('#getSingleton should use and respect singleton overrides', () => {
let app = new App();
app.provideSingleton(Database);
app.overrideSingleton(
Database,
class MockDb extends Database {
Expand Down Expand Up @@ -98,6 +103,7 @@ describe('Overrides', () => {

it('#clearSingletonOverrides should cause original singletons to instantiate', () => {
let app = new App();
app.provideSingleton(Database);
app.overrideSingleton(
Database,
class MockDb extends Database {
Expand All @@ -112,13 +118,15 @@ describe('Overrides', () => {
describe('App nesting', () => {
it('#getSingleton should find instantiated singletons in a parent app', () => {
let app = new App();
app.provideSingleton(Database);
let dbId = app.getSingleton(Database).id;
let childApp = app.createChildApp();
expect(childApp.getSingleton(Database).id).toEqual(dbId);
});

it('#getSingleton should instantiate non-existing singletons in the child app, not parent', () => {
let parentApp = new App();
parentApp.provideSingleton(Database);
let childApp = parentApp.createChildApp();
let childDbId = childApp.getSingleton(Database).id;
expect(parentApp.getSingleton(Database).id !== childDbId);
Expand Down Expand Up @@ -174,3 +182,11 @@ describe('Context disposal', () => {
);
});
});

describe('providing dependencies', () => {
it('should throw when loading', () => {
let app = new App();
class MySingleton extends AppSingleton {}
expect(() => app.getSingleton(MySingleton)).toThrowError();
});
});
48 changes: 48 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,18 @@ export type PublicInterface<T> = { [K in keyof T]: T[K] };
*/
export class App {
private singletonLocator: Locator<this>;
private providedSingletons: WeakMap<ConstructorOrFactory<App, any>, boolean> = new WeakMap();
parentApp: App | null;

constructor(opts: { parentApp?: App } = {}) {
this.parentApp = opts.parentApp ? opts.parentApp : null;
this.singletonLocator = new Locator(this, s => '__appSingleton' in s);

if (!opts.parentApp) {
// we must provide this, otherwise withServiceContext will fail every time.
// we do it once, on the parent app, because child app construction will fail otherwise.
this.provideSingleton(ServiceContextEvents); // otherwise, withServiceContext fails.
}
}

/**
Expand Down Expand Up @@ -102,6 +109,37 @@ export class App {
}
}

/**
* Registers a singleton as "provided". It's informing the application that a plugin
* agreed to expose that singleton.
*
* Calls to app.getSingleton(UnprovidedService) will fail with an error.
*
* Also see: `App#requireSingleton`
*/
provideSingleton<T>(Klass: ConstructorOrFactory<App, T>): void {
if (this.isSingletonProvided(Klass)) {
throw new Error(`The singleton ${Klass.name} is already provided.`);
}
this.providedSingletons.set(Klass, true);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we refactor so the same map is used for provides and overrides?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

on second thought maybe not, you might want to override before providing.

}

private isSingletonProvided<T>(Klass: ConstructorOrFactory<App, T>): boolean {
if (this.providedSingletons.has(Klass)) {
return true;
} else if (this.parentApp) {
return this.parentApp.isSingletonProvided(Klass);
} else {
return false;
}
}

requireSingleton<T>(Klass: ConstructorOrFactory<App, T>): void {
if (!this.providedSingletons.has(Klass)) {
throw new Error(`The singleton ${Klass} is required, but wasnt provided.`);
}
}

/**
* Returns an instance of the singleton, if it exists somewhere here or
* in some of the parent apps. If it doesn't it's created in this app.
Expand All @@ -115,6 +153,12 @@ export class App {
if (this.hasSingleton(Klass)) {
return this.getExistingSingleton(Klass);
}
if (!this.isSingletonProvided(Klass)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe at locator level.

throw new Error(`Singleton ${Klass.name} wasnt provided`);
// console.warn(`The singleton ${Klass} was constructed, but wasn't provided beforehand.`);
// console.warn(`Please provide it explicitly using "App#provideSingleton(${Klass})".`);
// console.warn('In the future, this will be an error.');
}
return this.singletonLocator.get(Klass);
}

Expand Down Expand Up @@ -298,6 +342,10 @@ export class ServiceContext {
getSingleton<T>(SingletonClass: ConstructorOrFactory<App, T>): T {
return this.app.getSingleton(SingletonClass);
}

requireSingleton<T>(SingletonClass: ConstructorOrFactory<App, T>) {
return this.app.requireSingleton(SingletonClass);
}
}

type ContextListener = (serviceCtx: ServiceContext, error: Error | null) => PromiseLike<void>;
Expand Down