@canarygate/sdk 0.1.0 → 0.1.1

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 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, with auto-reconnect and heartbeat detection
6
- - **Browser** — flags fetched once and cached; SSE stays off to protect your infrastructure
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
- // flags keep updating in the background via SSE
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
- | `stream` | `boolean` | `false` | Real-time SSE updates (server mode only) |
72
+ | `pollIntervalMs` | `number` | `30000` | Browser polling interval in ms. 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 flags and starts the stream when enabled |
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. With `stream: true`, an SSE connection receives granular updates per change.
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,7 @@ type FlagEvaluationContext = {
16
16
  type CanaryGateOptions = {
17
17
  baseUrl?: string;
18
18
  environment?: string;
19
- stream?: boolean;
19
+ pollIntervalMs?: number;
20
20
  reconnectDelay?: number;
21
21
  maxReconnectDelay?: number;
22
22
  heartbeatTimeoutMs?: number;
@@ -31,19 +31,20 @@ declare class CanaryGateBase {
31
31
  private readonly reconnectDelay;
32
32
  private readonly maxReconnectDelay;
33
33
  private readonly heartbeatTimeoutMs;
34
+ private readonly pollIntervalMs;
34
35
  private cache;
35
36
  private cacheVersions;
36
37
  private readonly anonId;
37
38
  private streamAbortController;
38
39
  private reconnectTimeout;
39
40
  private heartbeatTimeout;
41
+ private pollTimeout;
40
42
  private streamRetryDelay;
41
43
  private reconnectAttempts;
42
44
  private stale;
43
45
  private lastSyncAt;
44
46
  private destroyed;
45
47
  constructor(apiKey: string, options: CanaryGateOptions | undefined, streamEnabled: boolean, anonIdFactory: () => string);
46
- protected warnStreamDisabled(): void;
47
48
  init(): Promise<void>;
48
49
  private replaceCacheFromSnapshot;
49
50
  private fetchFlags;
@@ -55,6 +56,8 @@ declare class CanaryGateBase {
55
56
  private scheduleReconnect;
56
57
  private consumeStream;
57
58
  private connectStream;
59
+ private startPolling;
60
+ private stopPolling;
58
61
  getFlag(key: string, context?: FlagEvaluationContext): FlagData | undefined;
59
62
  getFlags(context?: FlagEvaluationContext): FlagData[];
60
63
  isStale(): boolean;
@@ -16,7 +16,7 @@ type FlagEvaluationContext = {
16
16
  type CanaryGateOptions = {
17
17
  baseUrl?: string;
18
18
  environment?: string;
19
- stream?: boolean;
19
+ pollIntervalMs?: number;
20
20
  reconnectDelay?: number;
21
21
  maxReconnectDelay?: number;
22
22
  heartbeatTimeoutMs?: number;
@@ -31,19 +31,20 @@ declare class CanaryGateBase {
31
31
  private readonly reconnectDelay;
32
32
  private readonly maxReconnectDelay;
33
33
  private readonly heartbeatTimeoutMs;
34
+ private readonly pollIntervalMs;
34
35
  private cache;
35
36
  private cacheVersions;
36
37
  private readonly anonId;
37
38
  private streamAbortController;
38
39
  private reconnectTimeout;
39
40
  private heartbeatTimeout;
41
+ private pollTimeout;
40
42
  private streamRetryDelay;
41
43
  private reconnectAttempts;
42
44
  private stale;
43
45
  private lastSyncAt;
44
46
  private destroyed;
45
47
  constructor(apiKey: string, options: CanaryGateOptions | undefined, streamEnabled: boolean, anonIdFactory: () => string);
46
- protected warnStreamDisabled(): void;
47
48
  init(): Promise<void>;
48
49
  private replaceCacheFromSnapshot;
49
50
  private fetchFlags;
@@ -55,6 +56,8 @@ declare class CanaryGateBase {
55
56
  private scheduleReconnect;
56
57
  private consumeStream;
57
58
  private connectStream;
59
+ private startPolling;
60
+ private stopPolling;
58
61
  getFlag(key: string, context?: FlagEvaluationContext): FlagData | undefined;
59
62
  getFlags(context?: FlagEvaluationContext): FlagData[];
60
63
  isStale(): boolean;
@@ -42,6 +42,7 @@ 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;
45
46
  function isAbortError(error) {
46
47
  return error instanceof DOMException && error.name === "AbortError" || error instanceof Error && error.name === "AbortError";
47
48
  }
@@ -59,6 +60,7 @@ var CanaryGateBase = class {
59
60
  this.streamAbortController = null;
60
61
  this.reconnectTimeout = null;
61
62
  this.heartbeatTimeout = null;
63
+ this.pollTimeout = null;
62
64
  this.reconnectAttempts = 0;
63
65
  this.stale = false;
64
66
  this.lastSyncAt = null;
@@ -74,17 +76,17 @@ var CanaryGateBase = class {
74
76
  this.reconnectDelay
75
77
  );
76
78
  this.heartbeatTimeoutMs = options.heartbeatTimeoutMs ?? DEFAULT_HEARTBEAT_TIMEOUT_MS;
79
+ this.pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
77
80
  this.streamRetryDelay = this.reconnectDelay;
78
81
  this.anonId = anonIdFactory();
79
82
  }
80
- warnStreamDisabled() {
81
- console.warn(
82
- "[canarygate] Real-time streams (SSE) are disabled in browser environments to protect network architecture."
83
- );
84
- }
85
83
  async init() {
86
84
  await this.fetchFlags();
87
- if (this.streamEnabled) this.connectStream();
85
+ if (this.streamEnabled) {
86
+ this.connectStream();
87
+ } else {
88
+ this.startPolling();
89
+ }
88
90
  }
89
91
  replaceCacheFromSnapshot(flags, requestedAt) {
90
92
  const nextCache = /* @__PURE__ */ new Map();
@@ -270,6 +272,20 @@ var CanaryGateBase = class {
270
272
  this.streamAbortController = abortController;
271
273
  void this.consumeStream(abortController);
272
274
  }
275
+ startPolling() {
276
+ if (this.destroyed || this.streamEnabled || this.pollIntervalMs <= 0 || this.pollTimeout) {
277
+ return;
278
+ }
279
+ this.pollTimeout = setInterval(() => {
280
+ void this.fetchFlags();
281
+ }, this.pollIntervalMs);
282
+ }
283
+ stopPolling() {
284
+ if (this.pollTimeout) {
285
+ clearInterval(this.pollTimeout);
286
+ this.pollTimeout = null;
287
+ }
288
+ }
273
289
  getFlag(key, context) {
274
290
  const raw = this.cache.get(key);
275
291
  if (!raw) return void 0;
@@ -298,6 +314,7 @@ var CanaryGateBase = class {
298
314
  }
299
315
  disconnect() {
300
316
  this.destroyed = true;
317
+ this.stopPolling();
301
318
  if (this.reconnectTimeout) {
302
319
  clearTimeout(this.reconnectTimeout);
303
320
  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-D069Q1Z_.mjs';
1
+ import { C as CanaryGateBase, a as CanaryGateOptions } from './canary-gate-base-DzuUUPOC.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-D069Q1Z_.js';
1
+ import { C as CanaryGateBase, a as CanaryGateOptions } from './canary-gate-base-DzuUUPOC.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,7 @@ 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;
71
72
  function isAbortError(error) {
72
73
  return error instanceof DOMException && error.name === "AbortError" || error instanceof Error && error.name === "AbortError";
73
74
  }
@@ -85,6 +86,7 @@ var CanaryGateBase = class {
85
86
  this.streamAbortController = null;
86
87
  this.reconnectTimeout = null;
87
88
  this.heartbeatTimeout = null;
89
+ this.pollTimeout = null;
88
90
  this.reconnectAttempts = 0;
89
91
  this.stale = false;
90
92
  this.lastSyncAt = null;
@@ -100,17 +102,17 @@ var CanaryGateBase = class {
100
102
  this.reconnectDelay
101
103
  );
102
104
  this.heartbeatTimeoutMs = options.heartbeatTimeoutMs ?? DEFAULT_HEARTBEAT_TIMEOUT_MS;
105
+ this.pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
103
106
  this.streamRetryDelay = this.reconnectDelay;
104
107
  this.anonId = anonIdFactory();
105
108
  }
106
- warnStreamDisabled() {
107
- console.warn(
108
- "[canarygate] Real-time streams (SSE) are disabled in browser environments to protect network architecture."
109
- );
110
- }
111
109
  async init() {
112
110
  await this.fetchFlags();
113
- if (this.streamEnabled) this.connectStream();
111
+ if (this.streamEnabled) {
112
+ this.connectStream();
113
+ } else {
114
+ this.startPolling();
115
+ }
114
116
  }
115
117
  replaceCacheFromSnapshot(flags, requestedAt) {
116
118
  const nextCache = /* @__PURE__ */ new Map();
@@ -296,6 +298,20 @@ var CanaryGateBase = class {
296
298
  this.streamAbortController = abortController;
297
299
  void this.consumeStream(abortController);
298
300
  }
301
+ startPolling() {
302
+ if (this.destroyed || this.streamEnabled || this.pollIntervalMs <= 0 || this.pollTimeout) {
303
+ return;
304
+ }
305
+ this.pollTimeout = setInterval(() => {
306
+ void this.fetchFlags();
307
+ }, this.pollIntervalMs);
308
+ }
309
+ stopPolling() {
310
+ if (this.pollTimeout) {
311
+ clearInterval(this.pollTimeout);
312
+ this.pollTimeout = null;
313
+ }
314
+ }
299
315
  getFlag(key, context) {
300
316
  const raw = this.cache.get(key);
301
317
  if (!raw) return void 0;
@@ -324,6 +340,7 @@ var CanaryGateBase = class {
324
340
  }
325
341
  disconnect() {
326
342
  this.destroyed = true;
343
+ this.stopPolling();
327
344
  if (this.reconnectTimeout) {
328
345
  clearTimeout(this.reconnectTimeout);
329
346
  this.reconnectTimeout = null;
@@ -348,10 +365,12 @@ function getOrCreateAnonId() {
348
365
  }
349
366
  var CanaryGate = class extends CanaryGateBase {
350
367
  constructor(apiKey, options = {}) {
351
- super(apiKey, options, false, getOrCreateAnonId);
352
- if (options.stream === true) {
353
- this.warnStreamDisabled();
368
+ if (typeof window === "undefined") {
369
+ throw new Error(
370
+ '@canarygate/sdk/client is browser-only. On server runtimes (Node.js, Deno, Bun, Edge) import from "@canarygate/sdk/server" instead.'
371
+ );
354
372
  }
373
+ super(apiKey, options, false, getOrCreateAnonId);
355
374
  }
356
375
  };
357
376
  // 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-7KAJ7OJO.mjs";
3
+ } from "./chunk-ECO47H4R.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
- super(apiKey, options, false, getOrCreateAnonId);
20
- if (options.stream === true) {
21
- this.warnStreamDisabled();
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-D069Q1Z_.mjs';
1
+ import { C as CanaryGateBase, a as CanaryGateOptions } from './canary-gate-base-DzuUUPOC.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-D069Q1Z_.js';
1
+ import { C as CanaryGateBase, a as CanaryGateOptions } from './canary-gate-base-DzuUUPOC.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,7 @@ 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;
71
72
  function isAbortError(error) {
72
73
  return error instanceof DOMException && error.name === "AbortError" || error instanceof Error && error.name === "AbortError";
73
74
  }
@@ -85,6 +86,7 @@ var CanaryGateBase = class {
85
86
  this.streamAbortController = null;
86
87
  this.reconnectTimeout = null;
87
88
  this.heartbeatTimeout = null;
89
+ this.pollTimeout = null;
88
90
  this.reconnectAttempts = 0;
89
91
  this.stale = false;
90
92
  this.lastSyncAt = null;
@@ -100,17 +102,17 @@ var CanaryGateBase = class {
100
102
  this.reconnectDelay
101
103
  );
102
104
  this.heartbeatTimeoutMs = options.heartbeatTimeoutMs ?? DEFAULT_HEARTBEAT_TIMEOUT_MS;
105
+ this.pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
103
106
  this.streamRetryDelay = this.reconnectDelay;
104
107
  this.anonId = anonIdFactory();
105
108
  }
106
- warnStreamDisabled() {
107
- console.warn(
108
- "[canarygate] Real-time streams (SSE) are disabled in browser environments to protect network architecture."
109
- );
110
- }
111
109
  async init() {
112
110
  await this.fetchFlags();
113
- if (this.streamEnabled) this.connectStream();
111
+ if (this.streamEnabled) {
112
+ this.connectStream();
113
+ } else {
114
+ this.startPolling();
115
+ }
114
116
  }
115
117
  replaceCacheFromSnapshot(flags, requestedAt) {
116
118
  const nextCache = /* @__PURE__ */ new Map();
@@ -296,6 +298,20 @@ var CanaryGateBase = class {
296
298
  this.streamAbortController = abortController;
297
299
  void this.consumeStream(abortController);
298
300
  }
301
+ startPolling() {
302
+ if (this.destroyed || this.streamEnabled || this.pollIntervalMs <= 0 || this.pollTimeout) {
303
+ return;
304
+ }
305
+ this.pollTimeout = setInterval(() => {
306
+ void this.fetchFlags();
307
+ }, this.pollIntervalMs);
308
+ }
309
+ stopPolling() {
310
+ if (this.pollTimeout) {
311
+ clearInterval(this.pollTimeout);
312
+ this.pollTimeout = null;
313
+ }
314
+ }
299
315
  getFlag(key, context) {
300
316
  const raw = this.cache.get(key);
301
317
  if (!raw) return void 0;
@@ -324,6 +340,7 @@ var CanaryGateBase = class {
324
340
  }
325
341
  disconnect() {
326
342
  this.destroyed = true;
343
+ this.stopPolling();
327
344
  if (this.reconnectTimeout) {
328
345
  clearTimeout(this.reconnectTimeout);
329
346
  this.reconnectTimeout = null;
@@ -337,7 +354,12 @@ var CanaryGateBase = class {
337
354
  // src/server.ts
338
355
  var CanaryGate = class extends CanaryGateBase {
339
356
  constructor(apiKey, options = {}) {
340
- super(apiKey, options, options.stream ?? false, () => crypto.randomUUID());
357
+ if (typeof window !== "undefined") {
358
+ throw new Error(
359
+ '@canarygate/sdk/server is server-only. In browsers import from "@canarygate/sdk/client" instead.'
360
+ );
361
+ }
362
+ super(apiKey, options, true, () => crypto.randomUUID());
341
363
  }
342
364
  };
343
365
  // 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-7KAJ7OJO.mjs";
3
+ } from "./chunk-ECO47H4R.mjs";
4
4
 
5
5
  // src/server.ts
6
6
  var CanaryGate = class extends CanaryGateBase {
7
7
  constructor(apiKey, options = {}) {
8
- super(apiKey, options, options.stream ?? false, () => crypto.randomUUID());
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.0",
3
+ "version": "0.1.1",
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
- "dist",
20
- "README.md",
21
- "LICENSE"
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
+ }