@flipfeatureflag/core 0.1.7 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flipfeatureflag/core",
3
- "version": "0.1.7",
3
+ "version": "0.1.10",
4
4
  "description": "flipFeatureFlag core SDK",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.mjs",
@@ -16,5 +16,8 @@
16
16
  "scripts": {
17
17
  "build": "tsup src/index.ts --format esm,cjs --dts",
18
18
  "test": "vitest run"
19
- }
19
+ },
20
+ "files": [
21
+ "dist"
22
+ ]
20
23
  }
package/src/client.ts DELETED
@@ -1,235 +0,0 @@
1
- import { Emitter } from "./emitter";
2
- import { HttpClient } from "./http";
3
- import { buildMetricEvent, buildMetricsRequest, MetricsBuffer } from "./metrics";
4
- import {
5
- FlagEvaluationResult,
6
- FlagValue,
7
- FlagsStatus,
8
- FlipFlagClientOptions,
9
- FlipFlagEvent,
10
- FlipFlagListener,
11
- SdkEvaluateResponse,
12
- SdkFlagEvaluation,
13
- SdkContext,
14
- } from "./types";
15
-
16
- const DEFAULT_REFRESH_MS = 15000;
17
- const DEFAULT_METRICS_MS = 60000;
18
-
19
- export class FlipFlagClient {
20
- private options: Required<Pick<FlipFlagClientOptions, "url" | "sdkKey">> & FlipFlagClientOptions;
21
- private http: HttpClient;
22
- private emitter = new Emitter();
23
- private status: FlagsStatus = { ready: false };
24
- private evaluations = new Map<string, SdkFlagEvaluation>();
25
- private refreshTimer?: ReturnType<typeof setInterval>;
26
- private metricsTimer?: ReturnType<typeof setInterval>;
27
- private metricsBuffer = new MetricsBuffer();
28
- private context: SdkContext;
29
- private started = false;
30
- private refreshInFlight?: Promise<void>;
31
- private pendingFlagKeys = new Set<string>();
32
- private pendingFullRefresh = false;
33
-
34
- constructor(options: FlipFlagClientOptions) {
35
- if (!options.url) {
36
- throw new Error("flipFeatureFlag SDK requires url");
37
- }
38
- if (!options.sdkKey) {
39
- throw new Error("flipFeatureFlag SDK requires sdkKey");
40
- }
41
-
42
- this.options = {
43
- refreshIntervalMs: DEFAULT_REFRESH_MS,
44
- metricsIntervalMs: DEFAULT_METRICS_MS,
45
- ...options,
46
- } as Required<Pick<FlipFlagClientOptions, "url" | "sdkKey">> & FlipFlagClientOptions;
47
-
48
- this.context = options.context ?? {};
49
-
50
- const fetchFn = options.fetch ?? fetch;
51
- this.http = new HttpClient({
52
- baseUrl: this.options.url,
53
- sdkKey: this.options.sdkKey,
54
- fetchFn,
55
- });
56
- }
57
-
58
- async start(): Promise<void> {
59
- if (this.started) {
60
- return;
61
- }
62
- this.started = true;
63
- await this.refresh();
64
-
65
- if (!this.options.disableRefresh) {
66
- this.refreshTimer = setInterval(() => {
67
- void this.refresh();
68
- }, this.options.refreshIntervalMs);
69
- }
70
-
71
- if (!this.options.disableMetrics) {
72
- this.metricsTimer = setInterval(() => {
73
- void this.flushMetrics();
74
- }, this.options.metricsIntervalMs);
75
- }
76
- }
77
-
78
- stop() {
79
- if (this.refreshTimer) {
80
- clearInterval(this.refreshTimer);
81
- this.refreshTimer = undefined;
82
- }
83
- if (this.metricsTimer) {
84
- clearInterval(this.metricsTimer);
85
- this.metricsTimer = undefined;
86
- }
87
- this.started = false;
88
- }
89
-
90
- on(event: FlipFlagEvent, listener: FlipFlagListener) {
91
- this.emitter.on(event, listener);
92
- }
93
-
94
- off(event: FlipFlagEvent, listener: FlipFlagListener) {
95
- this.emitter.off(event, listener);
96
- }
97
-
98
- getStatus(): FlagsStatus {
99
- return { ...this.status };
100
- }
101
-
102
- getContext(): SdkContext {
103
- return { ...this.context };
104
- }
105
-
106
- updateContext(partial: SdkContext) {
107
- this.context = {
108
- ...this.context,
109
- ...partial,
110
- attributes: {
111
- ...(this.context.attributes ?? {}),
112
- ...(partial.attributes ?? {}),
113
- },
114
- };
115
- void this.refresh();
116
- }
117
-
118
- getAllFlags(): Record<string, SdkFlagEvaluation> {
119
- const result: Record<string, SdkFlagEvaluation> = {};
120
- for (const [key, evaluation] of this.evaluations.entries()) {
121
- result[key] = { ...evaluation };
122
- }
123
- return result;
124
- }
125
-
126
- isEnabled(flagKey: string, defaultValue: boolean = false): boolean {
127
- const evaluation = this.evaluate(flagKey, defaultValue);
128
- return Boolean(evaluation.value);
129
- }
130
-
131
- getVariant(flagKey: string, defaultValue: FlagValue = false): SdkFlagEvaluation {
132
- return this.evaluate(flagKey, defaultValue);
133
- }
134
-
135
- private evaluate(flagKey: string, defaultValue: FlagValue): SdkFlagEvaluation {
136
- const cached = this.evaluations.get(flagKey);
137
- if (cached) {
138
- return this.recordEvaluation(flagKey, cached);
139
- }
140
-
141
- if (!this.options.disableRefresh) {
142
- void this.refresh([flagKey]);
143
- }
144
- return this.recordEvaluation(flagKey, {
145
- value: defaultValue,
146
- variationKey: "default",
147
- reason: "default",
148
- });
149
- }
150
-
151
- private recordEvaluation(flagKey: string, evaluation: SdkFlagEvaluation): SdkFlagEvaluation {
152
- this.evaluations.set(flagKey, evaluation);
153
- this.metricsBuffer.add(
154
- buildMetricEvent(flagKey, evaluation.variationKey, evaluation.reason, this.context),
155
- );
156
- return evaluation;
157
- }
158
-
159
- private async refresh(flagKeys?: string[]): Promise<void> {
160
- if (flagKeys && flagKeys.length > 0) {
161
- for (const key of flagKeys) {
162
- this.pendingFlagKeys.add(key);
163
- }
164
- } else {
165
- this.pendingFullRefresh = true;
166
- }
167
-
168
- if (this.refreshInFlight) {
169
- return this.refreshInFlight;
170
- }
171
-
172
- this.refreshInFlight = (async () => {
173
- const shouldFull = this.pendingFullRefresh;
174
- const keys = shouldFull ? undefined : Array.from(this.pendingFlagKeys);
175
- this.pendingFlagKeys.clear();
176
- this.pendingFullRefresh = false;
177
-
178
- try {
179
- const response = await this.http.evaluate({
180
- env: this.options.env,
181
- context: this.context,
182
- flagKeys: keys,
183
- });
184
- this.applyEvaluate(response);
185
-
186
- if (!this.status.ready) {
187
- this.status.ready = true;
188
- this.emitter.emit("ready");
189
- }
190
-
191
- this.emitter.emit("update");
192
- } catch (error) {
193
- this.status.error = error as Error;
194
- this.emitter.emit("error", error);
195
- } finally {
196
- this.refreshInFlight = undefined;
197
- }
198
-
199
- if (this.pendingFullRefresh || this.pendingFlagKeys.size > 0) {
200
- return this.refresh();
201
- }
202
- })();
203
-
204
- return this.refreshInFlight;
205
- }
206
-
207
- private applyEvaluate(response: SdkEvaluateResponse) {
208
- for (const [key, evaluation] of Object.entries(response.flags)) {
209
- this.evaluations.set(key, evaluation);
210
- }
211
- this.status.updatedAt = response.updatedAt;
212
- }
213
-
214
- async flushMetrics(): Promise<void> {
215
- const events = this.metricsBuffer.drain();
216
- if (events.length === 0) {
217
- return;
218
- }
219
- try {
220
- await this.http.metrics(buildMetricsRequest(this.options.env, events));
221
- this.emitter.emit("metrics", events);
222
- } catch (error) {
223
- this.status.error = error as Error;
224
- this.emitter.emit("error", error);
225
- }
226
- }
227
-
228
- listFlagEvaluations(): FlagEvaluationResult[] {
229
- const result: FlagEvaluationResult[] = [];
230
- for (const [flagKey, evaluation] of this.evaluations.entries()) {
231
- result.push({ flagKey, ...evaluation });
232
- }
233
- return result;
234
- }
235
- }
package/src/emitter.ts DELETED
@@ -1,29 +0,0 @@
1
- import { FlipFlagEvent, FlipFlagListener } from "./types";
2
-
3
- export class Emitter {
4
- private listeners = new Map<FlipFlagEvent, Set<FlipFlagListener>>();
5
-
6
- on(event: FlipFlagEvent, listener: FlipFlagListener) {
7
- const current = this.listeners.get(event) ?? new Set();
8
- current.add(listener);
9
- this.listeners.set(event, current);
10
- }
11
-
12
- off(event: FlipFlagEvent, listener: FlipFlagListener) {
13
- const current = this.listeners.get(event);
14
- if (!current) {
15
- return;
16
- }
17
- current.delete(listener);
18
- }
19
-
20
- emit(event: FlipFlagEvent, payload?: unknown) {
21
- const current = this.listeners.get(event);
22
- if (!current) {
23
- return;
24
- }
25
- for (const listener of current) {
26
- listener(payload);
27
- }
28
- }
29
- }
package/src/http.ts DELETED
@@ -1,65 +0,0 @@
1
- import { SdkEvaluateRequest, SdkEvaluateResponse, SdkMetricsRequest } from "./types";
2
-
3
- export interface HttpClientOptions {
4
- baseUrl: string;
5
- sdkKey: string;
6
- fetchFn: typeof fetch;
7
- }
8
-
9
- export class HttpClient {
10
- private baseUrl: string;
11
- private sdkKey: string;
12
- private fetchFn: typeof fetch;
13
-
14
- constructor(options: HttpClientOptions) {
15
- this.baseUrl = options.baseUrl.replace(/\/$/, "");
16
- this.sdkKey = options.sdkKey;
17
- this.fetchFn = (options.fetchFn ?? globalThis.fetch).bind(globalThis);
18
- }
19
-
20
- async evaluate(request: SdkEvaluateRequest): Promise<SdkEvaluateResponse> {
21
- return this.post("/v1/sdk/flags/evaluate", request);
22
- }
23
-
24
- async metrics(payload: SdkMetricsRequest): Promise<void> {
25
- await this.post("/v1/sdk/metrics", payload);
26
- }
27
-
28
- private async get<T>(path: string): Promise<T> {
29
- const response = await this.fetchFn(`${this.baseUrl}${path}`, {
30
- method: "GET",
31
- headers: this.headers(),
32
- });
33
- return this.parseJson(response);
34
- }
35
-
36
- private async post<T>(path: string, payload: unknown): Promise<T> {
37
- const response = await this.fetchFn(`${this.baseUrl}${path}`, {
38
- method: "POST",
39
- headers: {
40
- ...this.headers(),
41
- "content-type": "application/json",
42
- },
43
- body: JSON.stringify(payload),
44
- });
45
- if (response.status === 202 && !response.headers.get("content-type")) {
46
- return undefined as T;
47
- }
48
- return this.parseJson(response);
49
- }
50
-
51
-
52
- private headers() {
53
- return {
54
- "x-sdk-key": this.sdkKey,
55
- };
56
- }
57
-
58
- private async parseJson<T>(response: Response): Promise<T> {
59
- if (!response.ok) {
60
- const message = await response.text();
61
- throw new Error(`flipFeatureFlag SDK request failed: ${response.status} ${message}`);
62
- }
63
- return (await response.json()) as T;
64
- }
65
- }
package/src/index.ts DELETED
@@ -1,2 +0,0 @@
1
- export * from "./types";
2
- export { FlipFlagClient } from "./client";
package/src/metrics.ts DELETED
@@ -1,37 +0,0 @@
1
- import { SdkContext, SdkMetricEvent, SdkMetricsRequest } from "./types";
2
-
3
- export class MetricsBuffer {
4
- private events: SdkMetricEvent[] = [];
5
-
6
- add(event: SdkMetricEvent) {
7
- this.events.push(event);
8
- }
9
-
10
- drain(): SdkMetricEvent[] {
11
- const drained = this.events;
12
- this.events = [];
13
- return drained;
14
- }
15
- }
16
-
17
- export function buildMetricEvent(
18
- flagKey: string,
19
- variationKey: string,
20
- reason: string,
21
- context?: SdkContext,
22
- ): SdkMetricEvent {
23
- return {
24
- flagKey,
25
- variationKey,
26
- reason,
27
- ts: new Date().toISOString(),
28
- context,
29
- };
30
- }
31
-
32
- export function buildMetricsRequest(env: string | undefined, events: SdkMetricEvent[]): SdkMetricsRequest {
33
- return {
34
- env,
35
- events,
36
- };
37
- }
package/src/types.ts DELETED
@@ -1,121 +0,0 @@
1
- export type FlagValue = boolean | string | number | Record<string, unknown>;
2
-
3
- export type SdkReason = "rule_match" | "fallthrough" | "default" | "disabled";
4
-
5
- export interface SdkContext {
6
- userId?: string;
7
- attributes?: Record<string, unknown>;
8
- }
9
-
10
- export interface SdkEvaluateRequest {
11
- env?: string;
12
- context: SdkContext;
13
- flagKeys?: string[];
14
- }
15
-
16
- export interface SdkFlagEvaluation {
17
- value: FlagValue;
18
- variationKey: string;
19
- reason: SdkReason;
20
- }
21
-
22
- export interface SdkEvaluateResponse {
23
- flags: Record<string, SdkFlagEvaluation>;
24
- evalId?: string;
25
- updatedAt?: string;
26
- }
27
-
28
- export interface SdkMetadataResponse {
29
- items: Flag[];
30
- updatedAt: string;
31
- }
32
-
33
- export interface SdkMetricEvent {
34
- flagKey: string;
35
- variationKey: string;
36
- reason: SdkReason | string;
37
- ts: string;
38
- context?: SdkContext;
39
- }
40
-
41
- export interface SdkMetricsRequest {
42
- env?: string;
43
- events: SdkMetricEvent[];
44
- }
45
-
46
- export interface Flag {
47
- id: string;
48
- projectId: string;
49
- key: string;
50
- name: string;
51
- description?: string;
52
- type: "boolean" | "string" | "number" | "json";
53
- enabled: boolean;
54
- defaultValue: FlagValue;
55
- variations?: FlagVariation[];
56
- targeting?: TargetingRules;
57
- createdAt: string;
58
- updatedAt: string;
59
- }
60
-
61
- export interface FlagVariation {
62
- key: string;
63
- value: FlagValue;
64
- description?: string;
65
- weight?: number;
66
- }
67
-
68
- export interface TargetingRules {
69
- rules?: TargetingRule[];
70
- fallthrough?: FlagValue;
71
- }
72
-
73
- export interface TargetingRule {
74
- conditions: Condition[];
75
- variationKey: string;
76
- rollout?: number;
77
- }
78
-
79
- export type ConditionOperator =
80
- | "eq"
81
- | "ne"
82
- | "in"
83
- | "not_in"
84
- | "gt"
85
- | "gte"
86
- | "lt"
87
- | "lte"
88
- | "contains";
89
-
90
- export interface Condition {
91
- attribute: string;
92
- operator: ConditionOperator;
93
- value: string | number | boolean | string[];
94
- }
95
-
96
- export interface FlipFlagClientOptions {
97
- url: string;
98
- sdkKey: string;
99
- env?: string;
100
- appName?: string;
101
- refreshIntervalMs?: number;
102
- metricsIntervalMs?: number;
103
- disableRefresh?: boolean;
104
- disableMetrics?: boolean;
105
- context?: SdkContext;
106
- fetch?: typeof fetch;
107
- }
108
-
109
- export interface FlagsStatus {
110
- ready: boolean;
111
- error?: Error;
112
- updatedAt?: string;
113
- }
114
-
115
- export interface FlagEvaluationResult extends SdkFlagEvaluation {
116
- flagKey: string;
117
- }
118
-
119
- export type FlipFlagEvent = "ready" | "update" | "error" | "metrics";
120
-
121
- export type FlipFlagListener = (payload?: unknown) => void;
@@ -1,74 +0,0 @@
1
- import { describe, expect, it, vi } from "vitest";
2
- import { FlipFlagClient } from "../src/client";
3
-
4
- function createResponse(payload: unknown) {
5
- return {
6
- ok: true,
7
- status: 200,
8
- headers: new Headers({ "content-type": "application/json" }),
9
- json: async () => payload,
10
- text: async () => JSON.stringify(payload),
11
- } as Response;
12
- }
13
-
14
- describe("FlipFlagClient (remote)", () => {
15
- it("fetches evaluations on start", async () => {
16
- const fetchMock = vi.fn(async () =>
17
- createResponse({
18
- flags: {
19
- new_checkout: {
20
- value: true,
21
- variationKey: "on",
22
- reason: "rule_match",
23
- },
24
- },
25
- updatedAt: "2026-02-03T12:00:00Z",
26
- }),
27
- );
28
-
29
- const client = new FlipFlagClient({
30
- url: "https://api.example.com",
31
- sdkKey: "test",
32
- disableRefresh: true,
33
- disableMetrics: true,
34
- fetch: fetchMock as unknown as typeof fetch,
35
- });
36
-
37
- await client.start();
38
- expect(client.isEnabled("new_checkout")).toBe(true);
39
- expect(fetchMock).toHaveBeenCalledTimes(1);
40
- });
41
-
42
- it("flushes metrics", async () => {
43
- const fetchMock = vi
44
- .fn()
45
- .mockResolvedValueOnce(
46
- createResponse({
47
- flags: {
48
- flag_a: { value: true, variationKey: "on", reason: "rule_match" },
49
- },
50
- }),
51
- )
52
- .mockResolvedValueOnce({
53
- ok: true,
54
- status: 202,
55
- headers: new Headers(),
56
- json: async () => ({}),
57
- text: async () => "",
58
- } as Response);
59
-
60
- const client = new FlipFlagClient({
61
- url: "https://api.example.com",
62
- sdkKey: "test",
63
- disableRefresh: true,
64
- disableMetrics: true,
65
- fetch: fetchMock as unknown as typeof fetch,
66
- });
67
-
68
- await client.start();
69
- client.isEnabled("flag_a");
70
- await client.flushMetrics();
71
-
72
- expect(fetchMock).toHaveBeenCalledTimes(2);
73
- });
74
- });
package/tsconfig.json DELETED
@@ -1,4 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.base.json",
3
- "include": ["src"]
4
- }