flagmint-js-sdk 1.2.26 → 2.0.0
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/README.md +213 -469
- package/dist/flagmint.cjs.js +40369 -10
- package/dist/flagmint.cjs.js.map +1 -0
- package/dist/flagmint.es.js +35639 -15846
- package/dist/flagmint.es.js.map +1 -0
- package/dist/flagmint.umd.js +40381 -18
- package/dist/flagmint.umd.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/sdk/core/client.d.ts +148 -8
- package/dist/sdk/core/helpers/applicationEvents.d.ts +20 -0
- package/dist/sdk/core/helpers/connectionShare.d.ts +215 -0
- package/dist/sdk/core/transports/LongPollingTransport.d.ts +5 -1
- package/dist/sdk/core/transports/SSETransport.d.ts +90 -0
- package/dist/sdk/core/transports/Transport.d.ts +5 -1
- package/package.json +10 -3
- package/dist/sdk/core/transports/WebsocketTransport.d.ts +0 -33
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
export { FlagClient } from './sdk/core/client';
|
|
2
2
|
export type { FlagClientOptions } from './sdk/core/client';
|
|
3
3
|
export type { CacheAdapter, FeatureFlags } from './sdk/core/helpers/types';
|
|
4
|
+
export type { ApplicationEvent, ApplicationEventKind } from './sdk/core/helpers/applicationEvents';
|
|
4
5
|
export * as syncCache from './sdk/core/helpers/cacheHelper';
|
|
5
6
|
export * as asyncCache from './sdk/core/helpers/cacheHelper.async';
|
|
6
7
|
export type { Transport } from './sdk/core/transports/Transport';
|
|
7
8
|
export { LongPollingTransport } from './sdk/core/transports/LongPollingTransport';
|
|
8
|
-
export {
|
|
9
|
+
export { SseTransport } from './sdk/core/transports/SSETransport';
|
|
9
10
|
export type { FlagValue, EvaluationContext, Segment, Rollout, VariantRollout, VariantOption } from './sdk/core/evaluation/types';
|
|
10
11
|
export { evaluateFlagValue } from './sdk/core/evaluation/evaluateFlagValue';
|
|
11
12
|
export { evaluateRollout } from './sdk/core/evaluation/evaluateRollout';
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { CacheAdapter, FeatureFlags } from '../core/helpers/types';
|
|
2
2
|
import type { Transport } from '../core/transports/Transport';
|
|
3
3
|
import { FlagValue } from '../core/evaluation/types';
|
|
4
|
-
|
|
4
|
+
import { type ConnectionShareOptions } from '../core/helpers/connectionShare';
|
|
5
|
+
type TransportMode = 'auto' | 'long-polling' | 'sse';
|
|
5
6
|
export interface FlagClientOptions<C extends Record<string, any> = Record<string, any>> {
|
|
6
7
|
apiKey: string;
|
|
7
8
|
context?: C;
|
|
@@ -28,10 +29,33 @@ export interface FlagClientOptions<C extends Record<string, any> = Record<string
|
|
|
28
29
|
deferInitialization?: boolean;
|
|
29
30
|
cacheAdapter?: CacheAdapter<C>;
|
|
30
31
|
restEndpoint?: string;
|
|
31
|
-
|
|
32
|
+
sseEndpoint?: string;
|
|
33
|
+
/** Override the ASL handshake URL. Use with `sseEndpoint` for self-hosted gateways. */
|
|
34
|
+
handshakeEndpoint?: string;
|
|
32
35
|
debugLog?: boolean;
|
|
33
36
|
env?: string;
|
|
34
37
|
enableFlagmint: boolean;
|
|
38
|
+
wrapperInfo?: {
|
|
39
|
+
name: string;
|
|
40
|
+
version: string;
|
|
41
|
+
};
|
|
42
|
+
EventSourceImpl?: any;
|
|
43
|
+
/**
|
|
44
|
+
* Share one streaming connection across same-origin documents (tabs / iframes)
|
|
45
|
+
* that use the same API key. Defaults to true in browsers with BroadcastChannel,
|
|
46
|
+
* false in Node (cluster workers each keep their own stream).
|
|
47
|
+
*
|
|
48
|
+
* Followers do not handshake or open EventSource. They receive flags from the
|
|
49
|
+
* leader and forward updateContext() to it. Do not enable this when two clients
|
|
50
|
+
* must evaluate different contexts at the same time on one page, or when the
|
|
51
|
+
* origin hosts untrusted documents that should not steer or read this stream.
|
|
52
|
+
*/
|
|
53
|
+
shareConnection?: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Test/runtime hooks for the share hub (channel, storage, timers).
|
|
56
|
+
* Omit in production.
|
|
57
|
+
*/
|
|
58
|
+
share?: ConnectionShareOptions;
|
|
35
59
|
}
|
|
36
60
|
type FlagUpdateCallback<T> = (flags: FeatureFlags<T>) => void;
|
|
37
61
|
export declare class FlagClient<T = unknown, C extends Record<string, any> = Record<string, any>> {
|
|
@@ -44,7 +68,13 @@ export declare class FlagClient<T = unknown, C extends Record<string, any> = Rec
|
|
|
44
68
|
private cacheTTL;
|
|
45
69
|
private transport;
|
|
46
70
|
private restEndpoint;
|
|
47
|
-
private
|
|
71
|
+
private eventsEndpoint;
|
|
72
|
+
private aslHandshakeUrl;
|
|
73
|
+
private sseEndpoint;
|
|
74
|
+
private eventQueue;
|
|
75
|
+
private eventFlushTimer;
|
|
76
|
+
/** null = server has not sent a map yet; send and let ingest drop. */
|
|
77
|
+
private analyticsByFlag;
|
|
48
78
|
private readyPromise;
|
|
49
79
|
private resolveReady;
|
|
50
80
|
private rejectReady;
|
|
@@ -58,6 +88,8 @@ export declare class FlagClient<T = unknown, C extends Record<string, any> = Rec
|
|
|
58
88
|
private initializationOptions?;
|
|
59
89
|
private isInitialized;
|
|
60
90
|
private subscribers;
|
|
91
|
+
private shareHub;
|
|
92
|
+
private shareConnection;
|
|
61
93
|
/**
|
|
62
94
|
* Creates a new FlagClient instance.
|
|
63
95
|
* @param options - Configuration options for the client.
|
|
@@ -67,9 +99,40 @@ export declare class FlagClient<T = unknown, C extends Record<string, any> = Rec
|
|
|
67
99
|
* Initializes the client by loading cached flags and context, setting up the transport layer.
|
|
68
100
|
*/
|
|
69
101
|
private initialize;
|
|
102
|
+
private bindTransport;
|
|
70
103
|
/**
|
|
71
|
-
*
|
|
104
|
+
* @returns true when this document is a follower and should skip opening a stream.
|
|
72
105
|
*/
|
|
106
|
+
private joinConnectionShare;
|
|
107
|
+
private attachShareLeader;
|
|
108
|
+
/**
|
|
109
|
+
* Creates and initializes the transport layer used to receive feature flags
|
|
110
|
+
* updates from the Flagmint platform.
|
|
111
|
+
*
|
|
112
|
+
* This method performs the authentication handshake to obtain a fresh session
|
|
113
|
+
* identifier, selects the appropriate transport based on the configured
|
|
114
|
+
* transport mode, and initializes it before attaching the internal flag update
|
|
115
|
+
* listener.
|
|
116
|
+
*
|
|
117
|
+
* When operating in `auto` mode, the client attempts to establish a
|
|
118
|
+
* Server-Sent Events (SSE) connection first. If SSE initialization fails,
|
|
119
|
+
* it automatically falls back to the long-polling transport to maintain
|
|
120
|
+
* feature flag delivery.
|
|
121
|
+
*
|
|
122
|
+
* Once the transport is ready, any available initial flags are immediately
|
|
123
|
+
* synchronized into the client state before future updates are received
|
|
124
|
+
* through the transport's notification callbacks.
|
|
125
|
+
*
|
|
126
|
+
* @param {FlagClientOptions<C>} options - The client configuration used to
|
|
127
|
+
* authenticate the handshake request and determine which transport strategy
|
|
128
|
+
* to initialize.
|
|
129
|
+
*
|
|
130
|
+
* @returns {Promise<void>} A promise that resolves once the selected transport
|
|
131
|
+
* has been initialized and wired into the FlagClient.
|
|
132
|
+
*
|
|
133
|
+
* @throws {Error} If the authentication handshake fails, the session identifier
|
|
134
|
+
* cannot be obtained, or the configured transport fails to initialize.
|
|
135
|
+
*/
|
|
73
136
|
private setupTransport;
|
|
74
137
|
/**
|
|
75
138
|
* Updates flags and notifies all subscribers.
|
|
@@ -94,6 +157,23 @@ export declare class FlagClient<T = unknown, C extends Record<string, any> = Rec
|
|
|
94
157
|
* Get a single flag value.
|
|
95
158
|
*/
|
|
96
159
|
getFlag<K extends keyof FeatureFlags<T>>(key: K, fallback?: FeatureFlags<T>[K]): FeatureFlags<T>[K];
|
|
160
|
+
/**
|
|
161
|
+
* Report an application error that happened after this flag was served.
|
|
162
|
+
* Fire-and-forget: events are batched and never throw.
|
|
163
|
+
* No-ops when Analytics Tracking is off for that flag.
|
|
164
|
+
*/
|
|
165
|
+
trackError(flagKey: string, error?: unknown, extra?: Record<string, unknown>): void;
|
|
166
|
+
/**
|
|
167
|
+
* Report a custom metric event attributed to the currently served variation.
|
|
168
|
+
* Fire-and-forget: events are batched and never throw.
|
|
169
|
+
* No-ops when Analytics Tracking is off for that flag.
|
|
170
|
+
*/
|
|
171
|
+
track(flagKey: string, eventName: string, extra?: Record<string, unknown>): void;
|
|
172
|
+
/**
|
|
173
|
+
* Whether Analytics Tracking is on for this flag in the last streamed payload.
|
|
174
|
+
* `undefined` means the server has not sent a map yet (older API / long-polling).
|
|
175
|
+
*/
|
|
176
|
+
isAnalyticsEnabled(flagKey: string): boolean | undefined;
|
|
97
177
|
/**
|
|
98
178
|
* Update the evaluation context.
|
|
99
179
|
*/
|
|
@@ -103,15 +183,75 @@ export declare class FlagClient<T = unknown, C extends Record<string, any> = Rec
|
|
|
103
183
|
*/
|
|
104
184
|
destroy(): void;
|
|
105
185
|
/**
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
186
|
+
* Waits until the FlagClient is fully initialized and ready for feature flag
|
|
187
|
+
* evaluation.
|
|
188
|
+
*
|
|
189
|
+
* If deferred initialization is enabled, this method will first perform the
|
|
190
|
+
* initial client setup. It then waits for the first feature flag payload to
|
|
191
|
+
* become available (or until the specified timeout expires) before awaiting
|
|
192
|
+
* completion of the underlying transport initialization.
|
|
193
|
+
*
|
|
194
|
+
* This method guarantees that transport or connection failures are surfaced,
|
|
195
|
+
* even when feature flags have already been restored from cache.
|
|
196
|
+
*
|
|
197
|
+
* @param {number} [timeoutMs=3000] - The maximum amount of time, in
|
|
198
|
+
* milliseconds, to wait for the initial feature flag payload before
|
|
199
|
+
* continuing initialization.
|
|
200
|
+
*
|
|
201
|
+
* @returns {Promise<void>} A promise that resolves when the client is fully
|
|
202
|
+
* initialized and ready to evaluate feature flags.
|
|
203
|
+
*
|
|
204
|
+
* @throws {Error} If client initialization or the underlying transport fails,
|
|
205
|
+
* such as authentication, rate limiting, or connection errors.
|
|
206
|
+
*/
|
|
110
207
|
ready(timeoutMs?: number): Promise<void>;
|
|
111
208
|
/**
|
|
112
209
|
* Evaluate flags locally (for preview mode).
|
|
113
210
|
*/
|
|
114
211
|
private evaluateLocally;
|
|
212
|
+
/**
|
|
213
|
+
* Waits until the client receives its first non-empty feature flag payload,
|
|
214
|
+
* or until the specified timeout elapses.
|
|
215
|
+
*
|
|
216
|
+
* This method subscribes to flag updates and resolves as soon as at least one
|
|
217
|
+
* feature flag is available. If no flags are received within the allotted
|
|
218
|
+
* timeout, the promise still resolves, allowing the SDK to continue
|
|
219
|
+
* initialization using default or cached values instead of blocking
|
|
220
|
+
* indefinitely.
|
|
221
|
+
*
|
|
222
|
+
* The internal subscription is automatically removed when either condition
|
|
223
|
+
* is met to prevent memory leaks.
|
|
224
|
+
*
|
|
225
|
+
* @param {number} timeoutMs - The maximum amount of time, in milliseconds,
|
|
226
|
+
* to wait for the initial feature flag payload before resolving.
|
|
227
|
+
*
|
|
228
|
+
* @returns {Promise<void>} A promise that resolves when the first non-empty
|
|
229
|
+
* flag payload is received or when the timeout expires, whichever occurs first.
|
|
230
|
+
*/
|
|
115
231
|
private waitForFlags;
|
|
232
|
+
private updateAnalytics;
|
|
233
|
+
private queueApplicationEvent;
|
|
234
|
+
private flushApplicationEvents;
|
|
235
|
+
/**
|
|
236
|
+
* Performs the initial authentication handshake with the Flagmint server
|
|
237
|
+
* to obtain a fresh, single-use session identifier.
|
|
238
|
+
*
|
|
239
|
+
* The returned session ID is used by streaming transports (SSE/WebSocket)
|
|
240
|
+
* to establish an authenticated real-time connection. Session identifiers
|
|
241
|
+
* are intentionally short-lived and should not be reused across new
|
|
242
|
+
* streaming connections.
|
|
243
|
+
*
|
|
244
|
+
* @param {string} apiKey - The Flagmint API key used to authenticate the
|
|
245
|
+
* handshake request.
|
|
246
|
+
*
|
|
247
|
+
* @returns {Promise<string>} A promise that resolves with a newly issued
|
|
248
|
+
* session identifier.
|
|
249
|
+
*
|
|
250
|
+
* @throws {Error} If the API credentials are invalid (`ERR_AUTH`).
|
|
251
|
+
* @throws {Error} If the client has exceeded its rate limit (`ERR_RATE_LIMITED`).
|
|
252
|
+
* @throws {Error} If the handshake endpoint returns an unexpected HTTP error.
|
|
253
|
+
* @throws {Error} If the handshake response does not contain a valid session ID.
|
|
254
|
+
*/
|
|
255
|
+
private fetchFreshSessionId;
|
|
116
256
|
}
|
|
117
257
|
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare const MAX_EVENT_BATCH = 20;
|
|
2
|
+
export declare const EVENT_FLUSH_MS = 2000;
|
|
3
|
+
export type ApplicationEventKind = 'error' | 'custom';
|
|
4
|
+
export interface ApplicationEvent {
|
|
5
|
+
flagKey: string;
|
|
6
|
+
kind: ApplicationEventKind;
|
|
7
|
+
eventName?: string;
|
|
8
|
+
variationValue?: unknown;
|
|
9
|
+
userKey?: string;
|
|
10
|
+
timestamp: string;
|
|
11
|
+
extra?: Record<string, unknown>;
|
|
12
|
+
}
|
|
13
|
+
export declare function eventsUrlFromRestEndpoint(restEndpoint: string): string;
|
|
14
|
+
export declare function userKeyFromContext(context: Record<string, unknown> | undefined): string | undefined;
|
|
15
|
+
export declare function extraFromError(error: unknown, extra?: Record<string, unknown>): Record<string, unknown> | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* When the server has sent an analytics map, only flags with analytics on
|
|
18
|
+
* are reported. A null map means an older server — send and let ingest drop.
|
|
19
|
+
*/
|
|
20
|
+
export declare function shouldReportApplicationEvent(analyticsByFlag: Record<string, boolean> | null, flagKey: string): boolean;
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Same-origin connection sharing for Flagmint streaming transports.
|
|
3
|
+
*
|
|
4
|
+
* When two or more FlagClient instances run in the same origin — typical cases
|
|
5
|
+
* are a host page plus sibling iframes, or two widgets on one Python-embedded
|
|
6
|
+
* page — each would otherwise open its own ASL handshake and SSE/long-poll
|
|
7
|
+
* stream. That doubles evaluation quota and is almost never intended.
|
|
8
|
+
*
|
|
9
|
+
* This hub elects a single **leader** per API key:
|
|
10
|
+
*
|
|
11
|
+
* - **Leader** holds the real transport (handshake + EventSource). It broadcasts
|
|
12
|
+
* flag snapshots and errors over {@link BroadcastChannel}.
|
|
13
|
+
* - **Followers** skip handshake and EventSource. They apply the leader's flags
|
|
14
|
+
* and forward `updateContext()` to the leader, which runs it on the shared
|
|
15
|
+
* stream.
|
|
16
|
+
*
|
|
17
|
+
* Leadership is stored in `localStorage` (`flagmint_share_lock:<apiKey>`) with a
|
|
18
|
+
* short TTL and heartbeat. `destroy()` and a `pagehide` / `beforeunload` listener
|
|
19
|
+
* post `bye` and release the lock so another member can take over immediately.
|
|
20
|
+
* If the document is killed without those events, followers wait until the lock
|
|
21
|
+
* TTL expires.
|
|
22
|
+
*
|
|
23
|
+
* Sharing is a **browser** feature. Node cluster workers each need their own
|
|
24
|
+
* stream — {@link isConnectionSharingAvailable} is false there. Do not share
|
|
25
|
+
* when two clients on one page must keep different evaluation contexts at the
|
|
26
|
+
* same time; `/context` is per connection, so the last write wins.
|
|
27
|
+
*
|
|
28
|
+
* Trust: `BroadcastChannel` and `localStorage` are same-origin. Any document
|
|
29
|
+
* on this origin (including a third-party iframe you host there) can join the
|
|
30
|
+
* group, POST a context through the leader's authenticated stream, and read
|
|
31
|
+
* the flags broadcast back. That is expected among trusted siblings. If the
|
|
32
|
+
* origin serves untrusted content, set `shareConnection: false`.
|
|
33
|
+
*
|
|
34
|
+
* @module connectionShare
|
|
35
|
+
*/
|
|
36
|
+
import type { Transport } from '../../core/transports/Transport';
|
|
37
|
+
/** Role this document plays in the share group for a given API key. */
|
|
38
|
+
export type ShareRole = 'leader' | 'follower';
|
|
39
|
+
/**
|
|
40
|
+
* Persistence used for the leadership lock.
|
|
41
|
+
* Defaults to `localStorage` so sibling iframes on the same origin can compete.
|
|
42
|
+
*/
|
|
43
|
+
export interface ShareStorage {
|
|
44
|
+
getItem(key: string): string | null;
|
|
45
|
+
setItem(key: string, value: string): void;
|
|
46
|
+
removeItem(key: string): void;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Cross-document bus. Production uses {@link BroadcastChannel};
|
|
50
|
+
* tests inject an in-memory implementation.
|
|
51
|
+
*/
|
|
52
|
+
export interface ShareChannel {
|
|
53
|
+
postMessage(data: unknown): void;
|
|
54
|
+
close(): void;
|
|
55
|
+
onmessage: ((event: {
|
|
56
|
+
data: unknown;
|
|
57
|
+
}) => void) | null;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Test/runtime hooks. Omit in production — the hub uses `BroadcastChannel`,
|
|
61
|
+
* `localStorage`, and `setInterval`.
|
|
62
|
+
*/
|
|
63
|
+
export interface ConnectionShareOptions {
|
|
64
|
+
/** Override `new BroadcastChannel(name)` (tests). */
|
|
65
|
+
channelFactory?: (name: string) => ShareChannel;
|
|
66
|
+
/** Override `localStorage` for the leadership lock (tests). */
|
|
67
|
+
storage?: ShareStorage;
|
|
68
|
+
/** Clock used for lock expiry. */
|
|
69
|
+
now?: () => number;
|
|
70
|
+
setTimer?: (handler: () => void, ms: number) => ReturnType<typeof setInterval>;
|
|
71
|
+
clearTimer?: (id: ReturnType<typeof setInterval>) => void;
|
|
72
|
+
/** Delay used to stagger lock confirmation (tests inject a no-op). */
|
|
73
|
+
delay?: (ms: number) => Promise<void>;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Whether this runtime can share a stream across documents.
|
|
77
|
+
*
|
|
78
|
+
* @returns `true` in a browser with {@link BroadcastChannel} (includes same-origin
|
|
79
|
+
* iframes). `false` in Node, so cluster workers each keep their own connection.
|
|
80
|
+
*/
|
|
81
|
+
export declare function isConnectionSharingAvailable(): boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Elects a single streaming leader per API key across same-origin documents.
|
|
84
|
+
*
|
|
85
|
+
* {@link FlagClient} constructs one hub when `shareConnection` is enabled (the
|
|
86
|
+
* browser default). The leader opens SSE; followers use
|
|
87
|
+
* {@link ConnectionShareHub.createFollowerTransport} instead.
|
|
88
|
+
*
|
|
89
|
+
* @template C Evaluation context forwarded from follower `updateContext` calls.
|
|
90
|
+
* @template T Flag value type in snapshots broadcast by the leader.
|
|
91
|
+
*/
|
|
92
|
+
export declare class ConnectionShareHub<C = Record<string, unknown>, T = unknown> {
|
|
93
|
+
private readonly apiKey;
|
|
94
|
+
/** Stable id for this document in channel messages and the lock record. */
|
|
95
|
+
readonly memberId: string;
|
|
96
|
+
private role;
|
|
97
|
+
private channel;
|
|
98
|
+
private heartbeatId;
|
|
99
|
+
private watchId;
|
|
100
|
+
private closed;
|
|
101
|
+
private latestFlags;
|
|
102
|
+
private flagsWaiters;
|
|
103
|
+
private contextWaiters;
|
|
104
|
+
private onPromoteCallback?;
|
|
105
|
+
private leaderContextHandler?;
|
|
106
|
+
private followerFlagsCallback?;
|
|
107
|
+
private followerErrorCallback?;
|
|
108
|
+
private readonly now;
|
|
109
|
+
private readonly storage;
|
|
110
|
+
private readonly channelFactory?;
|
|
111
|
+
private readonly setTimer;
|
|
112
|
+
private readonly clearTimer;
|
|
113
|
+
private readonly delay;
|
|
114
|
+
private unloadHandler?;
|
|
115
|
+
private readonly lockKey;
|
|
116
|
+
private readonly channelName;
|
|
117
|
+
/**
|
|
118
|
+
* @param apiKey Share groups are keyed by API key. Distinct keys never share.
|
|
119
|
+
* @param options Optional test doubles for channel, storage, and timers.
|
|
120
|
+
*/
|
|
121
|
+
constructor(apiKey: string, options?: ConnectionShareOptions);
|
|
122
|
+
/** `leader` if this document holds the lock; otherwise `follower`. */
|
|
123
|
+
get currentRole(): ShareRole;
|
|
124
|
+
/**
|
|
125
|
+
* Join the share group: compete for the lock, subscribe to the channel.
|
|
126
|
+
*
|
|
127
|
+
* If {@link BroadcastChannel} is missing, this member becomes leader locally
|
|
128
|
+
* and never shares (same as a Node process).
|
|
129
|
+
*
|
|
130
|
+
* @returns The role this member should take. Only the leader should call
|
|
131
|
+
* ASL handshake / `SseTransport.init()`.
|
|
132
|
+
*/
|
|
133
|
+
join(): Promise<ShareRole>;
|
|
134
|
+
/**
|
|
135
|
+
* Invoked after a follower wins the lock (leader `bye` or expired heartbeat).
|
|
136
|
+
* The client should open a real transport here, then
|
|
137
|
+
* {@link ConnectionShareHub.broadcastFlags}.
|
|
138
|
+
*/
|
|
139
|
+
onPromote(callback: () => void | Promise<void>): void;
|
|
140
|
+
/**
|
|
141
|
+
* Leader-only: run `updateContext` on the real transport when a follower
|
|
142
|
+
* sends `context-request`. The context is whatever that same-origin
|
|
143
|
+
* document posted; the leader does not authenticate the sender beyond origin.
|
|
144
|
+
*/
|
|
145
|
+
setLeaderContextHandler(handler: (context: C, options?: {
|
|
146
|
+
persist?: boolean;
|
|
147
|
+
}) => Promise<Record<string, T>>): void;
|
|
148
|
+
/**
|
|
149
|
+
* Leader-only: publish the current evaluated flags to all followers.
|
|
150
|
+
* No-op on followers (avoids echo loops).
|
|
151
|
+
*/
|
|
152
|
+
broadcastFlags(flags: Record<string, T>): void;
|
|
153
|
+
/** Leader-only: mirror a transport `onError` (auth, quota) to followers. */
|
|
154
|
+
broadcastError(error: Error): void;
|
|
155
|
+
/**
|
|
156
|
+
* Transport used by follower FlagClients. `init` waits for the leader
|
|
157
|
+
* snapshot; `fetchFlags` RPCs `updateContext` through the leader.
|
|
158
|
+
* `destroy` is a no-op — {@link ConnectionShareHub.destroy} owns teardown.
|
|
159
|
+
*/
|
|
160
|
+
createFollowerTransport(): Transport<C, T>;
|
|
161
|
+
/**
|
|
162
|
+
* Leave the share group. Leaders release the lock and post `bye` so another
|
|
163
|
+
* iframe can open the stream. Also invoked from `pagehide` / `beforeunload`.
|
|
164
|
+
* Channel close is deferred one tick so `bye` can flush on native
|
|
165
|
+
* {@link BroadcastChannel}.
|
|
166
|
+
*/
|
|
167
|
+
destroy(): void;
|
|
168
|
+
private bindUnloadListener;
|
|
169
|
+
private unbindUnloadListener;
|
|
170
|
+
/** Open the BroadcastChannel for this API key, or a test factory channel. */
|
|
171
|
+
private openChannel;
|
|
172
|
+
/** Mark this member leader and start lock heartbeat + `alive` pings. */
|
|
173
|
+
private becomeLeader;
|
|
174
|
+
/**
|
|
175
|
+
* Follower poll: if the lock is free or expired, take it and run
|
|
176
|
+
* {@link ConnectionShareHub.onPromote}.
|
|
177
|
+
*/
|
|
178
|
+
private maybeTakeover;
|
|
179
|
+
/** Dispatch a {@link ShareMessage} from another document in the group. */
|
|
180
|
+
private handleMessage;
|
|
181
|
+
/**
|
|
182
|
+
* Leader: evaluate a follower's context on the real transport and reply.
|
|
183
|
+
* Any same-origin document can trigger this; the resulting flags are
|
|
184
|
+
* broadcast to the whole group.
|
|
185
|
+
*/
|
|
186
|
+
private handleContextRequest;
|
|
187
|
+
/**
|
|
188
|
+
* Block until the leader has sent a `flags` snapshot, or {@link FOLLOWER_WAIT_MS}.
|
|
189
|
+
* Used by follower `init()`.
|
|
190
|
+
*/
|
|
191
|
+
private waitForFlags;
|
|
192
|
+
/**
|
|
193
|
+
* Follower `fetchFlags`: ask the leader to POST `/context` and wait for
|
|
194
|
+
* `context-result`. Leaders run the handler locally.
|
|
195
|
+
*
|
|
196
|
+
* The posted context is not origin-isolated beyond BroadcastChannel itself.
|
|
197
|
+
* Untrusted same-origin frames must not share a connection with a trusted
|
|
198
|
+
* leader — set `shareConnection: false` on those clients.
|
|
199
|
+
*/
|
|
200
|
+
private requestContextUpdate;
|
|
201
|
+
/** Read the leadership lock, or `null` if missing/corrupt. */
|
|
202
|
+
private readLock;
|
|
203
|
+
/**
|
|
204
|
+
* Write the lock, wait so a competing write can land, then re-read.
|
|
205
|
+
* localStorage has no compare-and-set; the delay plus confirm is the
|
|
206
|
+
* best we can do so two iframes do not both become leader.
|
|
207
|
+
*
|
|
208
|
+
* @returns `true` if this member still owns the lock after the confirm.
|
|
209
|
+
*/
|
|
210
|
+
private tryAcquireLock;
|
|
211
|
+
/** Extend lock expiry; no-op if another member stole the lock. */
|
|
212
|
+
private refreshLock;
|
|
213
|
+
/** Drop the lock only if this member still owns it. */
|
|
214
|
+
private releaseLock;
|
|
215
|
+
}
|
|
@@ -13,6 +13,7 @@ export declare class LongPollingTransport<C, T> implements Transport<C, T> {
|
|
|
13
13
|
private backoffMultiplier;
|
|
14
14
|
private pollTimeoutId;
|
|
15
15
|
private onUpdateCallback?;
|
|
16
|
+
private onAnalyticsUpdatedCallback?;
|
|
16
17
|
private currentContext;
|
|
17
18
|
private currentFlags;
|
|
18
19
|
private consecutiveErrors;
|
|
@@ -23,7 +24,10 @@ export declare class LongPollingTransport<C, T> implements Transport<C, T> {
|
|
|
23
24
|
private poll;
|
|
24
25
|
private applyBackoff;
|
|
25
26
|
private flagsChanged;
|
|
26
|
-
fetchFlags(context: C
|
|
27
|
+
fetchFlags(context: C, options?: {
|
|
28
|
+
persist?: boolean;
|
|
29
|
+
}): Promise<Record<string, T>>;
|
|
27
30
|
onFlagsUpdated(callback: (flags: Record<string, T>) => void): void;
|
|
31
|
+
onAnalyticsUpdated(callback: (analytics: Record<string, boolean>) => void): void;
|
|
28
32
|
destroy(): void;
|
|
29
33
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { Transport } from './Transport';
|
|
2
|
+
export interface SseTransportConfig {
|
|
3
|
+
endpoint: string;
|
|
4
|
+
apiKey: string;
|
|
5
|
+
context: any;
|
|
6
|
+
wrapper?: {
|
|
7
|
+
name: string;
|
|
8
|
+
version: string;
|
|
9
|
+
};
|
|
10
|
+
EventSourceImpl?: any;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* A Server-Sent Events (SSE) transport implementation responsible for
|
|
14
|
+
* maintaining a persistent streaming connection to the Flagmint platform.
|
|
15
|
+
*
|
|
16
|
+
* Handshake: `POST /auth/asl-handshake` issues a single-use sessionId.
|
|
17
|
+
* Stream: `GET {endpoint}/stream?sessionId&context&sdkVersion&platform&wrapper*`
|
|
18
|
+
* emits `connected` then `flags`. Heartbeats are SSE comments (`: heartbeat`)
|
|
19
|
+
* and are not visible to EventSource — they only keep the TCP connection alive.
|
|
20
|
+
* Context: `POST {endpoint}/context` with `x-api-key` returns 202 and the
|
|
21
|
+
* re-evaluated flags arrive on the existing stream after a 400ms debounce.
|
|
22
|
+
*
|
|
23
|
+
* @template C The shape of the evaluation context sent to the Flagmint server.
|
|
24
|
+
* @template T The feature flag value type returned by the platform.
|
|
25
|
+
*
|
|
26
|
+
* @implements {Transport<C, T>}
|
|
27
|
+
*/
|
|
28
|
+
export declare class SseTransport<C, T> implements Transport<C, T> {
|
|
29
|
+
private endpoint;
|
|
30
|
+
private sessionId;
|
|
31
|
+
private refreshTokenCallback?;
|
|
32
|
+
private configOptions?;
|
|
33
|
+
private eventSource;
|
|
34
|
+
private flags;
|
|
35
|
+
private context;
|
|
36
|
+
private connectionId;
|
|
37
|
+
private onFlagsUpdatedCallback?;
|
|
38
|
+
private onAnalyticsUpdatedCallback?;
|
|
39
|
+
private onErrorCallback?;
|
|
40
|
+
private initialFlagsReceived;
|
|
41
|
+
private initialFlagsResolve;
|
|
42
|
+
private initialFlagsReject;
|
|
43
|
+
private nextFlagsResolve;
|
|
44
|
+
private connectPromise;
|
|
45
|
+
private connectResolve;
|
|
46
|
+
private connectReject;
|
|
47
|
+
private connectionListener?;
|
|
48
|
+
private flagsListener?;
|
|
49
|
+
private quotaListener?;
|
|
50
|
+
private errorListener?;
|
|
51
|
+
private reconnectTimeoutId;
|
|
52
|
+
private reconnectAttempts;
|
|
53
|
+
private terminalClose;
|
|
54
|
+
private contextUpdateQueue;
|
|
55
|
+
constructor(endpoint: string, sessionId: string, initialContext: C, refreshTokenCallback?: (() => Promise<string>) | undefined, configOptions?: Partial<SseTransportConfig> | undefined);
|
|
56
|
+
init(): Promise<void>;
|
|
57
|
+
/**
|
|
58
|
+
* POST /v2/flags/context. FF-EU always returns 202 and pushes the next
|
|
59
|
+
* `flags` event on the open SSE stream (after a 400ms debounce). The HTTP
|
|
60
|
+
* body does not contain evaluated flags — browser and Node share this path.
|
|
61
|
+
*/
|
|
62
|
+
fetchFlags(context: C, options?: {
|
|
63
|
+
persist?: boolean;
|
|
64
|
+
}): Promise<Record<string, T>>;
|
|
65
|
+
onFlagsUpdated(callback: (flags: Record<string, T>) => void): void;
|
|
66
|
+
onAnalyticsUpdated(callback: (analytics: Record<string, boolean>) => void): void;
|
|
67
|
+
onError(callback: (error: Error) => void): void;
|
|
68
|
+
destroy(): void;
|
|
69
|
+
private get apiKey();
|
|
70
|
+
private applyFlags;
|
|
71
|
+
private emitError;
|
|
72
|
+
private performContextUpdate;
|
|
73
|
+
private safeReadJson;
|
|
74
|
+
private quotaExtra;
|
|
75
|
+
private handleQuotaPayload;
|
|
76
|
+
private cleanupEventSource;
|
|
77
|
+
private resolveEventSourceImpl;
|
|
78
|
+
private connect;
|
|
79
|
+
private scheduleActiveReconnection;
|
|
80
|
+
private createConnectionListener;
|
|
81
|
+
private createFlagsListener;
|
|
82
|
+
private createQuotaListener;
|
|
83
|
+
/**
|
|
84
|
+
* Named `error` events from FF-EU (`event: error`) after the stream is hijacked.
|
|
85
|
+
* Distinct from EventSource.onerror, which fires on network/connection failure.
|
|
86
|
+
*/
|
|
87
|
+
private createStreamErrorListener;
|
|
88
|
+
private mapStreamErrorCode;
|
|
89
|
+
private resetInitialResolvers;
|
|
90
|
+
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
export interface Transport<C, T> {
|
|
2
2
|
init(onUpdate?: (flags: Record<string, T>) => void): Promise<void>;
|
|
3
|
-
fetchFlags(context: C
|
|
3
|
+
fetchFlags(context: C, options?: {
|
|
4
|
+
persist?: boolean;
|
|
5
|
+
}): Promise<Record<string, T>>;
|
|
4
6
|
destroy(): void;
|
|
5
7
|
onFlagsUpdated?: (callback: (flags: Record<string, T>) => void) => void;
|
|
8
|
+
onAnalyticsUpdated?: (callback: (analytics: Record<string, boolean>) => void) => void;
|
|
6
9
|
onContextUpdated?: (callback: (context: C) => void) => void;
|
|
10
|
+
onError?: (callback: (error: Error) => void) => void;
|
|
7
11
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flagmint-js-sdk",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Framework-agnostic JavaScript SDK for managing feature flags in Flagmint applications.",
|
|
5
5
|
"author": "Flagmint Team",
|
|
6
6
|
"license": "BSD 3-Clause",
|
|
@@ -22,7 +22,10 @@
|
|
|
22
22
|
"scripts": {
|
|
23
23
|
"clean": "rimraf dist",
|
|
24
24
|
"build": "npm run clean && vite build",
|
|
25
|
-
"test": "jest --runInBand --detectOpenHandles"
|
|
25
|
+
"test": "jest --runInBand --detectOpenHandles",
|
|
26
|
+
"simulate:unreachable": "npm run build && node ./scripts/simulate-unreachable-server.js",
|
|
27
|
+
"simulate:cache-fallback": "npm run build && node ./scripts/simulate-cache-adapter-fallback.js",
|
|
28
|
+
"simulate:asl-gen": "npm run build && node ./scripts/test-asl-handshake.mjs"
|
|
26
29
|
},
|
|
27
30
|
"peerDependencies": {
|
|
28
31
|
"crypto": "*"
|
|
@@ -42,7 +45,11 @@
|
|
|
42
45
|
"vite-plugin-dts": "^2.0.0"
|
|
43
46
|
},
|
|
44
47
|
"dependencies": {
|
|
45
|
-
"
|
|
48
|
+
"@noble/ciphers": "^2.2.0",
|
|
49
|
+
"@noble/curves": "^2.2.0",
|
|
50
|
+
"@noble/hashes": "^2.2.0",
|
|
51
|
+
"buffer": "^6.0.3",
|
|
52
|
+
"node-fetch": "^3.3.2"
|
|
46
53
|
},
|
|
47
54
|
"optionalDependencies": {
|
|
48
55
|
"ws": "^8.0.0"
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import type { Transport } from './Transport';
|
|
2
|
-
type ConnectionState = 'connecting' | 'connected' | 'disconnected' | 'reconnecting' | 'failed';
|
|
3
|
-
export declare class WebSocketTransport<C, T> implements Transport<C, T> {
|
|
4
|
-
private wsUrl;
|
|
5
|
-
private apiKey;
|
|
6
|
-
private maxRetries;
|
|
7
|
-
private initialBackoffMs;
|
|
8
|
-
private socket;
|
|
9
|
-
private flags;
|
|
10
|
-
private context;
|
|
11
|
-
private isReady;
|
|
12
|
-
private initialFlagsReceived;
|
|
13
|
-
private initialFlagsPromise;
|
|
14
|
-
private initialFlagsResolve;
|
|
15
|
-
private initialFlagsReject;
|
|
16
|
-
private onFlagsUpdatedCallback?;
|
|
17
|
-
private onConnectionStateCallback?;
|
|
18
|
-
private retries;
|
|
19
|
-
private reconnectTimeoutId;
|
|
20
|
-
constructor(wsUrl: string, apiKey: string, maxRetries?: number, initialBackoffMs?: number);
|
|
21
|
-
init(): Promise<void>;
|
|
22
|
-
private waitForInitialFlags;
|
|
23
|
-
private connectWithRetry;
|
|
24
|
-
private cleanupSocket;
|
|
25
|
-
private setConnectionState;
|
|
26
|
-
private getWebSocketImplementation;
|
|
27
|
-
fetchFlags(context: C): Promise<Record<string, T>>;
|
|
28
|
-
onFlagsUpdated(callback: (flags: Record<string, T>) => void): void;
|
|
29
|
-
onConnectionStateChanged(callback: (state: ConnectionState) => void): void;
|
|
30
|
-
destroy(): void;
|
|
31
|
-
private sendContext;
|
|
32
|
-
}
|
|
33
|
-
export {};
|