-
Notifications
You must be signed in to change notification settings - Fork 0
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
feat: buildAndInit for isolated client instances #166
base: main
Are you sure you want to change the base?
Conversation
5c466e9
to
a73bf0c
Compare
src/index.ts
Outdated
} | ||
} | ||
|
||
public waitForReady(): Promise<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.
new method! π
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.
what does "ready" mean in this context? adding comments is likely to help, also we should always do it for public APIs anyways
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.
renamed to waitForInitialization
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.
waitForConfiguration
to be in line with multiplatform's analog PollerThread::waitForConfiguration
src/index.ts
Outdated
if (!EppoJSClient.initialized) { | ||
private ensureInitialized() { | ||
if (!this.initialized) { | ||
// TODO: check super.isInitialized? |
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.
EppoClient.initialized
also exists, but means something different than it does here.
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.
what does it mean and how is that different than this? consider renaming either if so
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.
the super method checks that all the config stores are initialized while this this.initialized
is true when all the initialization workload is done so they are pretty much the same.
I think we can move to deprecate EppoJSClient.initialized
and defer to super.isInitialized()
.
src/index.ts
Outdated
/** | ||
* Tracks pending initialization. After an initialization completes, the value is removed from the map. | ||
*/ | ||
private static initializationPromise: Promise<EppoJSClient> | null = null; | ||
|
||
/** | ||
* This method is part of a bridge from using a singleton to independent instances. More specifically, it exists so | ||
* that the init method can access the private field, `readyResolver`. It should not be called by any | ||
* methods other than the `init` method. There are limited guards in place; the behaviour if called inappropriately | ||
* is undefined. | ||
* | ||
* It also keeps code that relies on internal details of EppoJSClient colocated in the class. | ||
* | ||
* @internal | ||
* | ||
* @param client | ||
* @param config | ||
*/ | ||
static async initializeClient( | ||
client: EppoJSClient, | ||
config: IClientConfig, | ||
): Promise<EppoJSClient> { | ||
validation.validateNotBlank(config.apiKey, 'API key required'); | ||
|
||
// If there is already an init in progress for this apiKey, return that. | ||
if (!EppoJSClient.initializationPromise) { | ||
EppoJSClient.initializationPromise = explicitInit(config, client).then((client) => { | ||
// Resolve the ready promise if it exists | ||
if (client.readyPromiseResolver) { | ||
client.readyPromiseResolver(); | ||
client.readyPromiseResolver = null; | ||
} | ||
return client; | ||
}); | ||
} | ||
|
||
const readyClient = await EppoJSClient.initializationPromise; | ||
EppoJSClient.initializationPromise = null; | ||
|
||
return readyClient; | ||
} | ||
} |
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.
This is the old init
method and initialization buffer tracker.
Moved the method to a static member of EppoJSClient
so that it can access the readyResolver
and notify waitForReady
src/index.ts
Outdated
/** | ||
* Initializes the Eppo client with configuration parameters. | ||
* This method should be called once on application startup. | ||
* If an initialization is in process, calling `init` will return the in-progress | ||
* `Promise<EppoClient>`. Once the initialization completes, calling `init` again will kick off the | ||
* initialization routine (if `forceReinitialization` is `true`). | ||
* | ||
* | ||
* @deprecated |
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.
controversial, I know.
willing to negotiate
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.
Seems like a minor change for the end user if the argument interface stays identical
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.
The @deprecated
annotation is here to help push devs to using the new construct + wait paradigm. This method will be removed and the argument interface for the new constructor is slightly different sdkKey
vs apiKey
Several false starts and iterations later, we're ready for another review. Note: this PR is now stacked on #170 where changes are made just to how the singleton is referenced, leaving the focus of this PR on |
@@ -165,4 +189,77 @@ export interface IClientConfig extends IBaseRequestConfig { | |||
*/ | |||
maxQueueSize?: number; | |||
}; | |||
}; | |||
|
|||
export type IStorageOptions = { |
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.
Perfect thanks! And now that I think about it if the have the same API it's actually probably not too bad if they are messing with eachother's persistent storage anyways as they should be using same configuration
src/index.ts
Outdated
|
||
/** | ||
* Resolved when the client is initialized | ||
* @private |
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.
super minor, but does this annotation really add anything given the private
keyword two lines later?
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.
nope. eager IDE.
src/index.ts
Outdated
@@ -620,6 +657,7 @@ export async function init(config: IClientConfig): Promise<EppoJSClient> { | |||
/** | |||
* Used to access a singleton SDK client instance. | |||
* Use the method after calling init() to initialize the client. | |||
* @deprecated |
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.
consider adding what the user should do instead after @deprecated
(e.g., why is this deprecated)
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.
Undeprecated the singleton (for now)
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.
Main feedback is around documentation and providing alternative APIs for what's been deprecated. I'm ready to approve after that, great work!
@@ -4,6 +4,10 @@ | |||
|
|||
## EppoJSClient.initialized property | |||
|
|||
> Warning: This API is now obsolete. |
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.
did you mean deprecated
instead of obsolete
?
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.
I'm not sure whereyarn docs
is getting this text from
docs/js-client-sdk.getinstance.md
Outdated
@@ -4,6 +4,10 @@ | |||
|
|||
## getInstance() function | |||
|
|||
> Warning: This API is now obsolete. |
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.
same question
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.
also whenever deprecating something, we should always propose an alternative so users are not left guessing.
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.
This file is auto-generated, so the text is out of my hands. I have, however, updated all of the deprecated tags with an alternative. thank you
*/ | ||
forceReinitialize?: boolean; | ||
|
||
export type IEventOptions = { |
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.
this needs docs
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.
added
@@ -165,4 +189,77 @@ export interface IClientConfig extends IBaseRequestConfig { | |||
*/ | |||
maxQueueSize?: number; | |||
}; | |||
}; | |||
|
|||
export type IStorageOptions = { |
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.
documentation for this type missing
src/i-client-config.ts
Outdated
ILoggers & | ||
IEventOptions & | ||
IStorageOptions & | ||
IPollingOptions; |
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.
i love this! thanks for organizing
src/index.ts
Outdated
@@ -109,10 +111,57 @@ export class EppoJSClient extends EppoClient { | |||
isObfuscated: true, | |||
}); | |||
|
|||
/** | |||
* @deprecated |
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.
alternative?
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.
documented!
src/index.ts
Outdated
* Resolved when the client is initialized | ||
* @private | ||
*/ | ||
private readonly initializedPromise: Promise<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.
nit: i'd rename to initializationPromise
or simply initPromise
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.
good point. It's not an initialized promise, it's a promise signalling when it is initialized.
Thank you, @rasendubi @felipecsl @aarsilv for you reviews and patience as we explore this change. I have made a key change - the singleton PTAL |
* Initialization happens outside the constructor, so we can't assign `initPromise` to the result | ||
* of initialization. Instead, we call the resolver when `init` is complete. | ||
*/ | ||
private initializedPromiseResolver: () => void = () => null; |
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.
nit: initPromiseResolver
labels: mergeable
Eppo Internal
ποΈ Fixes: FF-3888
π Multiple Eppo Clients
Configuration Side-loading
Motivation and Context
The
init
andgetInstance()
methods work on a singleton instance of theEppoClient
which makes for a generally smooth developer experience given that the Eppo SDK essentially handles dependency management for the dev. This does have its own drawbacks, but those are not particularly relevant here.There are use cases, however, when a non-singleton instance of the
EppoClient
is required. One such use case is embedding the Eppo SDK into a library which itself can be included in other applications. If that 3p library shipped with Eppo is integrated into another application that has the Eppo SDK running, there would be undefined behaviour as the SDK cannot handle this use case.The
init
method (and class constructors) forEppoClient
and subclasses have evolved organically over time, adding new options as new features are added to the clients. The very large options type is beginning to become a little untenable and disorganized so we take the opportunity to clean that up a bit here.There are other limitations and drawbacks to the current model of instantiating an
EppoClient
statically and then initializing it when the code callsinit
including the awkward coupling of needing to wait for init to resolve in order to get a reference to an initialized client. We have an opportunity to decouple initialization and waiting to make for a better DX (in addition to giving the dev full control over managing theEppoClient
reference (intrinsically allows for easier mocking in tests and will be consistent with the host applications existing DI approach).This change must be done without a major version bump and completely preserve the existing singleton API
Description
IClientConfig
name (this keeps the change backwards compatible while offering a big win in option clarity)- New method:
buildAndInit
which creates an isolated client instance.- new method:
waitForConfiguration
which resolves when the first config is loaded.How has this been documented?
How has this been tested?