@flagtide/core 0.0.0-stage → 1.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/LICENSE +21 -0
- package/README.md +46 -2
- package/index.d.ts +22 -0
- package/index.js +13 -0
- package/lib/bucket.d.ts +4 -0
- package/lib/bucket.js +7 -0
- package/lib/client.d.ts +66 -0
- package/lib/client.js +121 -0
- package/lib/connection-manager.d.ts +137 -0
- package/lib/connection-manager.js +336 -0
- package/lib/evaluate.d.ts +11 -0
- package/lib/evaluate.js +214 -0
- package/lib/fetch-snapshot.d.ts +17 -0
- package/lib/fetch-snapshot.js +21 -0
- package/lib/flag-store.d.ts +44 -0
- package/lib/flag-store.js +107 -0
- package/lib/murmur3.d.ts +4 -0
- package/lib/murmur3.js +48 -0
- package/lib/overrides.d.ts +19 -0
- package/lib/overrides.js +56 -0
- package/lib/protocol.d.ts +69 -0
- package/lib/protocol.js +90 -0
- package/lib/resolve.d.ts +22 -0
- package/lib/resolve.js +63 -0
- package/lib/semver.d.ts +11 -0
- package/lib/semver.js +73 -0
- package/lib/storage.d.ts +33 -0
- package/lib/storage.js +77 -0
- package/lib/types.d.ts +83 -0
- package/lib/types.js +1 -0
- package/lib/utf8.d.ts +4 -0
- package/lib/utf8.js +77 -0
- package/package.json +35 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Adrian Turbiński
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,47 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @flagtide/core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The framework-agnostic part of the flagtide SDK. It evaluates feature flags locally, with the same result as the Java server, and keeps them current over a WebSocket.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm install @flagtide/core rxjs
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { createFlagtideClient } from '@flagtide/core';
|
|
11
|
+
|
|
12
|
+
const client = createFlagtideClient({
|
|
13
|
+
streamUrl: 'wss://flags.example.com/sdk/v1/stream',
|
|
14
|
+
sdkKey: 'fws_...',
|
|
15
|
+
context: { key: 'user-42', attributes: { plan: 'pro', appVersion: '2.4.0' } },
|
|
16
|
+
});
|
|
17
|
+
client.start();
|
|
18
|
+
|
|
19
|
+
client.value('new-checkout', false);
|
|
20
|
+
client.resolve('banner-text', 'Welcome').reason;
|
|
21
|
+
client.status$.subscribe(console.log);
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Evaluation reads the snapshot the client already holds, so it never waits for the network. A flag the client does not know, or one whose type differs from the fallback, returns the fallback with the reason `FLAG_NOT_FOUND` or `TYPE_MISMATCH`.
|
|
25
|
+
|
|
26
|
+
## Connection behavior
|
|
27
|
+
|
|
28
|
+
- The SDK key travels in the first frame, never in the URL.
|
|
29
|
+
- A reconnect sends the last applied version and receives exactly the changes it missed, or a full snapshot when it is too far behind.
|
|
30
|
+
- Reconnects use exponential backoff with full jitter: `random(0, min(30 s, 500 ms * 2^attempt))`.
|
|
31
|
+
- A connection that stays silent for 40 seconds is treated as dead. The check compares timestamps, so a throttled background tab does not cause false alarms, and it runs again when the tab becomes visible.
|
|
32
|
+
- The status is one of `connecting`, `live`, `stale` and `offline`.
|
|
33
|
+
- The last snapshot is kept in `localStorage` (when it is usable) so a page can start without the server.
|
|
34
|
+
|
|
35
|
+
## Server side rendering
|
|
36
|
+
|
|
37
|
+
Do not open a socket on a server. Fetch the snapshot with `fetchSnapshot(baseUrl, sdkKey)`, pass it as `initialSnapshot`, and call `start()` only in the browser.
|
|
38
|
+
|
|
39
|
+
## Evaluation without a connection
|
|
40
|
+
|
|
41
|
+
`evaluate`, `indexSegments`, `bucketOf` and `murmur3x86_32` are exported for code that wants the algorithm alone, for example a Node service or a test. The algorithm is specified in `docs/evaluation-spec.md` of the repository and checked against shared test vectors in both Java and TypeScript.
|
|
42
|
+
|
|
43
|
+
## Requirements
|
|
44
|
+
|
|
45
|
+
RxJS 7.8 or later as a peer dependency, and a runtime with `WebSocket` (browsers, Node 22 and later).
|
|
46
|
+
|
|
47
|
+
MIT licensed.
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export { BUCKET_SPACE, bucketOf } from './lib/bucket.js';
|
|
2
|
+
export { createFlagtideClient, FlagtideClient } from './lib/client.js';
|
|
3
|
+
export type { FlagtideClientOptions } from './lib/client.js';
|
|
4
|
+
export { backoffDelay, ConnectionManager, createBrowserEnvironment, createBrowserSocketFactory, DEFAULT_TIMING, } from './lib/connection-manager.js';
|
|
5
|
+
export type { ConnectionManagerOptions, ConnectionStatus, ConnectionTiming, NetworkEnvironment, SocketControl, SocketFactory, SocketHandlers, StreamError, } from './lib/connection-manager.js';
|
|
6
|
+
export { InvalidFlagConfigError, evaluate, indexSegments } from './lib/evaluate.js';
|
|
7
|
+
export { fetchSnapshot } from './lib/fetch-snapshot.js';
|
|
8
|
+
export type { FetchLike } from './lib/fetch-snapshot.js';
|
|
9
|
+
export { FlagStore } from './lib/flag-store.js';
|
|
10
|
+
export type { ApplyOutcome, FlagSnapshot } from './lib/flag-store.js';
|
|
11
|
+
export { murmur3x86_32, murmur3x86_32OfText } from './lib/murmur3.js';
|
|
12
|
+
export { FlagOverrides } from './lib/overrides.js';
|
|
13
|
+
export { parseServerFrame, parseSnapshotBody } from './lib/protocol.js';
|
|
14
|
+
export type * from './lib/protocol.js';
|
|
15
|
+
export { flagTypeOf, jsonEquals, resolveFlag } from './lib/resolve.js';
|
|
16
|
+
export type { Resolution, ResolutionReason } from './lib/resolve.js';
|
|
17
|
+
export { compareSemanticVersions, parseSemanticVersion } from './lib/semver.js';
|
|
18
|
+
export type { SemanticVersion } from './lib/semver.js';
|
|
19
|
+
export { createLocalStore, createMemoryStore, createSnapshotStorage, createWebStore, snapshotStorageKey, } from './lib/storage.js';
|
|
20
|
+
export type { KeyValueStore, SnapshotStorage } from './lib/storage.js';
|
|
21
|
+
export { encodeUtf8, utf8Length } from './lib/utf8.js';
|
|
22
|
+
export type * from './lib/types.js';
|
package/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { BUCKET_SPACE, bucketOf } from './lib/bucket.js';
|
|
2
|
+
export { createFlagtideClient, FlagtideClient } from './lib/client.js';
|
|
3
|
+
export { backoffDelay, ConnectionManager, createBrowserEnvironment, createBrowserSocketFactory, DEFAULT_TIMING, } from './lib/connection-manager.js';
|
|
4
|
+
export { InvalidFlagConfigError, evaluate, indexSegments } from './lib/evaluate.js';
|
|
5
|
+
export { fetchSnapshot } from './lib/fetch-snapshot.js';
|
|
6
|
+
export { FlagStore } from './lib/flag-store.js';
|
|
7
|
+
export { murmur3x86_32, murmur3x86_32OfText } from './lib/murmur3.js';
|
|
8
|
+
export { FlagOverrides } from './lib/overrides.js';
|
|
9
|
+
export { parseServerFrame, parseSnapshotBody } from './lib/protocol.js';
|
|
10
|
+
export { flagTypeOf, jsonEquals, resolveFlag } from './lib/resolve.js';
|
|
11
|
+
export { compareSemanticVersions, parseSemanticVersion } from './lib/semver.js';
|
|
12
|
+
export { createLocalStore, createMemoryStore, createSnapshotStorage, createWebStore, snapshotStorageKey, } from './lib/storage.js';
|
|
13
|
+
export { encodeUtf8, utf8Length } from './lib/utf8.js';
|
package/lib/bucket.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Number of buckets a context key is hashed into. One unit is 0.001 percent. */
|
|
2
|
+
export declare const BUCKET_SPACE = 100000;
|
|
3
|
+
/** The bucket, from 0 to 99999, of a context key for a flag: MurmurHash3 over `flagKey.salt.contextKey` modulo 100000. */
|
|
4
|
+
export declare function bucketOf(flagKey: string, salt: string, contextKey: string): number;
|
package/lib/bucket.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { murmur3x86_32OfText } from './murmur3.js';
|
|
2
|
+
/** Number of buckets a context key is hashed into. One unit is 0.001 percent. */
|
|
3
|
+
export const BUCKET_SPACE = 100000;
|
|
4
|
+
/** The bucket, from 0 to 99999, of a context key for a flag: MurmurHash3 over `flagKey.salt.contextKey` modulo 100000. */
|
|
5
|
+
export function bucketOf(flagKey, salt, contextKey) {
|
|
6
|
+
return murmur3x86_32OfText(`${flagKey}.${salt}.${contextKey}`) % BUCKET_SPACE;
|
|
7
|
+
}
|
package/lib/client.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { SchedulerLike, Observable } from 'rxjs';
|
|
2
|
+
import type { ConnectionStatus, ConnectionTiming, NetworkEnvironment, SocketFactory, StreamError } from './connection-manager.js';
|
|
3
|
+
import type { FlagSnapshot } from './flag-store.js';
|
|
4
|
+
import { FlagOverrides } from './overrides.js';
|
|
5
|
+
import type { Resolution } from './resolve.js';
|
|
6
|
+
import type { KeyValueStore } from './storage.js';
|
|
7
|
+
import type { EvaluationContext, FlagType, JsonValue } from './types.js';
|
|
8
|
+
/** Everything {@link createFlagtideClient} accepts. Only `streamUrl` and `sdkKey` are required. */
|
|
9
|
+
export interface FlagtideClientOptions {
|
|
10
|
+
/** The stream endpoint, for example `ws://localhost:18081/sdk/v1/stream`. */
|
|
11
|
+
readonly streamUrl: string;
|
|
12
|
+
/** The read-only SDK key of one environment. */
|
|
13
|
+
readonly sdkKey: string;
|
|
14
|
+
/** Who flags are evaluated for. Without it the client uses an anonymous id that is kept in storage. */
|
|
15
|
+
readonly context?: EvaluationContext;
|
|
16
|
+
/** A snapshot to start from, for example one rendered on the server. It wins over the stored snapshot. */
|
|
17
|
+
readonly initialSnapshot?: FlagSnapshot;
|
|
18
|
+
/** Where the last snapshot, the overrides and the anonymous id live. Defaults to `localStorage` when there is one. */
|
|
19
|
+
readonly store?: KeyValueStore;
|
|
20
|
+
readonly socketFactory?: SocketFactory;
|
|
21
|
+
readonly environment?: NetworkEnvironment;
|
|
22
|
+
readonly scheduler?: SchedulerLike;
|
|
23
|
+
readonly random?: () => number;
|
|
24
|
+
readonly timing?: Partial<ConnectionTiming>;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Evaluates flags locally and keeps them current over a stream connection.
|
|
28
|
+
*
|
|
29
|
+
* Evaluation never waits for the network: it reads the snapshot the client holds. Create one client per
|
|
30
|
+
* environment with {@link createFlagtideClient}.
|
|
31
|
+
*/
|
|
32
|
+
export declare class FlagtideClient {
|
|
33
|
+
/** Local overrides. A resolution reports `OVERRIDE` when one applies. */
|
|
34
|
+
readonly overrides: FlagOverrides;
|
|
35
|
+
private readonly flagStore;
|
|
36
|
+
private readonly manager;
|
|
37
|
+
private readonly storage;
|
|
38
|
+
private readonly contextChanged;
|
|
39
|
+
private currentContext;
|
|
40
|
+
constructor(options: FlagtideClientOptions);
|
|
41
|
+
get status$(): Observable<ConnectionStatus>;
|
|
42
|
+
get status(): ConnectionStatus;
|
|
43
|
+
get errors$(): Observable<StreamError>;
|
|
44
|
+
/** Emits after anything that can change a flag value: a snapshot, a delta, an override or a new context. */
|
|
45
|
+
get changes$(): Observable<void>;
|
|
46
|
+
/** The version of the environment the client holds, or `null` before it has any data. */
|
|
47
|
+
get version(): number | null;
|
|
48
|
+
get context(): EvaluationContext;
|
|
49
|
+
setContext(context: EvaluationContext): void;
|
|
50
|
+
/** The held state, for handing to the browser after server rendering. `null` before any data. */
|
|
51
|
+
snapshot(): FlagSnapshot | null;
|
|
52
|
+
/** Replaces the held state, for example with a snapshot fetched on the server or one handed over by the server. */
|
|
53
|
+
hydrate(snapshot: FlagSnapshot): void;
|
|
54
|
+
flagKeys(): readonly string[];
|
|
55
|
+
/** The declared type of a flag, or `undefined` when the flag is unknown. */
|
|
56
|
+
flagType(key: string): FlagType | undefined;
|
|
57
|
+
/** Evaluates a flag with its reason. The fallback is returned for unknown flags and for a type mismatch. */
|
|
58
|
+
resolve<T extends JsonValue>(key: string, fallback: T): Resolution<T>;
|
|
59
|
+
/** Evaluates a flag and returns only its value. */
|
|
60
|
+
value<T extends JsonValue>(key: string, fallback: T): T;
|
|
61
|
+
/** Opens the stream. Call it in browsers only: servers should use {@link fetchSnapshot} instead. */
|
|
62
|
+
start(): void;
|
|
63
|
+
stop(): void;
|
|
64
|
+
}
|
|
65
|
+
/** Creates a client. It does nothing until `start()` is called. */
|
|
66
|
+
export declare function createFlagtideClient(options: FlagtideClientOptions): FlagtideClient;
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { Subject, map, merge } from 'rxjs';
|
|
2
|
+
import { ConnectionManager, createBrowserEnvironment, createBrowserSocketFactory, } from './connection-manager.js';
|
|
3
|
+
import { FlagStore } from './flag-store.js';
|
|
4
|
+
import { FlagOverrides } from './overrides.js';
|
|
5
|
+
import { resolveFlag } from './resolve.js';
|
|
6
|
+
import { createLocalStore, createSnapshotStorage, snapshotStorageKey } from './storage.js';
|
|
7
|
+
const SDK_NAME = 'flagtide-core/1.0.0';
|
|
8
|
+
const ANONYMOUS_ID_KEY = 'flagtide:anonymous-id';
|
|
9
|
+
function randomIdentifier() {
|
|
10
|
+
const generator = globalThis.crypto;
|
|
11
|
+
return generator?.randomUUID?.() ?? Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
12
|
+
}
|
|
13
|
+
function anonymousContext(store) {
|
|
14
|
+
const known = store.get(ANONYMOUS_ID_KEY);
|
|
15
|
+
const key = known ?? randomIdentifier();
|
|
16
|
+
if (known === null) {
|
|
17
|
+
store.set(ANONYMOUS_ID_KEY, key);
|
|
18
|
+
}
|
|
19
|
+
return { key, attributes: {} };
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Evaluates flags locally and keeps them current over a stream connection.
|
|
23
|
+
*
|
|
24
|
+
* Evaluation never waits for the network: it reads the snapshot the client holds. Create one client per
|
|
25
|
+
* environment with {@link createFlagtideClient}.
|
|
26
|
+
*/
|
|
27
|
+
export class FlagtideClient {
|
|
28
|
+
/** Local overrides. A resolution reports `OVERRIDE` when one applies. */
|
|
29
|
+
overrides;
|
|
30
|
+
flagStore;
|
|
31
|
+
manager;
|
|
32
|
+
storage;
|
|
33
|
+
contextChanged = new Subject();
|
|
34
|
+
currentContext;
|
|
35
|
+
constructor(options) {
|
|
36
|
+
const store = options.store ?? createLocalStore();
|
|
37
|
+
this.storage = createSnapshotStorage(store, options.sdkKey);
|
|
38
|
+
this.currentContext = options.context ?? anonymousContext(store);
|
|
39
|
+
this.overrides = new FlagOverrides(store, `${snapshotStorageKey(options.sdkKey)}:overrides`);
|
|
40
|
+
this.flagStore = new FlagStore(options.initialSnapshot ?? this.storage.load() ?? undefined);
|
|
41
|
+
if (options.initialSnapshot !== undefined) {
|
|
42
|
+
this.storage.save(options.initialSnapshot);
|
|
43
|
+
}
|
|
44
|
+
this.flagStore.changes$.subscribe(() => {
|
|
45
|
+
const snapshot = this.flagStore.snapshot();
|
|
46
|
+
if (snapshot !== null) {
|
|
47
|
+
this.storage.save(snapshot);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
this.manager = new ConnectionManager({
|
|
51
|
+
url: options.streamUrl,
|
|
52
|
+
sdkKey: options.sdkKey,
|
|
53
|
+
store: this.flagStore,
|
|
54
|
+
clientId: this.currentContext.key,
|
|
55
|
+
sdk: SDK_NAME,
|
|
56
|
+
socketFactory: options.socketFactory ?? createBrowserSocketFactory(),
|
|
57
|
+
environment: options.environment ?? createBrowserEnvironment(),
|
|
58
|
+
...(options.scheduler === undefined ? {} : { scheduler: options.scheduler }),
|
|
59
|
+
...(options.random === undefined ? {} : { random: options.random }),
|
|
60
|
+
...(options.timing === undefined ? {} : { timing: options.timing }),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
get status$() {
|
|
64
|
+
return this.manager.status$;
|
|
65
|
+
}
|
|
66
|
+
get status() {
|
|
67
|
+
return this.manager.status;
|
|
68
|
+
}
|
|
69
|
+
get errors$() {
|
|
70
|
+
return this.manager.errors$;
|
|
71
|
+
}
|
|
72
|
+
/** Emits after anything that can change a flag value: a snapshot, a delta, an override or a new context. */
|
|
73
|
+
get changes$() {
|
|
74
|
+
return merge(this.flagStore.changes$.pipe(map(() => undefined)), this.overrides.changes$, this.contextChanged);
|
|
75
|
+
}
|
|
76
|
+
/** The version of the environment the client holds, or `null` before it has any data. */
|
|
77
|
+
get version() {
|
|
78
|
+
return this.flagStore.version;
|
|
79
|
+
}
|
|
80
|
+
get context() {
|
|
81
|
+
return this.currentContext;
|
|
82
|
+
}
|
|
83
|
+
setContext(context) {
|
|
84
|
+
this.currentContext = context;
|
|
85
|
+
this.contextChanged.next();
|
|
86
|
+
}
|
|
87
|
+
/** The held state, for handing to the browser after server rendering. `null` before any data. */
|
|
88
|
+
snapshot() {
|
|
89
|
+
return this.flagStore.snapshot();
|
|
90
|
+
}
|
|
91
|
+
/** Replaces the held state, for example with a snapshot fetched on the server or one handed over by the server. */
|
|
92
|
+
hydrate(snapshot) {
|
|
93
|
+
this.flagStore.hydrate(snapshot);
|
|
94
|
+
}
|
|
95
|
+
flagKeys() {
|
|
96
|
+
return this.flagStore.flagKeys();
|
|
97
|
+
}
|
|
98
|
+
/** The declared type of a flag, or `undefined` when the flag is unknown. */
|
|
99
|
+
flagType(key) {
|
|
100
|
+
return this.flagStore.flag(key)?.type;
|
|
101
|
+
}
|
|
102
|
+
/** Evaluates a flag with its reason. The fallback is returned for unknown flags and for a type mismatch. */
|
|
103
|
+
resolve(key, fallback) {
|
|
104
|
+
return resolveFlag(this.flagStore.flag(key), this.currentContext, this.flagStore.segments, fallback, this.overrides.get(key));
|
|
105
|
+
}
|
|
106
|
+
/** Evaluates a flag and returns only its value. */
|
|
107
|
+
value(key, fallback) {
|
|
108
|
+
return this.resolve(key, fallback).value;
|
|
109
|
+
}
|
|
110
|
+
/** Opens the stream. Call it in browsers only: servers should use {@link fetchSnapshot} instead. */
|
|
111
|
+
start() {
|
|
112
|
+
this.manager.start();
|
|
113
|
+
}
|
|
114
|
+
stop() {
|
|
115
|
+
this.manager.stop();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/** Creates a client. It does nothing until `start()` is called. */
|
|
119
|
+
export function createFlagtideClient(options) {
|
|
120
|
+
return new FlagtideClient(options);
|
|
121
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { Observable } from 'rxjs';
|
|
2
|
+
import type { SchedulerLike } from 'rxjs';
|
|
3
|
+
import type { FlagStore } from './flag-store.js';
|
|
4
|
+
/**
|
|
5
|
+
* What the SDK can say about the flags it serves.
|
|
6
|
+
*
|
|
7
|
+
* - `connecting`: the first attempt is in progress and nothing is confirmed yet
|
|
8
|
+
* - `live`: the stream is open and the data is confirmed current
|
|
9
|
+
* - `stale`: flags are served but cannot be confirmed current
|
|
10
|
+
* - `offline`: the browser has no network or the server has been unreachable for too long
|
|
11
|
+
*/
|
|
12
|
+
export type ConnectionStatus = 'connecting' | 'live' | 'offline' | 'stale';
|
|
13
|
+
/** Callbacks a socket implementation reports to the connection manager. */
|
|
14
|
+
export interface SocketHandlers {
|
|
15
|
+
open(): void;
|
|
16
|
+
message(text: string): void;
|
|
17
|
+
close(code: number): void;
|
|
18
|
+
}
|
|
19
|
+
/** The part of a socket the connection manager drives. */
|
|
20
|
+
export interface SocketControl {
|
|
21
|
+
send(text: string): void;
|
|
22
|
+
close(code?: number): void;
|
|
23
|
+
}
|
|
24
|
+
/** Opens a socket to `url` and reports its events to `handlers`. */
|
|
25
|
+
export type SocketFactory = (url: string, handlers: SocketHandlers) => SocketControl;
|
|
26
|
+
/** The browser facts the connection manager reacts to. Inject a fake in tests, or use `createBrowserEnvironment`. */
|
|
27
|
+
export interface NetworkEnvironment {
|
|
28
|
+
isOnline(): boolean;
|
|
29
|
+
readonly online$: Observable<boolean>;
|
|
30
|
+
readonly visible$: Observable<boolean>;
|
|
31
|
+
}
|
|
32
|
+
/** An error reported by the server or found in what it sent. `code` follows the close codes of the stream protocol. */
|
|
33
|
+
export interface StreamError {
|
|
34
|
+
readonly code: number;
|
|
35
|
+
readonly message: string;
|
|
36
|
+
}
|
|
37
|
+
/** Backoff, heartbeat and offline limits. The defaults are the constants of the stream protocol. */
|
|
38
|
+
export interface ConnectionTiming {
|
|
39
|
+
readonly backoffBaseMs: number;
|
|
40
|
+
readonly backoffCapMs: number;
|
|
41
|
+
readonly staleAfterMs: number;
|
|
42
|
+
readonly connectTimeoutMs: number;
|
|
43
|
+
readonly offlineAfterMs: number;
|
|
44
|
+
}
|
|
45
|
+
export declare const DEFAULT_TIMING: ConnectionTiming;
|
|
46
|
+
export declare const CLOSE_UNAUTHORIZED = 4401;
|
|
47
|
+
export declare const CLOSE_BAD_FRAME = 4400;
|
|
48
|
+
export interface ConnectionManagerOptions {
|
|
49
|
+
readonly url: string;
|
|
50
|
+
readonly sdkKey: string;
|
|
51
|
+
readonly store: FlagStore;
|
|
52
|
+
readonly clientId?: string;
|
|
53
|
+
readonly sdk?: string;
|
|
54
|
+
readonly socketFactory: SocketFactory;
|
|
55
|
+
readonly environment?: NetworkEnvironment;
|
|
56
|
+
readonly scheduler?: SchedulerLike;
|
|
57
|
+
readonly random?: () => number;
|
|
58
|
+
readonly timing?: Partial<ConnectionTiming>;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The delay before reconnect attempt number `attempt` (zero based): `random(0, min(cap, base * 2^attempt))`.
|
|
62
|
+
* Full jitter keeps thousands of clients that lost the same server from reconnecting in step.
|
|
63
|
+
*/
|
|
64
|
+
export declare function backoffDelay(attempt: number, random: () => number, baseMs: number, capMs: number): number;
|
|
65
|
+
type BrowserSocket = {
|
|
66
|
+
onopen: (() => void) | null;
|
|
67
|
+
onmessage: ((event: {
|
|
68
|
+
data: unknown;
|
|
69
|
+
}) => void) | null;
|
|
70
|
+
onclose: ((event: {
|
|
71
|
+
code: number;
|
|
72
|
+
}) => void) | null;
|
|
73
|
+
onerror: (() => void) | null;
|
|
74
|
+
send(data: string): void;
|
|
75
|
+
close(code?: number): void;
|
|
76
|
+
};
|
|
77
|
+
type SocketConstructor = new (url: string) => BrowserSocket;
|
|
78
|
+
/** A socket factory on top of the `WebSocket` constructor of the runtime (browsers, Node 22 and later). */
|
|
79
|
+
export declare function createBrowserSocketFactory(override?: SocketConstructor): SocketFactory;
|
|
80
|
+
/** Online and visibility state from `window` and `document`. Outside a browser it reports online and never changes. */
|
|
81
|
+
export declare function createBrowserEnvironment(): NetworkEnvironment;
|
|
82
|
+
/**
|
|
83
|
+
* Keeps a stream connection to the server and feeds a {@link FlagStore}.
|
|
84
|
+
*
|
|
85
|
+
* It reconnects with exponential backoff and full jitter, resumes from the version the store holds, treats a
|
|
86
|
+
* silent connection as dead by comparing timestamps (background tabs throttle timers), and acknowledges
|
|
87
|
+
* every applied frame. All time comes from the injected scheduler and all randomness from the injected
|
|
88
|
+
* random source, so the behavior can be tested with a virtual clock.
|
|
89
|
+
*/
|
|
90
|
+
export declare class ConnectionManager {
|
|
91
|
+
private readonly options;
|
|
92
|
+
private readonly timing;
|
|
93
|
+
private readonly scheduler;
|
|
94
|
+
private readonly random;
|
|
95
|
+
private readonly environment;
|
|
96
|
+
private readonly statusSubject;
|
|
97
|
+
private readonly errorSubject;
|
|
98
|
+
private environmentSubscription;
|
|
99
|
+
private socket;
|
|
100
|
+
private generation;
|
|
101
|
+
private attempt;
|
|
102
|
+
private failingSince;
|
|
103
|
+
private lastFrameAt;
|
|
104
|
+
private silenceLimitMs;
|
|
105
|
+
private forceSnapshot;
|
|
106
|
+
private running;
|
|
107
|
+
private halted;
|
|
108
|
+
private retryTimer;
|
|
109
|
+
private watchdogTimer;
|
|
110
|
+
constructor(options: ConnectionManagerOptions);
|
|
111
|
+
get status$(): Observable<ConnectionStatus>;
|
|
112
|
+
get status(): ConnectionStatus;
|
|
113
|
+
get errors$(): Observable<StreamError>;
|
|
114
|
+
start(): void;
|
|
115
|
+
stop(): void;
|
|
116
|
+
private initialStatus;
|
|
117
|
+
private setStatus;
|
|
118
|
+
private now;
|
|
119
|
+
private connect;
|
|
120
|
+
private sendHello;
|
|
121
|
+
private receive;
|
|
122
|
+
private handle;
|
|
123
|
+
private handleDeltas;
|
|
124
|
+
private confirmed;
|
|
125
|
+
private acknowledge;
|
|
126
|
+
private connectionLost;
|
|
127
|
+
private scheduleReconnect;
|
|
128
|
+
private reconnectNow;
|
|
129
|
+
private statusWhileRetrying;
|
|
130
|
+
private networkChanged;
|
|
131
|
+
private armWatchdog;
|
|
132
|
+
private checkSilence;
|
|
133
|
+
private dropSocket;
|
|
134
|
+
private clearRetry;
|
|
135
|
+
private clearWatchdog;
|
|
136
|
+
}
|
|
137
|
+
export {};
|