Skip to content

feat: add EnvironmentDocumentHandler for offline handlers without a filesystem - #299

Open
justin-masse wants to merge 1 commit into
Flagsmith:mainfrom
justin-masse:feat/environment-document-offline-handler
Open

justin-masse wants to merge 1 commit into
Flagsmith:mainfrom
justin-masse:feat/environment-document-offline-handler

Conversation

@justin-masse

Copy link
Copy Markdown

Thanks for submitting a PR! Please check the boxes below:

  • I have read the Contributing Guide.
  • I have added information to docs/ if required so people know about the feature.
  • I have filled in the "Changes" section below.
  • I have filled in the "How did you test this code" section below.

Changes

Contributes to #187

Custom offline handlers are documented — subclass BaseOfflineHandler, return an EnvironmentModel — but in practice you can only build one if the environment document comes from disk.

The engine needs the API's JSON rehydrated into its model graph by buildEnvironmentModel. That function isn't exported from the package root, and it can't be deep-imported either, because the exports map has no subpaths:

"exports": { "import": "./build/esm/index.js", "require": "./build/cjs/index.js" }

So LocalFileHandler is the only public route to a usable handler. If your environment document lives anywhere else, you end up writing it to a temp file purely to read it back — which is what we're currently doing on AWS Lambda, where the document comes from DynamoDB and the filesystem round-trip is pure overhead.

This adds EnvironmentDocumentHandler, which takes an already-parsed environment document:

import { Flagsmith, EnvironmentDocumentHandler } from 'flagsmith-nodejs';

const document = await loadEnvironmentDocumentFromWherever();

const flagsmith = new Flagsmith({
    offlineMode: true,
    offlineHandler: new EnvironmentDocumentHandler(document)
});

LocalFileHandler now extends it, so reading from a file becomes the filesystem-shaped case of loading a document rather than a separate implementation. Its behaviour and public surface are unchanged — same constructor, same environment property, still an instanceof BaseOfflineHandler.

This follows on from #187, which exported FeatureModel for the same reason: the models a handler returns are public, but the function that builds one from your API's response isn't, so the job can't quite be finished. Happy to take this in a different direction if you'd prefer — exporting buildEnvironmentModel directly would also solve it, and I'm not attached to the class name.

Notes

  • No new dependencies, and no behaviour change to existing code paths.
  • The constructor takes object rather than any, so a parsed document passes straight through while a stray string or path doesn't.

How did you test this code?

npx vitest run — 188 passing, up from 185. Three new cases in tests/sdk/offline-handlers.test.ts:

  1. EnvironmentDocumentHandler builds a valid EnvironmentModel from a document, asserting on apiKey the same way the existing LocalFileHandler test does.
  2. It touches no filesystem — fs.readFileSync is spied on and asserted not called.
  3. LocalFileHandler is an instanceof EnvironmentDocumentHandler, covering the refactor.

The existing LocalFileHandler test is unchanged and still passes, which is the compatibility check.

Two pre-existing failures are unrelated to this change and reproduce identically on a clean checkout: tests/engine/e2e/engine.test.ts (needs live credentials) and some pino type errors during tsc that come from resolving dependencies without your package-lock.json.

npx prettier --check passes on all three changed files.

…ilesystem

Custom offline handlers are documented, but building one is only possible if
the environment document comes from disk: the engine needs the API's JSON
rehydrated by buildEnvironmentModel, which is not exported from the package
root and cannot be deep-imported because the exports map has no subpaths. The
only public route is LocalFileHandler.

EnvironmentDocumentHandler takes an already-parsed environment document, so a
handler can load it from anywhere - an object store, a cache, a database, or a
document embedded in the deployment - and hand it straight to the SDK.

LocalFileHandler now extends it and keeps its existing behaviour and public
surface, so reading from a file is just the filesystem-shaped case of loading
a document.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@justin-masse
justin-masse requested a review from a team as a code owner September 16, 2026 18:16
@justin-masse
justin-masse requested review from matthewelwell and removed request for a team September 16, 2026 18: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.

1 participant