@canarygate/sdk 0.1.2 → 0.1.3

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
@@ -65,14 +65,15 @@ Rollout evaluation hashes `userId` deterministically — the same user always ge
65
65
 
66
66
  ## Options
67
67
 
68
+ The API base URL is resolved automatically from the `CANARYGATE_BASE_URL` environment variable (in browsers, `NEXT_PUBLIC_CANARYGATE_BASE_URL`), falling back to `http://localhost:3001`.
69
+
68
70
  | Option | Type | Default | Description |
69
71
  | -------------------- | --------- | ----------------------- | ------------------------------------------------ |
70
- | `baseUrl` | `string` | `http://localhost:3001` | CanaryGate API base URL |
71
72
  | `environment` | `string` | — | Environment to evaluate flags against |
72
- | `pollIntervalMs` | `number` | `30000` | Browser polling interval in ms (clamped to a 3000ms minimum). Set `0` to disable |
73
- | `reconnectDelay` | `number` | `5000` | Initial SSE reconnect delay (ms) |
74
- | `maxReconnectDelay` | `number` | `30000` | Reconnect delay cap, exponential backoff (ms) |
75
- | `heartbeatTimeoutMs` | `number` | `65000` | Silence window before treating the stream as dead |
73
+ | `pollIntervalMs` | `number` | `30000` | Browser polling interval in ms (clamped to a 3000ms minimum). Set `0` to disable. Throws if passed to the server entry |
74
+ | `reconnectDelay` | `number` | `5000` | Initial SSE reconnect delay (ms). Server-only; throws on the browser entry |
75
+ | `maxReconnectDelay` | `number` | `30000` | Reconnect delay cap, exponential backoff (ms). Server-only |
76
+ | `heartbeatTimeoutMs` | `number` | `65000` | Silence window before treating the stream as dead. Server-only |
76
77
 
77
78
  ## Methods
78
79
 
@@ -0,0 +1,71 @@
1
+ type BooleanFlagData = {
2
+ key: string;
3
+ type: 'boolean';
4
+ enabled: boolean;
5
+ };
6
+ type RolloutFlagData = {
7
+ key: string;
8
+ type: 'rollout';
9
+ enabled: boolean;
10
+ percent: number;
11
+ };
12
+ type FlagData = BooleanFlagData | RolloutFlagData;
13
+ type FlagEvaluationContext = {
14
+ userId?: string;
15
+ };
16
+ type CanaryGateOptions = {
17
+ environment?: string;
18
+ /** Browser polling interval in ms. Default: 30000; minimum enforced: 3000. Set 0 to disable. Throws if set on the server entry. */
19
+ pollIntervalMs?: number;
20
+ /** Initial stream reconnect delay in ms. Default: 5000. Server-only. */
21
+ reconnectDelay?: number;
22
+ /** Maximum stream reconnect delay in ms. Default: 30000. Server-only. */
23
+ maxReconnectDelay?: number;
24
+ /** Time in ms without a server heartbeat before reconnecting the stream. Default: 65000. Server-only. */
25
+ heartbeatTimeoutMs?: number;
26
+ };
27
+
28
+ declare class CanaryGateBase {
29
+ protected readonly apiKey: string;
30
+ protected readonly streamEnabled: boolean;
31
+ protected readonly anonIdFactory: () => string;
32
+ private readonly baseUrl;
33
+ private readonly environment;
34
+ private readonly reconnectDelay;
35
+ private readonly maxReconnectDelay;
36
+ private readonly heartbeatTimeoutMs;
37
+ private readonly pollIntervalMs;
38
+ private cache;
39
+ private cacheVersions;
40
+ private readonly anonId;
41
+ private streamAbortController;
42
+ private reconnectTimeout;
43
+ private heartbeatTimeout;
44
+ private pollTimeout;
45
+ private streamRetryDelay;
46
+ private reconnectAttempts;
47
+ private stale;
48
+ private lastSyncAt;
49
+ private destroyed;
50
+ constructor(apiKey: string, options: CanaryGateOptions | undefined, streamEnabled: boolean, anonIdFactory: () => string);
51
+ init(): Promise<void>;
52
+ private replaceCacheFromSnapshot;
53
+ private fetchFlags;
54
+ private applyFlagUpdate;
55
+ private applyFlagDeletion;
56
+ private handleStreamMessage;
57
+ private clearHeartbeatTimeout;
58
+ private bumpHeartbeatTimeout;
59
+ private scheduleReconnect;
60
+ private consumeStream;
61
+ private connectStream;
62
+ private startPolling;
63
+ private stopPolling;
64
+ getFlag(key: string, context?: FlagEvaluationContext): FlagData | undefined;
65
+ getFlags(context?: FlagEvaluationContext): FlagData[];
66
+ isStale(): boolean;
67
+ getLastSyncAt(): string | null;
68
+ disconnect(): void;
69
+ }
70
+
71
+ export { CanaryGateBase as C, type CanaryGateOptions as a };
@@ -0,0 +1,71 @@
1
+ type BooleanFlagData = {
2
+ key: string;
3
+ type: 'boolean';
4
+ enabled: boolean;
5
+ };
6
+ type RolloutFlagData = {
7
+ key: string;
8
+ type: 'rollout';
9
+ enabled: boolean;
10
+ percent: number;
11
+ };
12
+ type FlagData = BooleanFlagData | RolloutFlagData;
13
+ type FlagEvaluationContext = {
14
+ userId?: string;
15
+ };
16
+ type CanaryGateOptions = {
17
+ environment?: string;
18
+ /** Browser polling interval in ms. Default: 30000; minimum enforced: 3000. Set 0 to disable. Throws if set on the server entry. */
19
+ pollIntervalMs?: number;
20
+ /** Initial stream reconnect delay in ms. Default: 5000. Server-only. */
21
+ reconnectDelay?: number;
22
+ /** Maximum stream reconnect delay in ms. Default: 30000. Server-only. */
23
+ maxReconnectDelay?: number;
24
+ /** Time in ms without a server heartbeat before reconnecting the stream. Default: 65000. Server-only. */
25
+ heartbeatTimeoutMs?: number;
26
+ };
27
+
28
+ declare class CanaryGateBase {
29
+ protected readonly apiKey: string;
30
+ protected readonly streamEnabled: boolean;
31
+ protected readonly anonIdFactory: () => string;
32
+ private readonly baseUrl;
33
+ private readonly environment;
34
+ private readonly reconnectDelay;
35
+ private readonly maxReconnectDelay;
36
+ private readonly heartbeatTimeoutMs;
37
+ private readonly pollIntervalMs;
38
+ private cache;
39
+ private cacheVersions;
40
+ private readonly anonId;
41
+ private streamAbortController;
42
+ private reconnectTimeout;
43
+ private heartbeatTimeout;
44
+ private pollTimeout;
45
+ private streamRetryDelay;
46
+ private reconnectAttempts;
47
+ private stale;
48
+ private lastSyncAt;
49
+ private destroyed;
50
+ constructor(apiKey: string, options: CanaryGateOptions | undefined, streamEnabled: boolean, anonIdFactory: () => string);
51
+ init(): Promise<void>;
52
+ private replaceCacheFromSnapshot;
53
+ private fetchFlags;
54
+ private applyFlagUpdate;
55
+ private applyFlagDeletion;
56
+ private handleStreamMessage;
57
+ private clearHeartbeatTimeout;
58
+ private bumpHeartbeatTimeout;
59
+ private scheduleReconnect;
60
+ private consumeStream;
61
+ private connectStream;
62
+ private startPolling;
63
+ private stopPolling;
64
+ getFlag(key: string, context?: FlagEvaluationContext): FlagData | undefined;
65
+ getFlags(context?: FlagEvaluationContext): FlagData[];
66
+ isStale(): boolean;
67
+ getLastSyncAt(): string | null;
68
+ disconnect(): void;
69
+ }
70
+
71
+ export { CanaryGateBase as C, type CanaryGateOptions as a };
@@ -0,0 +1,341 @@
1
+ // src/hash.ts
2
+ function hashString(input) {
3
+ let hash = 5381;
4
+ for (let i = 0; i < input.length; i++) {
5
+ hash = (hash << 5) + hash ^ input.charCodeAt(i);
6
+ hash = hash >>> 0;
7
+ }
8
+ return hash % 100;
9
+ }
10
+
11
+ // src/sse.ts
12
+ function parseSseEventBlock(block) {
13
+ let event = "message";
14
+ const dataLines = [];
15
+ let retryMs;
16
+ for (const line of block.split(/\r?\n/)) {
17
+ if (!line || line.startsWith(":")) continue;
18
+ const separatorIndex = line.indexOf(":");
19
+ const field = separatorIndex === -1 ? line : line.slice(0, separatorIndex);
20
+ const value = separatorIndex === -1 ? "" : line.slice(separatorIndex + 1).trimStart();
21
+ if (field === "event") {
22
+ event = value || "message";
23
+ continue;
24
+ }
25
+ if (field === "data") {
26
+ dataLines.push(value);
27
+ continue;
28
+ }
29
+ if (field === "retry") {
30
+ const parsedRetryMs = Number.parseInt(value, 10);
31
+ if (Number.isFinite(parsedRetryMs) && parsedRetryMs > 0) {
32
+ retryMs = parsedRetryMs;
33
+ }
34
+ }
35
+ }
36
+ if (dataLines.length === 0 && retryMs === void 0) {
37
+ return null;
38
+ }
39
+ return { event, data: dataLines.join("\n"), retryMs };
40
+ }
41
+
42
+ // src/canary-gate-base.ts
43
+ var DEFAULT_MAX_RECONNECT_DELAY_MS = 3e4;
44
+ var DEFAULT_HEARTBEAT_TIMEOUT_MS = 65e3;
45
+ var DEFAULT_POLL_INTERVAL_MS = 3e4;
46
+ var MIN_POLL_INTERVAL_MS = 3e3;
47
+ var DEFAULT_BASE_URL = "http://localhost:3001";
48
+ function resolveBaseUrl() {
49
+ if (typeof process !== "undefined" && typeof process.env === "object" && process.env !== null) {
50
+ return (process.env.CANARYGATE_BASE_URL ?? process.env.NEXT_PUBLIC_CANARYGATE_BASE_URL ?? DEFAULT_BASE_URL).replace(/\/$/, "");
51
+ }
52
+ return DEFAULT_BASE_URL;
53
+ }
54
+ function isAbortError(error) {
55
+ return error instanceof DOMException && error.name === "AbortError" || error instanceof Error && error.name === "AbortError";
56
+ }
57
+ function parseTimestamp(value) {
58
+ const parsed = Date.parse(value);
59
+ return Number.isNaN(parsed) ? 0 : parsed;
60
+ }
61
+ var CanaryGateBase = class {
62
+ constructor(apiKey, options = {}, streamEnabled, anonIdFactory) {
63
+ this.apiKey = apiKey;
64
+ this.streamEnabled = streamEnabled;
65
+ this.anonIdFactory = anonIdFactory;
66
+ this.cache = /* @__PURE__ */ new Map();
67
+ this.cacheVersions = /* @__PURE__ */ new Map();
68
+ this.streamAbortController = null;
69
+ this.reconnectTimeout = null;
70
+ this.heartbeatTimeout = null;
71
+ this.pollTimeout = null;
72
+ this.reconnectAttempts = 0;
73
+ this.stale = false;
74
+ this.lastSyncAt = null;
75
+ this.destroyed = false;
76
+ this.baseUrl = resolveBaseUrl();
77
+ this.environment = options.environment;
78
+ this.reconnectDelay = options.reconnectDelay ?? 5e3;
79
+ this.maxReconnectDelay = Math.max(
80
+ options.maxReconnectDelay ?? DEFAULT_MAX_RECONNECT_DELAY_MS,
81
+ this.reconnectDelay
82
+ );
83
+ this.heartbeatTimeoutMs = options.heartbeatTimeoutMs ?? DEFAULT_HEARTBEAT_TIMEOUT_MS;
84
+ this.pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
85
+ if (options.pollIntervalMs !== void 0 && options.pollIntervalMs > 0 && options.pollIntervalMs < MIN_POLL_INTERVAL_MS) {
86
+ console.warn(
87
+ `[canarygate] pollIntervalMs must be at least ${MIN_POLL_INTERVAL_MS}ms; clamped to ${MIN_POLL_INTERVAL_MS}.`
88
+ );
89
+ this.pollIntervalMs = MIN_POLL_INTERVAL_MS;
90
+ }
91
+ this.streamRetryDelay = this.reconnectDelay;
92
+ this.anonId = anonIdFactory();
93
+ }
94
+ async init() {
95
+ await this.fetchFlags();
96
+ if (this.streamEnabled) {
97
+ this.connectStream();
98
+ } else {
99
+ this.startPolling();
100
+ }
101
+ }
102
+ replaceCacheFromSnapshot(flags, requestedAt) {
103
+ const nextCache = /* @__PURE__ */ new Map();
104
+ const nextVersions = /* @__PURE__ */ new Map();
105
+ for (const flag of flags) {
106
+ const nextVersion = parseTimestamp(flag.updatedAt);
107
+ const currentVersion = this.cacheVersions.get(flag.key) ?? -1;
108
+ if (currentVersion > nextVersion && currentVersion > requestedAt) {
109
+ const currentFlag = this.cache.get(flag.key);
110
+ if (currentFlag) nextCache.set(flag.key, currentFlag);
111
+ nextVersions.set(flag.key, currentVersion);
112
+ continue;
113
+ }
114
+ nextCache.set(flag.key, flag);
115
+ nextVersions.set(flag.key, nextVersion);
116
+ }
117
+ for (const [key, currentVersion] of this.cacheVersions) {
118
+ if (nextVersions.has(key) || currentVersion <= requestedAt) {
119
+ continue;
120
+ }
121
+ const currentFlag = this.cache.get(key);
122
+ if (currentFlag) nextCache.set(key, currentFlag);
123
+ nextVersions.set(key, currentVersion);
124
+ }
125
+ this.cache = nextCache;
126
+ this.cacheVersions = nextVersions;
127
+ this.stale = false;
128
+ this.lastSyncAt = (/* @__PURE__ */ new Date()).toISOString();
129
+ }
130
+ async fetchFlags() {
131
+ const requestedAt = Date.now();
132
+ try {
133
+ const headers = { "X-Api-Key": this.apiKey };
134
+ if (this.environment) headers["X-Environment"] = this.environment;
135
+ const res = await fetch(`${this.baseUrl}/sdk/flags`, { headers });
136
+ if (!res.ok) {
137
+ console.error(
138
+ `[canarygate] Failed to fetch flags: ${res.status} ${res.statusText}`
139
+ );
140
+ this.stale = true;
141
+ return false;
142
+ }
143
+ const body = await res.json();
144
+ this.replaceCacheFromSnapshot(body.flags, requestedAt);
145
+ return true;
146
+ } catch (err) {
147
+ console.error("[canarygate] Error fetching flags:", err);
148
+ this.stale = true;
149
+ return false;
150
+ }
151
+ }
152
+ applyFlagUpdate(raw) {
153
+ const nextVersion = parseTimestamp(raw.updatedAt);
154
+ const currentVersion = this.cacheVersions.get(raw.key) ?? -1;
155
+ if (nextVersion < currentVersion) return;
156
+ this.cacheVersions.set(raw.key, nextVersion);
157
+ this.cache.set(raw.key, raw);
158
+ }
159
+ applyFlagDeletion(payload) {
160
+ const nextVersion = parseTimestamp(payload.deletedAt);
161
+ const currentVersion = this.cacheVersions.get(payload.key) ?? -1;
162
+ if (nextVersion < currentVersion) return;
163
+ this.cacheVersions.set(payload.key, nextVersion);
164
+ this.cache.delete(payload.key);
165
+ }
166
+ handleStreamMessage(event, data) {
167
+ if (event === "connected" || event === "connection-closing") return;
168
+ if (!data) return;
169
+ try {
170
+ if (event === "flag-deleted") {
171
+ const payload = JSON.parse(data);
172
+ this.applyFlagDeletion(payload);
173
+ return;
174
+ }
175
+ if (event === "flag-updated" || event === "flag-created") {
176
+ this.applyFlagUpdate(JSON.parse(data));
177
+ }
178
+ } catch (err) {
179
+ console.error(`[canarygate] Failed to parse ${event} event:`, err);
180
+ }
181
+ }
182
+ clearHeartbeatTimeout() {
183
+ if (this.heartbeatTimeout) {
184
+ clearTimeout(this.heartbeatTimeout);
185
+ this.heartbeatTimeout = null;
186
+ }
187
+ }
188
+ bumpHeartbeatTimeout(abortController) {
189
+ this.clearHeartbeatTimeout();
190
+ this.heartbeatTimeout = setTimeout(() => {
191
+ if (this.streamAbortController === abortController && !this.destroyed) {
192
+ abortController.abort();
193
+ }
194
+ }, this.heartbeatTimeoutMs);
195
+ }
196
+ scheduleReconnect() {
197
+ if (this.destroyed || this.reconnectTimeout) return;
198
+ const nextDelay = Math.min(
199
+ this.streamRetryDelay * 2 ** this.reconnectAttempts,
200
+ this.maxReconnectDelay
201
+ );
202
+ this.reconnectAttempts += 1;
203
+ this.reconnectTimeout = setTimeout(() => {
204
+ this.reconnectTimeout = null;
205
+ this.connectStream();
206
+ }, nextDelay);
207
+ }
208
+ async consumeStream(abortController) {
209
+ try {
210
+ const headers = { "X-Api-Key": this.apiKey };
211
+ if (this.environment) headers["X-Environment"] = this.environment;
212
+ const response = await fetch(`${this.baseUrl}/sdk/stream`, {
213
+ headers,
214
+ signal: abortController.signal,
215
+ cache: "no-store"
216
+ });
217
+ if (!response.ok) {
218
+ console.error(
219
+ `[canarygate] Failed to connect stream: ${response.status} ${response.statusText}`
220
+ );
221
+ this.stale = true;
222
+ return;
223
+ }
224
+ if (!response.body) {
225
+ console.error(
226
+ "[canarygate] Stream body is not available in this runtime"
227
+ );
228
+ this.stale = true;
229
+ return;
230
+ }
231
+ this.reconnectAttempts = 0;
232
+ this.bumpHeartbeatTimeout(abortController);
233
+ if (this.stale) {
234
+ await this.fetchFlags();
235
+ }
236
+ const reader = response.body.getReader();
237
+ const decoder = new TextDecoder();
238
+ let buffer = "";
239
+ while (!abortController.signal.aborted) {
240
+ const { done, value } = await reader.read();
241
+ if (done) break;
242
+ this.bumpHeartbeatTimeout(abortController);
243
+ buffer += decoder.decode(value, { stream: true });
244
+ const blocks = buffer.split(/\r?\n\r?\n/);
245
+ buffer = blocks.pop() ?? "";
246
+ for (const block of blocks) {
247
+ const parsedEvent = parseSseEventBlock(block);
248
+ if (!parsedEvent) continue;
249
+ if (parsedEvent.retryMs !== void 0) {
250
+ this.streamRetryDelay = parsedEvent.retryMs;
251
+ }
252
+ this.handleStreamMessage(parsedEvent.event, parsedEvent.data);
253
+ }
254
+ }
255
+ buffer += decoder.decode();
256
+ if (buffer.trim()) {
257
+ const parsedEvent = parseSseEventBlock(buffer);
258
+ if (parsedEvent) {
259
+ if (parsedEvent.retryMs !== void 0) {
260
+ this.streamRetryDelay = parsedEvent.retryMs;
261
+ }
262
+ this.handleStreamMessage(parsedEvent.event, parsedEvent.data);
263
+ }
264
+ }
265
+ } catch (err) {
266
+ if (!isAbortError(err)) {
267
+ console.error("[canarygate] Stream connection failed:", err);
268
+ }
269
+ } finally {
270
+ this.clearHeartbeatTimeout();
271
+ if (this.streamAbortController === abortController) {
272
+ this.streamAbortController = null;
273
+ }
274
+ if (!this.destroyed) {
275
+ this.stale = true;
276
+ this.scheduleReconnect();
277
+ }
278
+ }
279
+ }
280
+ connectStream() {
281
+ if (this.destroyed || this.streamAbortController) return;
282
+ const abortController = new AbortController();
283
+ this.streamAbortController = abortController;
284
+ void this.consumeStream(abortController);
285
+ }
286
+ startPolling() {
287
+ if (this.destroyed || this.streamEnabled || this.pollIntervalMs <= 0 || this.pollTimeout) {
288
+ return;
289
+ }
290
+ this.pollTimeout = setInterval(() => {
291
+ void this.fetchFlags();
292
+ }, this.pollIntervalMs);
293
+ }
294
+ stopPolling() {
295
+ if (this.pollTimeout) {
296
+ clearInterval(this.pollTimeout);
297
+ this.pollTimeout = null;
298
+ }
299
+ }
300
+ getFlag(key, context) {
301
+ const raw = this.cache.get(key);
302
+ if (!raw) return void 0;
303
+ const evaluationId = context?.userId || this.anonId;
304
+ if (raw.type === "rollout") {
305
+ const inRollout = raw.enabled && hashString(`${raw.key}:${evaluationId}`) < raw.rolloutPercent;
306
+ return {
307
+ key: raw.key,
308
+ type: "rollout",
309
+ enabled: inRollout,
310
+ percent: raw.rolloutPercent
311
+ };
312
+ }
313
+ return { key: raw.key, type: "boolean", enabled: raw.enabled };
314
+ }
315
+ getFlags(context) {
316
+ return Array.from(this.cache.keys()).map(
317
+ (key) => this.getFlag(key, context)
318
+ );
319
+ }
320
+ isStale() {
321
+ return this.stale;
322
+ }
323
+ getLastSyncAt() {
324
+ return this.lastSyncAt;
325
+ }
326
+ disconnect() {
327
+ this.destroyed = true;
328
+ this.stopPolling();
329
+ if (this.reconnectTimeout) {
330
+ clearTimeout(this.reconnectTimeout);
331
+ this.reconnectTimeout = null;
332
+ }
333
+ this.clearHeartbeatTimeout();
334
+ this.streamAbortController?.abort();
335
+ this.streamAbortController = null;
336
+ }
337
+ };
338
+
339
+ export {
340
+ CanaryGateBase
341
+ };
package/dist/client.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { C as CanaryGateBase, a as CanaryGateOptions } from './canary-gate-base-iXS3mVyK.mjs';
1
+ import { C as CanaryGateBase, a as CanaryGateOptions } from './canary-gate-base-jk1htbug.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-iXS3mVyK.js';
1
+ import { C as CanaryGateBase, a as CanaryGateOptions } from './canary-gate-base-jk1htbug.js';
2
2
 
3
3
  declare class CanaryGate extends CanaryGateBase {
4
4
  constructor(apiKey: string, options?: CanaryGateOptions);
package/dist/client.js CHANGED
@@ -70,6 +70,13 @@ var DEFAULT_MAX_RECONNECT_DELAY_MS = 3e4;
70
70
  var DEFAULT_HEARTBEAT_TIMEOUT_MS = 65e3;
71
71
  var DEFAULT_POLL_INTERVAL_MS = 3e4;
72
72
  var MIN_POLL_INTERVAL_MS = 3e3;
73
+ var DEFAULT_BASE_URL = "http://localhost:3001";
74
+ function resolveBaseUrl() {
75
+ if (typeof process !== "undefined" && typeof process.env === "object" && process.env !== null) {
76
+ return (process.env.CANARYGATE_BASE_URL ?? process.env.NEXT_PUBLIC_CANARYGATE_BASE_URL ?? DEFAULT_BASE_URL).replace(/\/$/, "");
77
+ }
78
+ return DEFAULT_BASE_URL;
79
+ }
73
80
  function isAbortError(error) {
74
81
  return error instanceof DOMException && error.name === "AbortError" || error instanceof Error && error.name === "AbortError";
75
82
  }
@@ -92,10 +99,7 @@ var CanaryGateBase = class {
92
99
  this.stale = false;
93
100
  this.lastSyncAt = null;
94
101
  this.destroyed = false;
95
- this.baseUrl = (options.baseUrl ?? "http://localhost:3001").replace(
96
- /\/$/,
97
- ""
98
- );
102
+ this.baseUrl = resolveBaseUrl();
99
103
  this.environment = options.environment;
100
104
  this.reconnectDelay = options.reconnectDelay ?? 5e3;
101
105
  this.maxReconnectDelay = Math.max(
@@ -360,6 +364,11 @@ var CanaryGateBase = class {
360
364
 
361
365
  // src/client.ts
362
366
  var ANON_ID_KEY = "__cg_anon_id__";
367
+ var SERVER_ONLY_OPTIONS = [
368
+ "reconnectDelay",
369
+ "maxReconnectDelay",
370
+ "heartbeatTimeoutMs"
371
+ ];
363
372
  function getOrCreateAnonId() {
364
373
  if (typeof localStorage !== "undefined") {
365
374
  const stored = localStorage.getItem(ANON_ID_KEY);
@@ -377,6 +386,13 @@ var CanaryGate = class extends CanaryGateBase {
377
386
  '@canarygate/sdk/client is browser-only. On server runtimes (Node.js, Deno, Bun, Edge) import from "@canarygate/sdk/server" instead.'
378
387
  );
379
388
  }
389
+ for (const key of SERVER_ONLY_OPTIONS) {
390
+ if (options[key] !== void 0) {
391
+ throw new Error(
392
+ `"${key}" is a server-only option and has no effect on @canarygate/sdk/client. Remove it, or use @canarygate/sdk/server.`
393
+ );
394
+ }
395
+ }
380
396
  super(apiKey, options, false, getOrCreateAnonId);
381
397
  }
382
398
  };
package/dist/client.mjs CHANGED
@@ -1,9 +1,14 @@
1
1
  import {
2
2
  CanaryGateBase
3
- } from "./chunk-LMPIWD3Z.mjs";
3
+ } from "./chunk-X2JAB5XL.mjs";
4
4
 
5
5
  // src/client.ts
6
6
  var ANON_ID_KEY = "__cg_anon_id__";
7
+ var SERVER_ONLY_OPTIONS = [
8
+ "reconnectDelay",
9
+ "maxReconnectDelay",
10
+ "heartbeatTimeoutMs"
11
+ ];
7
12
  function getOrCreateAnonId() {
8
13
  if (typeof localStorage !== "undefined") {
9
14
  const stored = localStorage.getItem(ANON_ID_KEY);
@@ -21,6 +26,13 @@ var CanaryGate = class extends CanaryGateBase {
21
26
  '@canarygate/sdk/client is browser-only. On server runtimes (Node.js, Deno, Bun, Edge) import from "@canarygate/sdk/server" instead.'
22
27
  );
23
28
  }
29
+ for (const key of SERVER_ONLY_OPTIONS) {
30
+ if (options[key] !== void 0) {
31
+ throw new Error(
32
+ `"${key}" is a server-only option and has no effect on @canarygate/sdk/client. Remove it, or use @canarygate/sdk/server.`
33
+ );
34
+ }
35
+ }
24
36
  super(apiKey, options, false, getOrCreateAnonId);
25
37
  }
26
38
  };
package/dist/server.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { C as CanaryGateBase, a as CanaryGateOptions } from './canary-gate-base-iXS3mVyK.mjs';
1
+ import { C as CanaryGateBase, a as CanaryGateOptions } from './canary-gate-base-jk1htbug.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-iXS3mVyK.js';
1
+ import { C as CanaryGateBase, a as CanaryGateOptions } from './canary-gate-base-jk1htbug.js';
2
2
 
3
3
  declare class CanaryGate extends CanaryGateBase {
4
4
  constructor(apiKey: string, options?: CanaryGateOptions);
package/dist/server.js CHANGED
@@ -70,6 +70,13 @@ var DEFAULT_MAX_RECONNECT_DELAY_MS = 3e4;
70
70
  var DEFAULT_HEARTBEAT_TIMEOUT_MS = 65e3;
71
71
  var DEFAULT_POLL_INTERVAL_MS = 3e4;
72
72
  var MIN_POLL_INTERVAL_MS = 3e3;
73
+ var DEFAULT_BASE_URL = "http://localhost:3001";
74
+ function resolveBaseUrl() {
75
+ if (typeof process !== "undefined" && typeof process.env === "object" && process.env !== null) {
76
+ return (process.env.CANARYGATE_BASE_URL ?? process.env.NEXT_PUBLIC_CANARYGATE_BASE_URL ?? DEFAULT_BASE_URL).replace(/\/$/, "");
77
+ }
78
+ return DEFAULT_BASE_URL;
79
+ }
73
80
  function isAbortError(error) {
74
81
  return error instanceof DOMException && error.name === "AbortError" || error instanceof Error && error.name === "AbortError";
75
82
  }
@@ -92,10 +99,7 @@ var CanaryGateBase = class {
92
99
  this.stale = false;
93
100
  this.lastSyncAt = null;
94
101
  this.destroyed = false;
95
- this.baseUrl = (options.baseUrl ?? "http://localhost:3001").replace(
96
- /\/$/,
97
- ""
98
- );
102
+ this.baseUrl = resolveBaseUrl();
99
103
  this.environment = options.environment;
100
104
  this.reconnectDelay = options.reconnectDelay ?? 5e3;
101
105
  this.maxReconnectDelay = Math.max(
@@ -366,6 +370,11 @@ var CanaryGate = class extends CanaryGateBase {
366
370
  '@canarygate/sdk/server is server-only. In browsers import from "@canarygate/sdk/client" instead.'
367
371
  );
368
372
  }
373
+ if (options.pollIntervalMs !== void 0) {
374
+ throw new Error(
375
+ '"pollIntervalMs" is a browser-only option and has no effect on @canarygate/sdk/server. Remove it, or use @canarygate/sdk/client.'
376
+ );
377
+ }
369
378
  super(apiKey, options, true, () => crypto.randomUUID());
370
379
  }
371
380
  };
package/dist/server.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  CanaryGateBase
3
- } from "./chunk-LMPIWD3Z.mjs";
3
+ } from "./chunk-X2JAB5XL.mjs";
4
4
 
5
5
  // src/server.ts
6
6
  var CanaryGate = class extends CanaryGateBase {
@@ -10,6 +10,11 @@ var CanaryGate = class extends CanaryGateBase {
10
10
  '@canarygate/sdk/server is server-only. In browsers import from "@canarygate/sdk/client" instead.'
11
11
  );
12
12
  }
13
+ if (options.pollIntervalMs !== void 0) {
14
+ throw new Error(
15
+ '"pollIntervalMs" is a browser-only option and has no effect on @canarygate/sdk/server. Remove it, or use @canarygate/sdk/client.'
16
+ );
17
+ }
13
18
  super(apiKey, options, true, () => crypto.randomUUID());
14
19
  }
15
20
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canarygate/sdk",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "CanaryGate SDK — feature flags with real-time SSE streaming",
5
5
  "exports": {
6
6
  "./client": {