Skip to content
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

chore: move initialization into class methods, limit singleton use #170

Merged
merged 12 commits into from
Feb 18, 2025

Conversation

typotter
Copy link
Collaborator

@typotter typotter commented Feb 11, 2025


labels: mergeable

Eppo Internal
🎟️ towards FF-3888
πŸ“œ Multiple Eppo Clients
Configuration Side-loading

Motivation and Context

This PR is carved from #166 in an attempt to make #166 focused on the the "new" API. This PR adds no net new functionality, however, the new EppoJSClient.init method, marked @internal does allow for unbuffered initialization calls just as the global init method previously allowed. This internal method can be made private once we remove the global init method in the next major release.

The init and getInstance() methods work on a singleton instance of the EppoJSClient 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 EppoJSClient 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.

TL;DR: We want to move away from singleton access and allow for multiple instance of the EppoJSClient while maintaining the current API

Description

  • Move bulk of initialization to a class method
  • funnel all references to the static instance through getInstance()
  • deprecate EppoJSClient.instance
  • make initialized and ensureInitialized class members; still set EppoJSClient.initialized as before to ensure backward compatibility with the current API

How has this been documented?

  • doc comments

How has this been tested?

  • tests

@@ -287,308 +584,42 @@ export function buildStorageKeySuffix(apiKey: string): string {
* @public
*/
export function offlineInit(config: IClientConfigSync): EppoClient {
const isObfuscated = config.isObfuscated ?? false;
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

moved to class

Copy link
Contributor

Choose a reason for hiding this comment

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

πŸ”₯

return client;
}

async function explicitInit(config: IClientConfig): Promise<EppoClient> {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

moved to EppoJSClient.init

@typotter typotter changed the title Typo/no singleton chore: move initialization into class methods, limit singleton use Feb 11, 2025
@typotter typotter marked this pull request as ready for review February 11, 2025 23:31
src/index.ts Outdated
* @internal
* @param config
*/
async init(config: IClientConfig): Promise<EppoJSClient> {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

no changes here from explicitInit

* @internal
* @param config
*/
offlineInit(config: IClientConfigSync) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

no changes from offline init

: initFromConfigStoreError
? initFromConfigStoreError
: new Error('Eppo SDK: No configuration source produced a valid configuration');
applicationLogger.warn(
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The diff makes it hard to see what's left in this method;

  1. We keep the forceReinitialize check here to keep it out of the EppoJSClient.init method
  2. Warn if an initialization is currently in progress

@felipecsl
Copy link
Member

Just a thought here, since this will require a major version bump, does it even make sense to deprecate the old APIs?

Since we're doing a major departure in terms of API design, it might not make sense to keep deprecated APIs around, as that will make the internal design a lot more complicated.

Since upgrading will require code changes anyways (assuming it will be a major bump), then we might as well just redesign it as a blank slate, not worrying about deprecations. WDYT?

Copy link
Contributor

@aarsilv aarsilv left a comment

Choose a reason for hiding this comment

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

Thanks for carving out this PR, makes things very clear. I like what I see! πŸš€

Consider throwing a package.json minor version bump in here just so you don't forget πŸ˜›

Comment on lines +104 to +106
/**
* @deprecated. use `getInstance()` instead.
*/
Copy link
Contributor

Choose a reason for hiding this comment

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

Guessing the plan in the future is to make this private? Or have getInstance() construct just-in time if one not set?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Affirmative to making this private, when we make the next major bump. The first step is to encourage everyone to move to the single accessor.

public static initialized = false;

private initialized = false;
Copy link
Contributor

Choose a reason for hiding this comment

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

nice

@@ -287,308 +584,42 @@ export function buildStorageKeySuffix(apiKey: string): string {
* @public
*/
export function offlineInit(config: IClientConfigSync): EppoClient {
const isObfuscated = config.isObfuscated ?? false;
Copy link
Contributor

Choose a reason for hiding this comment

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

πŸ”₯

Copy link
Contributor

@aarsilv aarsilv left a comment

Choose a reason for hiding this comment

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

Thanks for iterating on all the comments!

The one thing I'm still confused about (probably because it's late πŸ˜›) is the deprecation of getInstance(). Are we expecting people to handle managing the instance they get from buildAndInit()? Does this mean that they would need to write their own getInstance() methods and wrapper assignment methods with null checks that return default value if not initialized? If so, I think this may be unnecessary friction and we should think about how to make the 99% use case--a singleton in a code base where they are not concerned about conflicts--as friction-free as possible.

@aarsilv aarsilv self-requested a review February 16, 2025 05:27
Copy link
Collaborator Author

@typotter typotter left a comment

Choose a reason for hiding this comment

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

Just a thought here, since this will require a major version bump, does it even make sense to deprecate the old APIs?
The intent for this PR was no more than a minor bump. Is there a breaking change to the API I overlooked? @felipecsl

Since we're doing a major departure in terms of API design, it might not make sense to keep deprecated APIs around, as that will make the internal design a lot more complicated.

This I agree on, however, that's better left for #166. This PR doesn't change any if the current API, nor deprecates it (without an immediately available equivalent, ex: getInstance instead of EppoJSClient.instance)

Since upgrading will require code changes anyways (assuming it will be a major bump), then we might as well just redesign it as a blank slate, not worrying about deprecations. WDYT?

Again, the intent of this PR is no breaking changes.
The current focus is to deliver on the concrete ask of multiple instances and leveraging the opportunity to to incrementally work towards the ideal interface.
Deprecating with a replacement doesn't require a code change but if one is made to avoid the annotation, it's a very simple change. Accumulating some @deprecateds while we work out what the ideal is seems like a good tradeoff to allow us to continue to ship improvements and bug fixes to the current major version.

Comment on lines +104 to +106
/**
* @deprecated. use `getInstance()` instead.
*/
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Affirmative to making this private, when we make the next major bump. The first step is to encourage everyone to move to the single accessor.

@typotter
Copy link
Collaborator Author

The one thing I'm still confused about (probably because it's late πŸ˜›) is the deprecation of getInstance(). Are we expecting people to handle managing the instance they get from buildAndInit()? Does this mean that they would need to write their own getInstance() methods and wrapper assignment methods with null checks that return default value if not initialized? If so, I think this may be unnecessary friction and we should think about how to make the 99% use case--a singleton in a code base where they are not concerned about conflicts--as friction-free as possible.

@aarsilv no deprecation of getInstance in this PR. Will likely drop that in #166 so we can ship the customer-requested feature and continue discussion around the evolution of the SDK in a non-blocking way.

Copy link
Member

@felipecsl felipecsl left a comment

Choose a reason for hiding this comment

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

I think this is directionally correct, LGTM!

@typotter typotter merged commit a602ba5 into main Feb 18, 2025
8 checks passed
@typotter typotter deleted the typo/no-singleton branch February 18, 2025 17:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants