Skip to content

Offline Init Part 3 of 4: Add offlineInit() for synchronous SDK initialization without network#118

Merged
aarsilv merged 5 commits intomainfrom
aarsilv/ffesupport-461-offline-init-part3-offline-init
Jan 27, 2026
Merged

Offline Init Part 3 of 4: Add offlineInit() for synchronous SDK initialization without network#118
aarsilv merged 5 commits intomainfrom
aarsilv/ffesupport-461-offline-init-part3-offline-init

Conversation

@aarsilv
Copy link
Contributor

@aarsilv aarsilv commented Jan 20, 2026

Summary

  • Adds offlineInit() function to initialize the SDK with pre-fetched configuration
  • Enables SDK usage in environments without network access or for faster startup

Stacked PRs

Test plan

  • Verify offlineInit() initializes the client with provided configuration
  • Verify flag assignments work correctly after offline initialization
  • Verify getInstance() returns the client after offline init

🤖 Generated with Claude Code

@aarsilv aarsilv force-pushed the aarsilv/ffesupport-461-offline-init-part2-get-bandits-configuration branch from 6109625 to 7eb55bb Compare January 20, 2026 03:04
@aarsilv aarsilv force-pushed the aarsilv/ffesupport-461-offline-init-part3-offline-init branch 2 times, most recently from c6bb75d to a769852 Compare January 20, 2026 03:08
@aarsilv aarsilv force-pushed the aarsilv/ffesupport-461-offline-init-part2-get-bandits-configuration branch from 5c56e10 to eae1ff4 Compare January 20, 2026 03:10
@aarsilv aarsilv force-pushed the aarsilv/ffesupport-461-offline-init-part3-offline-init branch from a769852 to b9221c2 Compare January 20, 2026 03:10
@aarsilv aarsilv changed the title Add offlineInit() for synchronous SDK initialization without network Offline Init Part 3 of 4: Add offlineInit() for synchronous SDK initialization without network Jan 20, 2026
@aarsilv aarsilv force-pushed the aarsilv/ffesupport-461-offline-init-part2-get-bandits-configuration branch from eae1ff4 to 3f88511 Compare January 20, 2026 03:20
@aarsilv aarsilv force-pushed the aarsilv/ffesupport-461-offline-init-part3-offline-init branch 2 times, most recently from 7cc0255 to 0466712 Compare January 20, 2026 03:24
@aarsilv aarsilv force-pushed the aarsilv/ffesupport-461-offline-init-part2-get-bandits-configuration branch from 7396a8f to 128bac6 Compare January 20, 2026 03:41
@aarsilv aarsilv force-pushed the aarsilv/ffesupport-461-offline-init-part3-offline-init branch 2 times, most recently from 065c1b6 to 3b021c2 Compare January 20, 2026 03:48
* }
* ```
*/
flagsConfiguration: string;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Opted to deviate from our client JS SDK and follow in the footsteps of python and iOS using the more user-friendly and information-rich wire format.

Comment on lines +55 to +58
/**
* Configuration settings for the event dispatcher.
* @deprecated Eppo has discontinued eventing support. Event tracking will be handled by Datadog SDKs.
*/
Copy link
Contributor Author

Choose a reason for hiding this comment

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

May as well start flagging this as deprecated so we can rip out in a future version! 🪓

Comment on lines +423 to +425
/**
* @deprecated Eppo has discontinued eventing support. Event tracking will be handled by Datadog SDKs.
*/
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Deprecating this event-related method too! 🪓

* @returns the initialized client instance
* @public
*/
export function offlineInit(config: IOfflineClientConfig): EppoClient {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

🚀 The main new method!

@aarsilv aarsilv marked this pull request as ready for review January 20, 2026 04:12
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds an offlineInit() function to enable synchronous SDK initialization using pre-fetched configuration, allowing the SDK to operate without network access or achieve faster startup times.

Changes:

  • Introduces offlineInit() function that parses and loads flag and bandit configurations from JSON strings
  • Adds IOfflineClientConfig interface for offline initialization configuration
  • Extracts DEFAULT_ASSIGNMENT_CACHE_SIZE constant to share between init() and offlineInit()

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
src/index.ts Adds offlineInit() function, IOfflineClientConfig export, and DEFAULT_ASSIGNMENT_CACHE_SIZE constant
src/index.spec.ts Comprehensive test suite for offlineInit() covering initialization, assignment logging, error handling, and bandit support
src/i-client-config.ts Defines IOfflineClientConfig interface with documentation for offline initialization parameters

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

src/index.ts Outdated
Comment on lines 335 to 336
// Note: setEntries is async but MemoryOnlyConfigurationStore performs synchronous operations internally,
// so there's no race condition. We add .catch() for defensive error handling, matching JS client SDK pattern.
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

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

The comment claims setEntries is async but MemoryOnlyConfigurationStore performs synchronous operations internally. This is confusing and potentially misleading. If the operations are truly synchronous, the method shouldn't return a promise. Consider clarifying whether this is actually asynchronous or if the promise is just for API consistency.

Suggested change
// Note: setEntries is async but MemoryOnlyConfigurationStore performs synchronous operations internally,
// so there's no race condition. We add .catch() for defensive error handling, matching JS client SDK pattern.
// Note: setEntries returns a Promise for API consistency, but MemoryOnlyConfigurationStore
// performs its work synchronously, so there is no race condition. We still attach .catch()
// for defensive error handling in case the Promise is ever rejected.

Copilot uses AI. Check for mistakes.
src/index.ts Outdated
Comment on lines 339 to 341
.catch((err) =>
applicationLogger.warn(`Error setting flags for memory-only configuration store: ${err}`),
);
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

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

The error message uses string interpolation with ${err} which will result in "[object Object]" for Error objects. Use err.message or handle the error object properly to provide a meaningful error message.

Suggested change
.catch((err) =>
applicationLogger.warn(`Error setting flags for memory-only configuration store: ${err}`),
);
.catch((err) => {
const message = err instanceof Error ? err.message : String(err);
applicationLogger.warn(
`Error setting flags for memory-only configuration store: ${message}`,
);
});

Copilot uses AI. Check for mistakes.
src/index.ts Outdated
Comment on lines 381 to 387
banditModelConfigurationStore
.setEntries(banditsConfigResponse.bandits ?? {})
.catch((err) =>
applicationLogger.warn(
`Error setting bandit models for memory-only configuration store: ${err}`,
),
);
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

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

The error message uses string interpolation with ${err} which will result in "[object Object]" for Error objects. Use err.message or handle the error object properly to provide a meaningful error message.

Copilot uses AI. Check for mistakes.
src/index.ts Outdated
Comment on lines 311 to 323
const flagsConfigResponse = JSON.parse(flagsConfiguration) as {
createdAt?: string;
format?: string;
environment?: { name: string };
flags: Record<string, Flag>;
banditReferences?: Record<
string,
{
modelVersion: string;
flagVariations: BanditVariation[];
}
>;
};
Copy link
Contributor

@greghuels greghuels Jan 20, 2026

Choose a reason for hiding this comment

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

There should never be a situation where a configuration object is missing a key, so why not just validate that each field exists and throw or log an "invalid configuration" error if it's missing anything? That way you can cast to IUniversalFlagConfigResponse for valid configurations and you can remove the guards below. The main benefit here is that you reduce the chance that an invalid configuration causes issues in other parts of the SDK that expect certain fields to be set.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Great call! Will go beyond just parsing to checking expected keys are all there.

Copy link
Contributor

@greghuels greghuels left a comment

Choose a reason for hiding this comment

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

Main issue is that I don't think we shouldn't allow invalid configurations

@greghuels greghuels assigned aarsilv and unassigned greghuels Jan 21, 2026
@aarsilv aarsilv force-pushed the aarsilv/ffesupport-461-offline-init-part2-get-bandits-configuration branch from 1eb8a4e to 87e6424 Compare January 26, 2026 02:28
@aarsilv aarsilv force-pushed the aarsilv/ffesupport-461-offline-init-part3-offline-init branch 2 times, most recently from 4bd2bc5 to ae372fa Compare January 26, 2026 02:31
@aarsilv aarsilv force-pushed the aarsilv/ffesupport-461-offline-init-part2-get-bandits-configuration branch from b235e30 to c35ea62 Compare January 26, 2026 02:48
@aarsilv aarsilv force-pushed the aarsilv/ffesupport-461-offline-init-part3-offline-init branch 2 times, most recently from 96ca311 to 5a34532 Compare January 26, 2026 03:05
try {
// Parse and validate the flags configuration JSON
const parsedFlagsConfig = JSON.parse(flagsConfiguration);
const flagsValidationErrors = validateFlagsConfiguration(parsedFlagsConfig);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@greghuels Claude whipped this up to ensure we meet the type so we don't need to be super defensive with checking field existence per your suggestion

Copy link
Contributor

Choose a reason for hiding this comment

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

Nice, thanks for updating!

@aarsilv aarsilv requested a review from greghuels January 26, 2026 03:15
@aarsilv aarsilv assigned greghuels and unassigned aarsilv Jan 26, 2026
@greghuels greghuels assigned aarsilv and unassigned greghuels Jan 26, 2026
@aarsilv aarsilv force-pushed the aarsilv/ffesupport-461-offline-init-part2-get-bandits-configuration branch from 291bc6f to f3ee984 Compare January 27, 2026 00:12
Base automatically changed from aarsilv/ffesupport-461-offline-init-part2-get-bandits-configuration to main January 27, 2026 00:13
aarsilv and others added 5 commits January 26, 2026 19:14
Adds the ability to initialize the SDK with pre-fetched configuration JSON,
enabling:
- Synchronous initialization without network requests
- Use cases where configuration is bootstrapped from another source
- Edge/serverless environments where polling isn't desired

Also includes:
- IOfflineClientConfig interface for offline initialization options
- DEFAULT_ASSIGNMENT_CACHE_SIZE constant for consistent cache sizing
- Deprecation annotations on event tracking (discontinued feature)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Use realistic bandit model coefficients from bandit-models-v1.json and
test subject "alice" from test-case-banner-bandit.json to verify that
getBanditAction returns the expected variation and action after offline
initialization.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Rename "makes client available via getInstance()" to "can request assignment"
- Move "no network requests" test into "basic initialization" section
- Add assignment verification to "empty flags configuration" test

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add validateFlagsConfiguration() to ensure all required fields exist
- Add validateBanditsConfiguration() to validate bandit config structure
- Update offlineInit() to validate configs before loading:
  - If validation fails and throwOnFailedInitialization=true: throw error
  - If validation fails and throwOnFailedInitialization=false: log warning
    and use empty config (all assignments return defaults)
- Update test helper to include banditReferences field

This ensures invalid configurations are caught early and provides clear
error messages about what fields are missing or invalid.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Reorder validation functions to follow clean code principles (high-level methods first)
- Fix ${err} string interpolation to properly extract error messages

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@aarsilv aarsilv force-pushed the aarsilv/ffesupport-461-offline-init-part3-offline-init branch from 75ac5ee to 6111973 Compare January 27, 2026 00:14
@aarsilv aarsilv merged commit c266cf1 into main Jan 27, 2026
8 checks passed
@aarsilv aarsilv deleted the aarsilv/ffesupport-461-offline-init-part3-offline-init branch January 27, 2026 00:16
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.

2 participants