feat: Adds feature flags runtime client with local evaluation#1511
feat: Adds feature flags runtime client with local evaluation#1511stanleyphu merged 16 commits intomainfrom
Conversation
Add interfaces for the runtime client: EvaluationContext, FlagPollEntry, FlagPollResponse, RuntimeClientOptions, RuntimeClientStats, and RuntimeClientLogger. These define the contract for the polling-based feature flag client. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Simple in-memory store that holds the polling response. Supports atomic swap of the full flag map, O(1) lookup by slug, and size tracking. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Synchronous flag evaluation against the in-memory store. Evaluation order: flag not found → defaultValue, flag disabled → false, org target match → target.enabled, user target match → target.enabled, no match → default_value. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
EventEmitter-based client that polls GET /sdk/feature-flags, caches flags in memory, and evaluates locally. Features: configurable polling interval with jitter, per-request timeout via Promise.race, bootstrap flags, waitUntilReady with timeout, change events on subsequent polls, and 401 detection that stops polling and emits 'failed'. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds createRuntimeClient() method to FeatureFlags module, allowing users to create a polling-based runtime client via workos.featureFlags.createRuntimeClient(). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds exponential backoff (1s base, 2x multiplier, 60s cap) when polls fail consecutively. The backoff delay is used when it exceeds the normal polling interval, and resets after a successful poll. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
User-level targets are more specific than organization-level targets and should take precedence when both are provided in the evaluation context. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tion Defer the first poll via setTimeout(0) so callers can attach error/failed event listeners before the initial network request fires. Replace JSON.stringify-based flag comparison with field-by-field checks to avoid false change events from key ordering differences. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Introduces a pollAbortController that races alongside the fetch and timeout promises. close() now aborts any in-flight request immediately instead of letting it complete silently in the background. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add explicit return type to getFlag() and return a shallow copy from InMemoryStore.getAll() to prevent external mutation of internal state. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
859c04e to
6d1c765
Compare
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Greptile SummaryThis PR introduces Key changes:
Issues found:
Confidence Score: 4/5Safe to merge after addressing the P1 One P1 defect remains: src/feature-flags/runtime-client.ts — specifically the Important Files Changed
Sequence DiagramsequenceDiagram
participant Caller
participant Client as FeatureFlagsRuntimeClient
participant Store as InMemoryStore
participant Evaluator
participant API as WorkOS API
Caller->>Client: createRuntimeClient(options)
Note over Client: Bootstrap flags → store.swap(), resolveReady()
Client-->>Client: setTimeout(() => poll(), 0)
Client->>API: GET /sdk/feature-flags
alt success
API-->>Client: FlagPollResponse
Client->>Store: store.swap(data)
Client-->>Client: resolveReady()
Client-->>Caller: emit('change', ...) [if 2nd+ poll]
Client-->>Client: scheduleNextPoll(baseDelay + jitter)
else error (non-401)
API-->>Client: Error
Client-->>Caller: emit('error', err)
Client-->>Client: scheduleNextPoll(backoff + jitter)
else 401 Unauthorized
API-->>Client: UnauthorizedException
Client-->>Caller: emit('error', err)
Client-->>Caller: emit('failed', err)
Client-->>Client: readyReject(err) [if not initialized]
Note over Client: Polling stops permanently
end
Caller->>Client: waitUntilReady({ timeoutMs? })
Client-->>Caller: readyPromise (resolves or rejects)
Caller->>Client: close()
Client-->>Client: closed=true, abort in-flight, clearTimer, removeAllListeners
|
…t comparison Prevent unhandled 'error' event crash when close() aborts an in-flight poll by short-circuiting the catch block when closed. Switch target comparison to a Map-based lookup so reordered targets from the server don't trigger spurious change events. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
@greptile review |
If the first poll returns a 401, the ready promise was never settled, causing waitUntilReady() without a timeout to hang indefinitely. Now reject the ready promise with the UnauthorizedException so callers get a clear error instead of hanging. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
@greptile review |
Description
FeatureFlagsRuntimeClient, a polling-based client that fetches all flag configurations from/sdk/feature-flagsand evaluates them locally in-memory. Targeting rules (org-level and user-level) are resolved client-side with zero per-request API overhead.workos.featureFlags.createRuntimeClient(options?).Implementation Details
InMemoryStore— atomic-swap key/value store for flag dataEvaluator— stateless evaluation: flag enabled → org target → user target → default valueFeatureFlagsRuntimeClient— orchestrates polling, caching, and evaluation'failed'eventwaitUntilReady()with optional timeout'change'events when flags are added, modified, or removedgetStats()for observabilityUsage
Basic usage
With targeting context
Configuration options
Reacting to changes
Evaluating all flags at once
Observability
Cleanup
Documentation
Does this require changes to the WorkOS Docs? E.g. the API Reference or code snippets need updates.
If yes, link a related docs PR and add a docs maintainer as a reviewer. Their approval is required.