@replanejs/sdk 0.5.11 → 0.6.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
@@ -46,27 +46,27 @@ interface PasswordRequirements {
46
46
  requireSymbol: boolean;
47
47
  }
48
48
 
49
- const configs = await createReplaneClient<Configs>({
49
+ const replane = await createReplaneClient<Configs>({
50
50
  // Each SDK key belongs to one project only
51
51
  sdkKey: process.env.REPLANE_SDK_KEY!,
52
52
  baseUrl: "https://replane.my-hosting.com",
53
53
  });
54
54
 
55
55
  // Get a config value (knows about latest updates via SSE)
56
- const featureFlag = configs.get("new-onboarding"); // Typed as boolean
56
+ const featureFlag = replane.get("new-onboarding"); // Typed as boolean
57
57
 
58
58
  if (featureFlag) {
59
59
  console.log("New onboarding enabled!");
60
60
  }
61
61
 
62
62
  // Typed config - no need to specify type again
63
- const passwordReqs = configs.get("password-requirements");
63
+ const passwordReqs = replane.get("password-requirements");
64
64
 
65
65
  // Use the value directly
66
66
  const { minLength } = passwordReqs; // TypeScript knows this is PasswordRequirements
67
67
 
68
68
  // With context for override evaluation
69
- const enabled = configs.get("billing-enabled", {
69
+ const enabled = replane.get("billing-enabled", {
70
70
  context: {
71
71
  userId: "user-123",
72
72
  plan: "premium",
@@ -79,7 +79,7 @@ if (enabled) {
79
79
  }
80
80
 
81
81
  // When done, clean up resources
82
- configs.close();
82
+ replane.close();
83
83
  ```
84
84
 
85
85
  ## API
@@ -105,7 +105,7 @@ Type parameter `T` defines the shape of your configs (a mapping of config names
105
105
  - `retryDelayMs` (number) – base delay between retries in ms (a small jitter is applied). Default: 200.
106
106
  - `logger` (object) – custom logger with `debug`, `info`, `warn`, `error` methods. Default: `console`.
107
107
 
108
- ### `configs.get<K>(name, options?)`
108
+ ### `replane.get<K>(name, options?)`
109
109
 
110
110
  Gets the current config value. The configs client maintains an up-to-date cache that receives realtime updates via Server-Sent Events (SSE) in the background.
111
111
 
@@ -119,7 +119,7 @@ Returns the config value of type `T[K]` (synchronous). The return type is automa
119
119
 
120
120
  Notes:
121
121
 
122
- - The configs client receives realtime updates via SSE in the background.
122
+ - The Replane client receives realtime updates via SSE in the background.
123
123
  - If the config is not found, throws a `ReplaneError` with code `not_found`.
124
124
  - Context-based overrides are evaluated automatically based on context.
125
125
 
@@ -131,24 +131,24 @@ interface Configs {
131
131
  "max-connections": number;
132
132
  }
133
133
 
134
- const configs = await createReplaneClient<Configs>({
134
+ const replane = await createReplaneClient<Configs>({
135
135
  sdkKey: "your-sdk-key",
136
136
  baseUrl: "https://replane.my-host.com",
137
137
  });
138
138
 
139
139
  // Get value without context - TypeScript knows this is boolean
140
- const enabled = configs.get("billing-enabled");
140
+ const enabled = replane.get("billing-enabled");
141
141
 
142
142
  // Get value with context for override evaluation
143
- const userEnabled = configs.get("billing-enabled", {
143
+ const userEnabled = replane.get("billing-enabled", {
144
144
  context: { userId: "user-123", plan: "premium" },
145
145
  });
146
146
 
147
147
  // Clean up when done
148
- configs.close();
148
+ replane.close();
149
149
  ```
150
150
 
151
- ### `configs.subscribe(callback)` or `configs.subscribe(configName, callback)`
151
+ ### `replane.subscribe(callback)` or `replane.subscribe(configName, callback)`
152
152
 
153
153
  Subscribe to config changes and receive real-time updates when configs are modified.
154
154
 
@@ -157,14 +157,14 @@ Subscribe to config changes and receive real-time updates when configs are modif
157
157
  1. **Subscribe to all config changes:**
158
158
 
159
159
  ```ts
160
- const unsubscribe = configs.subscribe((config) => {
160
+ const unsubscribe = replane.subscribe((config) => {
161
161
  console.log(`Config ${config.name} changed to:`, config.value);
162
162
  });
163
163
  ```
164
164
 
165
165
  2. **Subscribe to a specific config:**
166
166
  ```ts
167
- const unsubscribe = configs.subscribe("billing-enabled", (config) => {
167
+ const unsubscribe = replane.subscribe("billing-enabled", (config) => {
168
168
  console.log(`billing-enabled changed to:`, config.value);
169
169
  });
170
170
  ```
@@ -184,18 +184,18 @@ interface Configs {
184
184
  "max-connections": number;
185
185
  }
186
186
 
187
- const configs = await createReplaneClient<Configs>({
187
+ const replane = await createReplaneClient<Configs>({
188
188
  sdkKey: "your-sdk-key",
189
189
  baseUrl: "https://replane.my-host.com",
190
190
  });
191
191
 
192
192
  // Subscribe to all config changes
193
- const unsubscribeAll = configs.subscribe((config) => {
193
+ const unsubscribeAll = replane.subscribe((config) => {
194
194
  console.log(`Config ${config.name} updated:`, config.value);
195
195
  });
196
196
 
197
197
  // Subscribe to a specific config
198
- const unsubscribeFeature = configs.subscribe("feature-flag", (config) => {
198
+ const unsubscribeFeature = replane.subscribe("feature-flag", (config) => {
199
199
  console.log("Feature flag changed:", config.value);
200
200
  // config.value is typed as boolean
201
201
  });
@@ -205,7 +205,7 @@ unsubscribeAll();
205
205
  unsubscribeFeature();
206
206
 
207
207
  // Clean up when done
208
- configs.close();
208
+ replane.close();
209
209
  ```
210
210
 
211
211
  ### `createInMemoryReplaneClient(initialData)`
@@ -234,34 +234,34 @@ interface Configs {
234
234
  "max-items": { value: number; ttl: number };
235
235
  }
236
236
 
237
- const configs = createInMemoryReplaneClient<Configs>({
237
+ const replane = createInMemoryReplaneClient<Configs>({
238
238
  "feature-a": true,
239
239
  "max-items": { value: 10, ttl: 3600 },
240
240
  });
241
241
 
242
- const featureA = configs.get("feature-a"); // TypeScript knows this is boolean
242
+ const featureA = replane.get("feature-a"); // TypeScript knows this is boolean
243
243
  console.log(featureA); // true
244
244
 
245
- const maxItems = configs.get("max-items"); // TypeScript knows the type
245
+ const maxItems = replane.get("max-items"); // TypeScript knows the type
246
246
  console.log(maxItems); // { value: 10, ttl: 3600 }
247
247
 
248
- configs.close();
248
+ replane.close();
249
249
  ```
250
250
 
251
- ### `configs.close()`
251
+ ### `replane.close()`
252
252
 
253
- Gracefully shuts down the configs client and cleans up resources. Subsequent method calls will throw. Use this in environments where you manage resource lifecycles explicitly (e.g. shutting down a server or worker).
253
+ Gracefully shuts down the Replane client and cleans up resources. Subsequent method calls will throw. Use this in environments where you manage resource lifecycles explicitly (e.g. shutting down a server or worker).
254
254
 
255
255
  ```ts
256
256
  // During shutdown
257
- configs.close();
257
+ replane.close();
258
258
  ```
259
259
 
260
260
  ### Errors
261
261
 
262
262
  `createReplaneClient` throws if the initial request to fetch configs fails with non‑2xx HTTP responses and network errors. A `ReplaneError` is thrown for HTTP failures; other errors may be thrown for network/parse issues.
263
263
 
264
- The configs client receives realtime updates via SSE in the background. SSE connection errors are logged and automatically retried, but don't affect `get` calls (which return the last known value).
264
+ The Replane client receives realtime updates via SSE in the background. SSE connection errors are logged and automatically retried, but don't affect `get` calls (which return the last known value).
265
265
 
266
266
  ## Environment notes
267
267
 
@@ -282,12 +282,12 @@ interface Configs {
282
282
  layout: LayoutConfig;
283
283
  }
284
284
 
285
- const configs = await createReplaneClient<Configs>({
285
+ const replane = await createReplaneClient<Configs>({
286
286
  sdkKey: process.env.REPLANE_SDK_KEY!,
287
287
  baseUrl: "https://replane.my-host.com",
288
288
  });
289
289
 
290
- const layout = configs.get("layout"); // TypeScript knows this is LayoutConfig
290
+ const layout = replane.get("layout"); // TypeScript knows this is LayoutConfig
291
291
  console.log(layout); // { variant: "a", ttl: 3600 }
292
292
  ```
293
293
 
@@ -298,7 +298,7 @@ interface Configs {
298
298
  "advanced-features": boolean;
299
299
  }
300
300
 
301
- const configs = await createReplaneClient<Configs>({
301
+ const replane = await createReplaneClient<Configs>({
302
302
  sdkKey: process.env.REPLANE_SDK_KEY!,
303
303
  baseUrl: "https://replane.my-host.com",
304
304
  });
@@ -306,12 +306,12 @@ const configs = await createReplaneClient<Configs>({
306
306
  // Config has base value `false` but override: if `plan === "premium"` then `true`
307
307
 
308
308
  // Free user
309
- const freeUserEnabled = configs.get("advanced-features", {
309
+ const freeUserEnabled = replane.get("advanced-features", {
310
310
  context: { plan: "free" },
311
311
  }); // false
312
312
 
313
313
  // Premium user
314
- const premiumUserEnabled = configs.get("advanced-features", {
314
+ const premiumUserEnabled = replane.get("advanced-features", {
315
315
  context: { plan: "premium" },
316
316
  }); // true
317
317
  ```
@@ -323,7 +323,7 @@ interface Configs {
323
323
  "feature-flag": boolean;
324
324
  }
325
325
 
326
- const configs = await createReplaneClient<Configs>({
326
+ const replane = await createReplaneClient<Configs>({
327
327
  sdkKey: process.env.REPLANE_SDK_KEY!,
328
328
  baseUrl: "https://replane.my-host.com",
329
329
  context: {
@@ -333,8 +333,8 @@ const configs = await createReplaneClient<Configs>({
333
333
  });
334
334
 
335
335
  // This context is used for all configs unless overridden
336
- const value1 = configs.get("feature-flag"); // Uses client-level context
337
- const value2 = configs.get("feature-flag", {
336
+ const value1 = replane.get("feature-flag"); // Uses client-level context
337
+ const value2 = replane.get("feature-flag", {
338
338
  context: { userId: "user-321" },
339
339
  }); // Merges with client context
340
340
  ```
@@ -342,7 +342,7 @@ const value2 = configs.get("feature-flag", {
342
342
  ### Custom fetch (tests)
343
343
 
344
344
  ```ts
345
- const configs = await createReplaneClient({
345
+ const replane = await createReplaneClient({
346
346
  sdkKey: "TKN",
347
347
  baseUrl: "https://api",
348
348
  fetchFn: mockFetch,
@@ -358,7 +358,7 @@ interface Configs {
358
358
  "optional-feature": boolean;
359
359
  }
360
360
 
361
- const configs = await createReplaneClient<Configs>({
361
+ const replane = await createReplaneClient<Configs>({
362
362
  sdkKey: process.env.REPLANE_SDK_KEY!,
363
363
  baseUrl: "https://replane.my-host.com",
364
364
  required: {
@@ -383,7 +383,7 @@ interface Configs {
383
383
  "timeout-ms": number;
384
384
  }
385
385
 
386
- const configs = await createReplaneClient<Configs>({
386
+ const replane = await createReplaneClient<Configs>({
387
387
  sdkKey: process.env.REPLANE_SDK_KEY!,
388
388
  baseUrl: "https://replane.my-host.com",
389
389
  fallbacks: {
@@ -395,7 +395,7 @@ const configs = await createReplaneClient<Configs>({
395
395
 
396
396
  // If the initial fetch fails, fallback values are used
397
397
  // Once the configs client connects, it will receive realtime updates
398
- const maxConnections = configs.get("max-connections"); // 10 (or real value)
398
+ const maxConnections = replane.get("max-connections"); // 10 (or real value)
399
399
  ```
400
400
 
401
401
  ### Multiple projects
@@ -411,7 +411,7 @@ interface ProjectBConfigs {
411
411
  "api-rate-limit": number;
412
412
  }
413
413
 
414
- // Each project needs its own SDK key and configs client instance
414
+ // Each project needs its own SDK key and Replane client instance
415
415
  const projectAConfigs = await createReplaneClient<ProjectAConfigs>({
416
416
  sdkKey: process.env.PROJECT_A_SDK_KEY!,
417
417
  baseUrl: "https://replane.my-host.com",
@@ -422,7 +422,7 @@ const projectBConfigs = await createReplaneClient<ProjectBConfigs>({
422
422
  baseUrl: "https://replane.my-host.com",
423
423
  });
424
424
 
425
- // Each configs client only accesses configs from its respective project
425
+ // Each Replane client only accesses configs from its respective project
426
426
  const featureA = projectAConfigs.get("feature-flag"); // boolean
427
427
  const featureB = projectBConfigs.get("feature-flag"); // boolean
428
428
  ```
@@ -435,13 +435,13 @@ interface Configs {
435
435
  "max-users": number;
436
436
  }
437
437
 
438
- const configs = await createReplaneClient<Configs>({
438
+ const replane = await createReplaneClient<Configs>({
439
439
  sdkKey: process.env.REPLANE_SDK_KEY!,
440
440
  baseUrl: "https://replane.my-host.com",
441
441
  });
442
442
 
443
443
  // Subscribe to all config changes
444
- const unsubscribeAll = configs.subscribe((config) => {
444
+ const unsubscribeAll = replane.subscribe((config) => {
445
445
  console.log(`Config ${config.name} changed:`, config.value);
446
446
 
447
447
  // React to specific config changes
@@ -451,13 +451,13 @@ const unsubscribeAll = configs.subscribe((config) => {
451
451
  });
452
452
 
453
453
  // Subscribe to a specific config only
454
- const unsubscribeFeature = configs.subscribe("feature-flag", (config) => {
454
+ const unsubscribeFeature = replane.subscribe("feature-flag", (config) => {
455
455
  console.log("Feature flag changed:", config.value);
456
456
  // config.value is automatically typed as boolean
457
457
  });
458
458
 
459
459
  // Subscribe to multiple specific configs
460
- const unsubscribeMaxUsers = configs.subscribe("max-users", (config) => {
460
+ const unsubscribeMaxUsers = replane.subscribe("max-users", (config) => {
461
461
  console.log("Max users changed:", config.value);
462
462
  // config.value is automatically typed as number
463
463
  });
@@ -466,7 +466,7 @@ const unsubscribeMaxUsers = configs.subscribe("max-users", (config) => {
466
466
  unsubscribeAll();
467
467
  unsubscribeFeature();
468
468
  unsubscribeMaxUsers();
469
- configs.close();
469
+ replane.close();
470
470
  ```
471
471
 
472
472
  ## Roadmap
package/dist/index.cjs CHANGED
@@ -213,24 +213,44 @@ class ReplaneRemoteStorage {
213
213
  }
214
214
  }
215
215
  async *startReplicationStreamImpl(options) {
216
- const rawEvents = fetchSse({
217
- fetchFn: options.fetchFn,
218
- headers: {
219
- Authorization: this.getAuthHeader(options),
220
- "Content-Type": "application/json"
221
- },
222
- body: JSON.stringify(options.getBody()),
223
- timeoutMs: options.requestTimeoutMs,
224
- method: "POST",
225
- signal: options.signal,
226
- url: this.getApiEndpoint(`/sdk/v1/replication/stream`, options),
227
- onConnect: options.onConnect
228
- });
229
- for await (const rawEvent of rawEvents) {
230
- const event = JSON.parse(rawEvent);
231
- if (typeof event === "object" && event !== null && "type" in event && typeof event.type === "string" && SUPPORTED_REPLICATION_STREAM_RECORD_TYPES.includes(event.type)) {
232
- yield event;
216
+ const inactivityAbortController = new AbortController();
217
+ const { signal: combinedSignal, cleanUpSignals } = options.signal ? combineAbortSignals([options.signal, inactivityAbortController.signal]) : { signal: inactivityAbortController.signal, cleanUpSignals: () => {
218
+ } };
219
+ let inactivityTimer = null;
220
+ const resetInactivityTimer = () => {
221
+ if (inactivityTimer) clearTimeout(inactivityTimer);
222
+ inactivityTimer = setTimeout(() => {
223
+ inactivityAbortController.abort();
224
+ }, options.inactivityTimeoutMs);
225
+ };
226
+ try {
227
+ const rawEvents = fetchSse({
228
+ fetchFn: options.fetchFn,
229
+ headers: {
230
+ Authorization: this.getAuthHeader(options),
231
+ "Content-Type": "application/json"
232
+ },
233
+ body: JSON.stringify(options.getBody()),
234
+ timeoutMs: options.requestTimeoutMs,
235
+ method: "POST",
236
+ signal: combinedSignal,
237
+ url: this.getApiEndpoint(`/sdk/v1/replication/stream`, options),
238
+ onConnect: () => {
239
+ resetInactivityTimer();
240
+ options.onConnect?.();
241
+ }
242
+ });
243
+ for await (const sseEvent of rawEvents) {
244
+ resetInactivityTimer();
245
+ if (sseEvent.type === "comment") continue;
246
+ const event = JSON.parse(sseEvent.data);
247
+ if (typeof event === "object" && event !== null && "type" in event && typeof event.type === "string" && SUPPORTED_REPLICATION_STREAM_RECORD_TYPES.includes(event.type)) {
248
+ yield event;
249
+ }
233
250
  }
251
+ } finally {
252
+ if (inactivityTimer) clearTimeout(inactivityTimer);
253
+ cleanUpSignals();
234
254
  }
235
255
  }
236
256
  close() {
@@ -304,20 +324,17 @@ async function _createReplaneClient(sdkOptions, storage) {
304
324
  currentConfigs: [...configs.values()].map((config) => ({
305
325
  name: config.name,
306
326
  overrides: config.overrides,
307
- version: config.version,
308
327
  value: config.value
309
328
  })),
310
329
  requiredConfigs: sdkOptions.requiredConfigs
311
330
  })
312
331
  });
313
332
  for await (const event of replicationStream) {
314
- const updatedConfigs = event.type === "config_change" ? [event] : event.configs;
333
+ const updatedConfigs = event.type === "config_change" ? [event.config] : event.configs;
315
334
  for (const config of updatedConfigs) {
316
- if (config.version <= (configs.get(config.name)?.version ?? -1)) continue;
317
335
  configs.set(config.name, {
318
336
  name: config.name,
319
337
  overrides: config.overrides,
320
- version: config.version,
321
338
  value: config.value
322
339
  });
323
340
  for (const callback of clientSubscriptions) {
@@ -436,6 +453,7 @@ function toFinalOptions(defaults) {
436
453
  globalThis.fetch.bind(globalThis),
437
454
  requestTimeoutMs: defaults.requestTimeoutMs ?? 2e3,
438
455
  initializationTimeoutMs: defaults.initializationTimeoutMs ?? 5e3,
456
+ inactivityTimeoutMs: defaults.inactivityTimeoutMs ?? 3e4,
439
457
  logger: defaults.logger ?? console,
440
458
  retryDelayMs: defaults.retryDelayMs ?? 200,
441
459
  context: {
@@ -513,17 +531,23 @@ async function* fetchSse(params) {
513
531
  buffer = frames.pop() ?? "";
514
532
  for (const frame of frames) {
515
533
  const dataLines = [];
534
+ let comment = null;
516
535
  for (const rawLine of frame.split(/\r?\n/)) {
517
536
  if (!rawLine) continue;
518
- if (rawLine.startsWith(":")) continue;
537
+ if (rawLine.startsWith(":")) {
538
+ comment = rawLine.slice(1);
539
+ continue;
540
+ }
519
541
  if (rawLine.startsWith(SSE_DATA_PREFIX)) {
520
542
  const line = rawLine.slice(SSE_DATA_PREFIX.length).replace(/^\s/, "");
521
543
  dataLines.push(line);
522
544
  }
523
545
  }
524
546
  if (dataLines.length) {
525
- const payload = dataLines.join("\n");
526
- yield payload;
547
+ const data = dataLines.join("\n");
548
+ yield { type: "data", data };
549
+ } else if (comment !== null) {
550
+ yield { type: "comment", comment };
527
551
  }
528
552
  }
529
553
  }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  //#region src/index.d.ts
2
2
  type Configs = object;
3
- type ReplaneContext = Record<string, unknown>;
3
+ type ReplaneContext = Record<string, string | number | boolean | null | undefined>;
4
4
  interface ReplaneClientOptions<T extends Configs> {
5
5
  /**
6
6
  * Base URL of the Replane instance (no trailing slash).
@@ -36,6 +36,12 @@ interface ReplaneClientOptions<T extends Configs> {
36
36
  * @default 200
37
37
  */
38
38
  retryDelayMs?: number;
39
+ /**
40
+ * Timeout in ms for SSE connection inactivity.
41
+ * If no events (including pings) are received within this time, the connection will be re-established.
42
+ * @default 30000
43
+ */
44
+ inactivityTimeoutMs?: number;
39
45
  /**
40
46
  * Optional logger (defaults to console).
41
47
  */
package/dist/index.js CHANGED
@@ -164,22 +164,44 @@ var ReplaneRemoteStorage = class {
164
164
  }
165
165
  }
166
166
  async *startReplicationStreamImpl(options) {
167
- const rawEvents = fetchSse({
168
- fetchFn: options.fetchFn,
169
- headers: {
170
- Authorization: this.getAuthHeader(options),
171
- "Content-Type": "application/json"
172
- },
173
- body: JSON.stringify(options.getBody()),
174
- timeoutMs: options.requestTimeoutMs,
175
- method: "POST",
176
- signal: options.signal,
177
- url: this.getApiEndpoint(`/sdk/v1/replication/stream`, options),
178
- onConnect: options.onConnect
179
- });
180
- for await (const rawEvent of rawEvents) {
181
- const event = JSON.parse(rawEvent);
182
- if (typeof event === "object" && event !== null && "type" in event && typeof event.type === "string" && SUPPORTED_REPLICATION_STREAM_RECORD_TYPES.includes(event.type)) yield event;
167
+ const inactivityAbortController = new AbortController();
168
+ const { signal: combinedSignal, cleanUpSignals } = options.signal ? combineAbortSignals([options.signal, inactivityAbortController.signal]) : {
169
+ signal: inactivityAbortController.signal,
170
+ cleanUpSignals: () => {}
171
+ };
172
+ let inactivityTimer = null;
173
+ const resetInactivityTimer = () => {
174
+ if (inactivityTimer) clearTimeout(inactivityTimer);
175
+ inactivityTimer = setTimeout(() => {
176
+ inactivityAbortController.abort();
177
+ }, options.inactivityTimeoutMs);
178
+ };
179
+ try {
180
+ const rawEvents = fetchSse({
181
+ fetchFn: options.fetchFn,
182
+ headers: {
183
+ Authorization: this.getAuthHeader(options),
184
+ "Content-Type": "application/json"
185
+ },
186
+ body: JSON.stringify(options.getBody()),
187
+ timeoutMs: options.requestTimeoutMs,
188
+ method: "POST",
189
+ signal: combinedSignal,
190
+ url: this.getApiEndpoint(`/sdk/v1/replication/stream`, options),
191
+ onConnect: () => {
192
+ resetInactivityTimer();
193
+ options.onConnect?.();
194
+ }
195
+ });
196
+ for await (const sseEvent of rawEvents) {
197
+ resetInactivityTimer();
198
+ if (sseEvent.type === "comment") continue;
199
+ const event = JSON.parse(sseEvent.data);
200
+ if (typeof event === "object" && event !== null && "type" in event && typeof event.type === "string" && SUPPORTED_REPLICATION_STREAM_RECORD_TYPES.includes(event.type)) yield event;
201
+ }
202
+ } finally {
203
+ if (inactivityTimer) clearTimeout(inactivityTimer);
204
+ cleanUpSignals();
183
205
  }
184
206
  }
185
207
  close() {
@@ -259,20 +281,17 @@ async function _createReplaneClient(sdkOptions, storage) {
259
281
  currentConfigs: [...configs.values()].map((config) => ({
260
282
  name: config.name,
261
283
  overrides: config.overrides,
262
- version: config.version,
263
284
  value: config.value
264
285
  })),
265
286
  requiredConfigs: sdkOptions.requiredConfigs
266
287
  })
267
288
  });
268
289
  for await (const event of replicationStream) {
269
- const updatedConfigs = event.type === "config_change" ? [event] : event.configs;
290
+ const updatedConfigs = event.type === "config_change" ? [event.config] : event.configs;
270
291
  for (const config of updatedConfigs) {
271
- if (config.version <= (configs.get(config.name)?.version ?? -1)) continue;
272
292
  configs.set(config.name, {
273
293
  name: config.name,
274
294
  overrides: config.overrides,
275
- version: config.version,
276
295
  value: config.value
277
296
  });
278
297
  for (const callback of clientSubscriptions) callback({
@@ -370,6 +389,7 @@ function toFinalOptions(defaults) {
370
389
  fetchFn: defaults.fetchFn ?? globalThis.fetch.bind(globalThis),
371
390
  requestTimeoutMs: defaults.requestTimeoutMs ?? 2e3,
372
391
  initializationTimeoutMs: defaults.initializationTimeoutMs ?? 5e3,
392
+ inactivityTimeoutMs: defaults.inactivityTimeoutMs ?? 3e4,
373
393
  logger: defaults.logger ?? console,
374
394
  retryDelayMs: defaults.retryDelayMs ?? 200,
375
395
  context: { ...defaults.context ?? {} },
@@ -437,18 +457,28 @@ async function* fetchSse(params) {
437
457
  buffer = frames.pop() ?? "";
438
458
  for (const frame of frames) {
439
459
  const dataLines = [];
460
+ let comment = null;
440
461
  for (const rawLine of frame.split(/\r?\n/)) {
441
462
  if (!rawLine) continue;
442
- if (rawLine.startsWith(":")) continue;
463
+ if (rawLine.startsWith(":")) {
464
+ comment = rawLine.slice(1);
465
+ continue;
466
+ }
443
467
  if (rawLine.startsWith(SSE_DATA_PREFIX)) {
444
468
  const line = rawLine.slice(SSE_DATA_PREFIX.length).replace(/^\s/, "");
445
469
  dataLines.push(line);
446
470
  }
447
471
  }
448
472
  if (dataLines.length) {
449
- const payload = dataLines.join("\n");
450
- yield payload;
451
- }
473
+ const data = dataLines.join("\n");
474
+ yield {
475
+ type: "data",
476
+ data
477
+ };
478
+ } else if (comment !== null) yield {
479
+ type: "comment",
480
+ comment
481
+ };
452
482
  }
453
483
  }
454
484
  } finally {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@replanejs/sdk",
3
- "version": "0.5.11",
3
+ "version": "0.6.1",
4
4
  "description": "Dynamic configuration SDK for browser and server environments (Node.js, Deno, Bun). Powered by Replane.",
5
5
  "type": "module",
6
6
  "license": "MIT",