@canarygate/sdk 0.1.0 → 0.1.2
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 +11 -8
- package/dist/{canary-gate-base-D069Q1Z_.d.mts → canary-gate-base-iXS3mVyK.d.mts} +6 -2
- package/dist/{canary-gate-base-D069Q1Z_.d.ts → canary-gate-base-iXS3mVyK.d.ts} +6 -2
- package/dist/{chunk-7KAJ7OJO.mjs → chunk-LMPIWD3Z.mjs} +30 -6
- package/dist/client.d.mts +1 -1
- package/dist/client.d.ts +1 -1
- package/dist/client.js +35 -9
- package/dist/client.mjs +6 -4
- package/dist/server.d.mts +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js +36 -7
- package/dist/server.mjs +7 -2
- package/package.json +12 -21
package/README.md
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Feature flag client for [CanaryGate](https://github.com/rborges98/canarygate).
|
|
4
4
|
|
|
5
|
-
- **Server (Node.js)** — snapshot on `init()` + live updates via SSE,
|
|
6
|
-
- **Browser** —
|
|
5
|
+
- **Server (Node.js)** — snapshot on `init()` + live updates via SSE, auto-reconnect and heartbeat
|
|
6
|
+
- **Browser** — snapshot + polling via `pollIntervalMs`
|
|
7
7
|
|
|
8
8
|
## Install
|
|
9
9
|
|
|
@@ -19,12 +19,11 @@ npm install @canarygate/sdk
|
|
|
19
19
|
import { CanaryGate } from '@canarygate/sdk/server'
|
|
20
20
|
|
|
21
21
|
const gate = new CanaryGate('your-api-key', {
|
|
22
|
-
environment: 'production'
|
|
23
|
-
stream: true
|
|
22
|
+
environment: 'production'
|
|
24
23
|
})
|
|
25
24
|
|
|
26
25
|
await gate.init()
|
|
27
|
-
//
|
|
26
|
+
// the snapshot is fetched and SSE opens automatically
|
|
28
27
|
|
|
29
28
|
gate.disconnect() // when shutting down
|
|
30
29
|
```
|
|
@@ -70,7 +69,7 @@ Rollout evaluation hashes `userId` deterministically — the same user always ge
|
|
|
70
69
|
| -------------------- | --------- | ----------------------- | ------------------------------------------------ |
|
|
71
70
|
| `baseUrl` | `string` | `http://localhost:3001` | CanaryGate API base URL |
|
|
72
71
|
| `environment` | `string` | — | Environment to evaluate flags against |
|
|
73
|
-
| `
|
|
72
|
+
| `pollIntervalMs` | `number` | `30000` | Browser polling interval in ms (clamped to a 3000ms minimum). Set `0` to disable |
|
|
74
73
|
| `reconnectDelay` | `number` | `5000` | Initial SSE reconnect delay (ms) |
|
|
75
74
|
| `maxReconnectDelay` | `number` | `30000` | Reconnect delay cap, exponential backoff (ms) |
|
|
76
75
|
| `heartbeatTimeoutMs` | `number` | `65000` | Silence window before treating the stream as dead |
|
|
@@ -79,7 +78,7 @@ Rollout evaluation hashes `userId` deterministically — the same user always ge
|
|
|
79
78
|
|
|
80
79
|
| Method | Description |
|
|
81
80
|
| --------------------- | -------------------------------------------------- |
|
|
82
|
-
| `init()` | Fetches
|
|
81
|
+
| `init()` | Fetches the snapshot; the server entry opens SSE, the browser entry starts polling |
|
|
83
82
|
| `getFlag(key, ctx?)` | Evaluates one flag (`boolean` or `rollout`) |
|
|
84
83
|
| `getFlags(ctx?)` | Evaluates all cached flags |
|
|
85
84
|
| `isStale()` | Whether the last sync attempt failed |
|
|
@@ -89,9 +88,13 @@ Rollout evaluation hashes `userId` deterministically — the same user always ge
|
|
|
89
88
|
## How sync works
|
|
90
89
|
|
|
91
90
|
1. `init()` fetches a full snapshot from `/sdk/flags`.
|
|
92
|
-
2.
|
|
91
|
+
2. The server entry always opens an SSE connection that receives granular updates per change; the browser entry polls the snapshot every `pollIntervalMs`.
|
|
93
92
|
3. On disconnect, the SDK reconnects with exponential backoff and does a full resync over the snapshot endpoint.
|
|
94
93
|
|
|
94
|
+
## Entry-point guards
|
|
95
|
+
|
|
96
|
+
Each entry point is bound to its environment: the server entry throws `Error('@canarygate/sdk/server is server-only. In browsers import from "@canarygate/sdk/client" instead.')` if used where `window` exists, and the client entry throws a browser-only error if used where there is no `window`.
|
|
97
|
+
|
|
95
98
|
## License
|
|
96
99
|
|
|
97
100
|
[MIT](./LICENSE)
|
|
@@ -16,7 +16,8 @@ type FlagEvaluationContext = {
|
|
|
16
16
|
type CanaryGateOptions = {
|
|
17
17
|
baseUrl?: string;
|
|
18
18
|
environment?: string;
|
|
19
|
-
|
|
19
|
+
/** Browser polling interval in ms. Default: 30000; minimum enforced: 3000. Set 0 to disable. Server always uses SSE instead. */
|
|
20
|
+
pollIntervalMs?: number;
|
|
20
21
|
reconnectDelay?: number;
|
|
21
22
|
maxReconnectDelay?: number;
|
|
22
23
|
heartbeatTimeoutMs?: number;
|
|
@@ -31,19 +32,20 @@ declare class CanaryGateBase {
|
|
|
31
32
|
private readonly reconnectDelay;
|
|
32
33
|
private readonly maxReconnectDelay;
|
|
33
34
|
private readonly heartbeatTimeoutMs;
|
|
35
|
+
private readonly pollIntervalMs;
|
|
34
36
|
private cache;
|
|
35
37
|
private cacheVersions;
|
|
36
38
|
private readonly anonId;
|
|
37
39
|
private streamAbortController;
|
|
38
40
|
private reconnectTimeout;
|
|
39
41
|
private heartbeatTimeout;
|
|
42
|
+
private pollTimeout;
|
|
40
43
|
private streamRetryDelay;
|
|
41
44
|
private reconnectAttempts;
|
|
42
45
|
private stale;
|
|
43
46
|
private lastSyncAt;
|
|
44
47
|
private destroyed;
|
|
45
48
|
constructor(apiKey: string, options: CanaryGateOptions | undefined, streamEnabled: boolean, anonIdFactory: () => string);
|
|
46
|
-
protected warnStreamDisabled(): void;
|
|
47
49
|
init(): Promise<void>;
|
|
48
50
|
private replaceCacheFromSnapshot;
|
|
49
51
|
private fetchFlags;
|
|
@@ -55,6 +57,8 @@ declare class CanaryGateBase {
|
|
|
55
57
|
private scheduleReconnect;
|
|
56
58
|
private consumeStream;
|
|
57
59
|
private connectStream;
|
|
60
|
+
private startPolling;
|
|
61
|
+
private stopPolling;
|
|
58
62
|
getFlag(key: string, context?: FlagEvaluationContext): FlagData | undefined;
|
|
59
63
|
getFlags(context?: FlagEvaluationContext): FlagData[];
|
|
60
64
|
isStale(): boolean;
|
|
@@ -16,7 +16,8 @@ type FlagEvaluationContext = {
|
|
|
16
16
|
type CanaryGateOptions = {
|
|
17
17
|
baseUrl?: string;
|
|
18
18
|
environment?: string;
|
|
19
|
-
|
|
19
|
+
/** Browser polling interval in ms. Default: 30000; minimum enforced: 3000. Set 0 to disable. Server always uses SSE instead. */
|
|
20
|
+
pollIntervalMs?: number;
|
|
20
21
|
reconnectDelay?: number;
|
|
21
22
|
maxReconnectDelay?: number;
|
|
22
23
|
heartbeatTimeoutMs?: number;
|
|
@@ -31,19 +32,20 @@ declare class CanaryGateBase {
|
|
|
31
32
|
private readonly reconnectDelay;
|
|
32
33
|
private readonly maxReconnectDelay;
|
|
33
34
|
private readonly heartbeatTimeoutMs;
|
|
35
|
+
private readonly pollIntervalMs;
|
|
34
36
|
private cache;
|
|
35
37
|
private cacheVersions;
|
|
36
38
|
private readonly anonId;
|
|
37
39
|
private streamAbortController;
|
|
38
40
|
private reconnectTimeout;
|
|
39
41
|
private heartbeatTimeout;
|
|
42
|
+
private pollTimeout;
|
|
40
43
|
private streamRetryDelay;
|
|
41
44
|
private reconnectAttempts;
|
|
42
45
|
private stale;
|
|
43
46
|
private lastSyncAt;
|
|
44
47
|
private destroyed;
|
|
45
48
|
constructor(apiKey: string, options: CanaryGateOptions | undefined, streamEnabled: boolean, anonIdFactory: () => string);
|
|
46
|
-
protected warnStreamDisabled(): void;
|
|
47
49
|
init(): Promise<void>;
|
|
48
50
|
private replaceCacheFromSnapshot;
|
|
49
51
|
private fetchFlags;
|
|
@@ -55,6 +57,8 @@ declare class CanaryGateBase {
|
|
|
55
57
|
private scheduleReconnect;
|
|
56
58
|
private consumeStream;
|
|
57
59
|
private connectStream;
|
|
60
|
+
private startPolling;
|
|
61
|
+
private stopPolling;
|
|
58
62
|
getFlag(key: string, context?: FlagEvaluationContext): FlagData | undefined;
|
|
59
63
|
getFlags(context?: FlagEvaluationContext): FlagData[];
|
|
60
64
|
isStale(): boolean;
|
|
@@ -42,6 +42,8 @@ function parseSseEventBlock(block) {
|
|
|
42
42
|
// src/canary-gate-base.ts
|
|
43
43
|
var DEFAULT_MAX_RECONNECT_DELAY_MS = 3e4;
|
|
44
44
|
var DEFAULT_HEARTBEAT_TIMEOUT_MS = 65e3;
|
|
45
|
+
var DEFAULT_POLL_INTERVAL_MS = 3e4;
|
|
46
|
+
var MIN_POLL_INTERVAL_MS = 3e3;
|
|
45
47
|
function isAbortError(error) {
|
|
46
48
|
return error instanceof DOMException && error.name === "AbortError" || error instanceof Error && error.name === "AbortError";
|
|
47
49
|
}
|
|
@@ -59,6 +61,7 @@ var CanaryGateBase = class {
|
|
|
59
61
|
this.streamAbortController = null;
|
|
60
62
|
this.reconnectTimeout = null;
|
|
61
63
|
this.heartbeatTimeout = null;
|
|
64
|
+
this.pollTimeout = null;
|
|
62
65
|
this.reconnectAttempts = 0;
|
|
63
66
|
this.stale = false;
|
|
64
67
|
this.lastSyncAt = null;
|
|
@@ -74,17 +77,23 @@ var CanaryGateBase = class {
|
|
|
74
77
|
this.reconnectDelay
|
|
75
78
|
);
|
|
76
79
|
this.heartbeatTimeoutMs = options.heartbeatTimeoutMs ?? DEFAULT_HEARTBEAT_TIMEOUT_MS;
|
|
80
|
+
this.pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
81
|
+
if (options.pollIntervalMs !== void 0 && options.pollIntervalMs > 0 && options.pollIntervalMs < MIN_POLL_INTERVAL_MS) {
|
|
82
|
+
console.warn(
|
|
83
|
+
`[canarygate] pollIntervalMs must be at least ${MIN_POLL_INTERVAL_MS}ms; clamped to ${MIN_POLL_INTERVAL_MS}.`
|
|
84
|
+
);
|
|
85
|
+
this.pollIntervalMs = MIN_POLL_INTERVAL_MS;
|
|
86
|
+
}
|
|
77
87
|
this.streamRetryDelay = this.reconnectDelay;
|
|
78
88
|
this.anonId = anonIdFactory();
|
|
79
89
|
}
|
|
80
|
-
warnStreamDisabled() {
|
|
81
|
-
console.warn(
|
|
82
|
-
"[canarygate] Real-time streams (SSE) are disabled in browser environments to protect network architecture."
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
90
|
async init() {
|
|
86
91
|
await this.fetchFlags();
|
|
87
|
-
if (this.streamEnabled)
|
|
92
|
+
if (this.streamEnabled) {
|
|
93
|
+
this.connectStream();
|
|
94
|
+
} else {
|
|
95
|
+
this.startPolling();
|
|
96
|
+
}
|
|
88
97
|
}
|
|
89
98
|
replaceCacheFromSnapshot(flags, requestedAt) {
|
|
90
99
|
const nextCache = /* @__PURE__ */ new Map();
|
|
@@ -270,6 +279,20 @@ var CanaryGateBase = class {
|
|
|
270
279
|
this.streamAbortController = abortController;
|
|
271
280
|
void this.consumeStream(abortController);
|
|
272
281
|
}
|
|
282
|
+
startPolling() {
|
|
283
|
+
if (this.destroyed || this.streamEnabled || this.pollIntervalMs <= 0 || this.pollTimeout) {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
this.pollTimeout = setInterval(() => {
|
|
287
|
+
void this.fetchFlags();
|
|
288
|
+
}, this.pollIntervalMs);
|
|
289
|
+
}
|
|
290
|
+
stopPolling() {
|
|
291
|
+
if (this.pollTimeout) {
|
|
292
|
+
clearInterval(this.pollTimeout);
|
|
293
|
+
this.pollTimeout = null;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
273
296
|
getFlag(key, context) {
|
|
274
297
|
const raw = this.cache.get(key);
|
|
275
298
|
if (!raw) return void 0;
|
|
@@ -298,6 +321,7 @@ var CanaryGateBase = class {
|
|
|
298
321
|
}
|
|
299
322
|
disconnect() {
|
|
300
323
|
this.destroyed = true;
|
|
324
|
+
this.stopPolling();
|
|
301
325
|
if (this.reconnectTimeout) {
|
|
302
326
|
clearTimeout(this.reconnectTimeout);
|
|
303
327
|
this.reconnectTimeout = null;
|
package/dist/client.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as CanaryGateBase, a as CanaryGateOptions } from './canary-gate-base-
|
|
1
|
+
import { C as CanaryGateBase, a as CanaryGateOptions } from './canary-gate-base-iXS3mVyK.mjs';
|
|
2
2
|
|
|
3
3
|
declare class CanaryGate extends CanaryGateBase {
|
|
4
4
|
constructor(apiKey: string, options?: CanaryGateOptions);
|
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as CanaryGateBase, a as CanaryGateOptions } from './canary-gate-base-
|
|
1
|
+
import { C as CanaryGateBase, a as CanaryGateOptions } from './canary-gate-base-iXS3mVyK.js';
|
|
2
2
|
|
|
3
3
|
declare class CanaryGate extends CanaryGateBase {
|
|
4
4
|
constructor(apiKey: string, options?: CanaryGateOptions);
|
package/dist/client.js
CHANGED
|
@@ -68,6 +68,8 @@ function parseSseEventBlock(block) {
|
|
|
68
68
|
// src/canary-gate-base.ts
|
|
69
69
|
var DEFAULT_MAX_RECONNECT_DELAY_MS = 3e4;
|
|
70
70
|
var DEFAULT_HEARTBEAT_TIMEOUT_MS = 65e3;
|
|
71
|
+
var DEFAULT_POLL_INTERVAL_MS = 3e4;
|
|
72
|
+
var MIN_POLL_INTERVAL_MS = 3e3;
|
|
71
73
|
function isAbortError(error) {
|
|
72
74
|
return error instanceof DOMException && error.name === "AbortError" || error instanceof Error && error.name === "AbortError";
|
|
73
75
|
}
|
|
@@ -85,6 +87,7 @@ var CanaryGateBase = class {
|
|
|
85
87
|
this.streamAbortController = null;
|
|
86
88
|
this.reconnectTimeout = null;
|
|
87
89
|
this.heartbeatTimeout = null;
|
|
90
|
+
this.pollTimeout = null;
|
|
88
91
|
this.reconnectAttempts = 0;
|
|
89
92
|
this.stale = false;
|
|
90
93
|
this.lastSyncAt = null;
|
|
@@ -100,17 +103,23 @@ var CanaryGateBase = class {
|
|
|
100
103
|
this.reconnectDelay
|
|
101
104
|
);
|
|
102
105
|
this.heartbeatTimeoutMs = options.heartbeatTimeoutMs ?? DEFAULT_HEARTBEAT_TIMEOUT_MS;
|
|
106
|
+
this.pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
107
|
+
if (options.pollIntervalMs !== void 0 && options.pollIntervalMs > 0 && options.pollIntervalMs < MIN_POLL_INTERVAL_MS) {
|
|
108
|
+
console.warn(
|
|
109
|
+
`[canarygate] pollIntervalMs must be at least ${MIN_POLL_INTERVAL_MS}ms; clamped to ${MIN_POLL_INTERVAL_MS}.`
|
|
110
|
+
);
|
|
111
|
+
this.pollIntervalMs = MIN_POLL_INTERVAL_MS;
|
|
112
|
+
}
|
|
103
113
|
this.streamRetryDelay = this.reconnectDelay;
|
|
104
114
|
this.anonId = anonIdFactory();
|
|
105
115
|
}
|
|
106
|
-
warnStreamDisabled() {
|
|
107
|
-
console.warn(
|
|
108
|
-
"[canarygate] Real-time streams (SSE) are disabled in browser environments to protect network architecture."
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
116
|
async init() {
|
|
112
117
|
await this.fetchFlags();
|
|
113
|
-
if (this.streamEnabled)
|
|
118
|
+
if (this.streamEnabled) {
|
|
119
|
+
this.connectStream();
|
|
120
|
+
} else {
|
|
121
|
+
this.startPolling();
|
|
122
|
+
}
|
|
114
123
|
}
|
|
115
124
|
replaceCacheFromSnapshot(flags, requestedAt) {
|
|
116
125
|
const nextCache = /* @__PURE__ */ new Map();
|
|
@@ -296,6 +305,20 @@ var CanaryGateBase = class {
|
|
|
296
305
|
this.streamAbortController = abortController;
|
|
297
306
|
void this.consumeStream(abortController);
|
|
298
307
|
}
|
|
308
|
+
startPolling() {
|
|
309
|
+
if (this.destroyed || this.streamEnabled || this.pollIntervalMs <= 0 || this.pollTimeout) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
this.pollTimeout = setInterval(() => {
|
|
313
|
+
void this.fetchFlags();
|
|
314
|
+
}, this.pollIntervalMs);
|
|
315
|
+
}
|
|
316
|
+
stopPolling() {
|
|
317
|
+
if (this.pollTimeout) {
|
|
318
|
+
clearInterval(this.pollTimeout);
|
|
319
|
+
this.pollTimeout = null;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
299
322
|
getFlag(key, context) {
|
|
300
323
|
const raw = this.cache.get(key);
|
|
301
324
|
if (!raw) return void 0;
|
|
@@ -324,6 +347,7 @@ var CanaryGateBase = class {
|
|
|
324
347
|
}
|
|
325
348
|
disconnect() {
|
|
326
349
|
this.destroyed = true;
|
|
350
|
+
this.stopPolling();
|
|
327
351
|
if (this.reconnectTimeout) {
|
|
328
352
|
clearTimeout(this.reconnectTimeout);
|
|
329
353
|
this.reconnectTimeout = null;
|
|
@@ -348,10 +372,12 @@ function getOrCreateAnonId() {
|
|
|
348
372
|
}
|
|
349
373
|
var CanaryGate = class extends CanaryGateBase {
|
|
350
374
|
constructor(apiKey, options = {}) {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
375
|
+
if (typeof window === "undefined") {
|
|
376
|
+
throw new Error(
|
|
377
|
+
'@canarygate/sdk/client is browser-only. On server runtimes (Node.js, Deno, Bun, Edge) import from "@canarygate/sdk/server" instead.'
|
|
378
|
+
);
|
|
354
379
|
}
|
|
380
|
+
super(apiKey, options, false, getOrCreateAnonId);
|
|
355
381
|
}
|
|
356
382
|
};
|
|
357
383
|
// Annotate the CommonJS export names for ESM import in node:
|
package/dist/client.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CanaryGateBase
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-LMPIWD3Z.mjs";
|
|
4
4
|
|
|
5
5
|
// src/client.ts
|
|
6
6
|
var ANON_ID_KEY = "__cg_anon_id__";
|
|
@@ -16,10 +16,12 @@ function getOrCreateAnonId() {
|
|
|
16
16
|
}
|
|
17
17
|
var CanaryGate = class extends CanaryGateBase {
|
|
18
18
|
constructor(apiKey, options = {}) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
if (typeof window === "undefined") {
|
|
20
|
+
throw new Error(
|
|
21
|
+
'@canarygate/sdk/client is browser-only. On server runtimes (Node.js, Deno, Bun, Edge) import from "@canarygate/sdk/server" instead.'
|
|
22
|
+
);
|
|
22
23
|
}
|
|
24
|
+
super(apiKey, options, false, getOrCreateAnonId);
|
|
23
25
|
}
|
|
24
26
|
};
|
|
25
27
|
export {
|
package/dist/server.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as CanaryGateBase, a as CanaryGateOptions } from './canary-gate-base-
|
|
1
|
+
import { C as CanaryGateBase, a as CanaryGateOptions } from './canary-gate-base-iXS3mVyK.mjs';
|
|
2
2
|
|
|
3
3
|
declare class CanaryGate extends CanaryGateBase {
|
|
4
4
|
constructor(apiKey: string, options?: CanaryGateOptions);
|
package/dist/server.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as CanaryGateBase, a as CanaryGateOptions } from './canary-gate-base-
|
|
1
|
+
import { C as CanaryGateBase, a as CanaryGateOptions } from './canary-gate-base-iXS3mVyK.js';
|
|
2
2
|
|
|
3
3
|
declare class CanaryGate extends CanaryGateBase {
|
|
4
4
|
constructor(apiKey: string, options?: CanaryGateOptions);
|
package/dist/server.js
CHANGED
|
@@ -68,6 +68,8 @@ function parseSseEventBlock(block) {
|
|
|
68
68
|
// src/canary-gate-base.ts
|
|
69
69
|
var DEFAULT_MAX_RECONNECT_DELAY_MS = 3e4;
|
|
70
70
|
var DEFAULT_HEARTBEAT_TIMEOUT_MS = 65e3;
|
|
71
|
+
var DEFAULT_POLL_INTERVAL_MS = 3e4;
|
|
72
|
+
var MIN_POLL_INTERVAL_MS = 3e3;
|
|
71
73
|
function isAbortError(error) {
|
|
72
74
|
return error instanceof DOMException && error.name === "AbortError" || error instanceof Error && error.name === "AbortError";
|
|
73
75
|
}
|
|
@@ -85,6 +87,7 @@ var CanaryGateBase = class {
|
|
|
85
87
|
this.streamAbortController = null;
|
|
86
88
|
this.reconnectTimeout = null;
|
|
87
89
|
this.heartbeatTimeout = null;
|
|
90
|
+
this.pollTimeout = null;
|
|
88
91
|
this.reconnectAttempts = 0;
|
|
89
92
|
this.stale = false;
|
|
90
93
|
this.lastSyncAt = null;
|
|
@@ -100,17 +103,23 @@ var CanaryGateBase = class {
|
|
|
100
103
|
this.reconnectDelay
|
|
101
104
|
);
|
|
102
105
|
this.heartbeatTimeoutMs = options.heartbeatTimeoutMs ?? DEFAULT_HEARTBEAT_TIMEOUT_MS;
|
|
106
|
+
this.pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
107
|
+
if (options.pollIntervalMs !== void 0 && options.pollIntervalMs > 0 && options.pollIntervalMs < MIN_POLL_INTERVAL_MS) {
|
|
108
|
+
console.warn(
|
|
109
|
+
`[canarygate] pollIntervalMs must be at least ${MIN_POLL_INTERVAL_MS}ms; clamped to ${MIN_POLL_INTERVAL_MS}.`
|
|
110
|
+
);
|
|
111
|
+
this.pollIntervalMs = MIN_POLL_INTERVAL_MS;
|
|
112
|
+
}
|
|
103
113
|
this.streamRetryDelay = this.reconnectDelay;
|
|
104
114
|
this.anonId = anonIdFactory();
|
|
105
115
|
}
|
|
106
|
-
warnStreamDisabled() {
|
|
107
|
-
console.warn(
|
|
108
|
-
"[canarygate] Real-time streams (SSE) are disabled in browser environments to protect network architecture."
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
116
|
async init() {
|
|
112
117
|
await this.fetchFlags();
|
|
113
|
-
if (this.streamEnabled)
|
|
118
|
+
if (this.streamEnabled) {
|
|
119
|
+
this.connectStream();
|
|
120
|
+
} else {
|
|
121
|
+
this.startPolling();
|
|
122
|
+
}
|
|
114
123
|
}
|
|
115
124
|
replaceCacheFromSnapshot(flags, requestedAt) {
|
|
116
125
|
const nextCache = /* @__PURE__ */ new Map();
|
|
@@ -296,6 +305,20 @@ var CanaryGateBase = class {
|
|
|
296
305
|
this.streamAbortController = abortController;
|
|
297
306
|
void this.consumeStream(abortController);
|
|
298
307
|
}
|
|
308
|
+
startPolling() {
|
|
309
|
+
if (this.destroyed || this.streamEnabled || this.pollIntervalMs <= 0 || this.pollTimeout) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
this.pollTimeout = setInterval(() => {
|
|
313
|
+
void this.fetchFlags();
|
|
314
|
+
}, this.pollIntervalMs);
|
|
315
|
+
}
|
|
316
|
+
stopPolling() {
|
|
317
|
+
if (this.pollTimeout) {
|
|
318
|
+
clearInterval(this.pollTimeout);
|
|
319
|
+
this.pollTimeout = null;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
299
322
|
getFlag(key, context) {
|
|
300
323
|
const raw = this.cache.get(key);
|
|
301
324
|
if (!raw) return void 0;
|
|
@@ -324,6 +347,7 @@ var CanaryGateBase = class {
|
|
|
324
347
|
}
|
|
325
348
|
disconnect() {
|
|
326
349
|
this.destroyed = true;
|
|
350
|
+
this.stopPolling();
|
|
327
351
|
if (this.reconnectTimeout) {
|
|
328
352
|
clearTimeout(this.reconnectTimeout);
|
|
329
353
|
this.reconnectTimeout = null;
|
|
@@ -337,7 +361,12 @@ var CanaryGateBase = class {
|
|
|
337
361
|
// src/server.ts
|
|
338
362
|
var CanaryGate = class extends CanaryGateBase {
|
|
339
363
|
constructor(apiKey, options = {}) {
|
|
340
|
-
|
|
364
|
+
if (typeof window !== "undefined") {
|
|
365
|
+
throw new Error(
|
|
366
|
+
'@canarygate/sdk/server is server-only. In browsers import from "@canarygate/sdk/client" instead.'
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
super(apiKey, options, true, () => crypto.randomUUID());
|
|
341
370
|
}
|
|
342
371
|
};
|
|
343
372
|
// Annotate the CommonJS export names for ESM import in node:
|
package/dist/server.mjs
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CanaryGateBase
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-LMPIWD3Z.mjs";
|
|
4
4
|
|
|
5
5
|
// src/server.ts
|
|
6
6
|
var CanaryGate = class extends CanaryGateBase {
|
|
7
7
|
constructor(apiKey, options = {}) {
|
|
8
|
-
|
|
8
|
+
if (typeof window !== "undefined") {
|
|
9
|
+
throw new Error(
|
|
10
|
+
'@canarygate/sdk/server is server-only. In browsers import from "@canarygate/sdk/client" instead.'
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
super(apiKey, options, true, () => crypto.randomUUID());
|
|
9
14
|
}
|
|
10
15
|
};
|
|
11
16
|
export {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canarygate/sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "CanaryGate SDK — feature flags with real-time SSE streaming",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./client": {
|
|
@@ -15,20 +15,18 @@
|
|
|
15
15
|
},
|
|
16
16
|
"./package.json": "./package.json"
|
|
17
17
|
},
|
|
18
|
-
"files": [
|
|
19
|
-
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
|
|
18
|
+
"files": ["dist", "README.md", "LICENSE"],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsup src/client.ts src/server.ts --format esm,cjs --dts --out-dir dist",
|
|
21
|
+
"check-types": "tsc --noEmit",
|
|
22
|
+
"test": "vitest run",
|
|
23
|
+
"test:watch": "vitest",
|
|
24
|
+
"test:coverage": "vitest run --coverage",
|
|
25
|
+
"prepublishOnly": "pnpm build"
|
|
26
|
+
},
|
|
23
27
|
"sideEffects": false,
|
|
24
28
|
"license": "MIT",
|
|
25
|
-
"keywords": [
|
|
26
|
-
"feature-flags",
|
|
27
|
-
"feature-toggles",
|
|
28
|
-
"sse",
|
|
29
|
-
"real-time",
|
|
30
|
-
"rollout"
|
|
31
|
-
],
|
|
29
|
+
"keywords": ["feature-flags", "feature-toggles", "sse", "real-time", "rollout"],
|
|
32
30
|
"repository": {
|
|
33
31
|
"type": "git",
|
|
34
32
|
"url": "git+https://github.com/rborges98/canarygate.git",
|
|
@@ -47,12 +45,5 @@
|
|
|
47
45
|
"tsup": "^8.5.1",
|
|
48
46
|
"typescript": "^5",
|
|
49
47
|
"vitest": "^3.0.0"
|
|
50
|
-
},
|
|
51
|
-
"scripts": {
|
|
52
|
-
"build": "tsup src/client.ts src/server.ts --format esm,cjs --dts --out-dir dist",
|
|
53
|
-
"check-types": "tsc --noEmit",
|
|
54
|
-
"test": "vitest run",
|
|
55
|
-
"test:watch": "vitest",
|
|
56
|
-
"test:coverage": "vitest run --coverage"
|
|
57
48
|
}
|
|
58
|
-
}
|
|
49
|
+
}
|