-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
provide singletons #46
base: master
Are you sure you want to change the base?
Changes from all commits
b0d6c5f
b87ccf9
9441f10
471c9ea
a4ae359
71d67b2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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. | ||
} | ||
} | ||
|
||
/** | ||
|
@@ -102,6 +109,42 @@ 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); | ||
} | ||
|
||
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; | ||
} | ||
} | ||
|
||
/** | ||
* Ensures that the singleton is provided; throws if it's not. | ||
* | ||
* Use this to detect unprovided but used singletons early. | ||
*/ | ||
requireSingleton<T>(Klass: ConstructorOrFactory<App, T>): void { | ||
if (!this.isSingletonProvided(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. | ||
|
@@ -115,6 +158,12 @@ export class App { | |
if (this.hasSingleton(Klass)) { | ||
return this.getExistingSingleton(Klass); | ||
} | ||
if (!this.isSingletonProvided(Klass)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
} | ||
|
||
|
@@ -170,10 +219,16 @@ export class App { | |
* While singleton classes are typically side effect free and can be instantiated lazily when | ||
* first requested, plugins have side-effects, such as adding router routes, adding RPC endpoints | ||
* or setting up event listeners. The load method is therefore used to load those plugins. | ||
* | ||
* You can load a plugin only once; load throws an error the second time. | ||
*/ | ||
load<T>(Klass: ConstructorOrFactory<App, T>): void { | ||
this.getSingleton(Klass); // force initialization; | ||
return; | ||
if (this.isSingletonProvided(Klass)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In this case it should be just getSingleton (multiple loads should be ok) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we need to re-do this with a bit of thinking. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Redo this PR or the loading-once-or-twice mechanics? Fine with both really. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need to think whether we want to allow loading plugins within plugins, because thats what implies multiple load calls are OK. |
||
throw new Error(`Singleton ${Klass.name} is already initialized.`); | ||
} else { | ||
this.provideSingleton(Klass); | ||
this.getSingleton(Klass); // force initialization | ||
} | ||
} | ||
|
||
/** | ||
|
@@ -298,6 +353,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>; | ||
|
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.