@replanejs/sdk 0.5.11 → 0.5.12
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 +45 -45
- package/dist/index.cjs +46 -19
- package/dist/index.d.ts +6 -0
- package/dist/index.js +49 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,27 +46,27 @@ interface PasswordRequirements {
|
|
|
46
46
|
requireSymbol: boolean;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
const
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
### `
|
|
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
|
|
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
|
|
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 =
|
|
140
|
+
const enabled = replane.get("billing-enabled");
|
|
141
141
|
|
|
142
142
|
// Get value with context for override evaluation
|
|
143
|
-
const userEnabled =
|
|
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
|
-
|
|
148
|
+
replane.close();
|
|
149
149
|
```
|
|
150
150
|
|
|
151
|
-
### `
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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 =
|
|
242
|
+
const featureA = replane.get("feature-a"); // TypeScript knows this is boolean
|
|
243
243
|
console.log(featureA); // true
|
|
244
244
|
|
|
245
|
-
const maxItems =
|
|
245
|
+
const maxItems = replane.get("max-items"); // TypeScript knows the type
|
|
246
246
|
console.log(maxItems); // { value: 10, ttl: 3600 }
|
|
247
247
|
|
|
248
|
-
|
|
248
|
+
replane.close();
|
|
249
249
|
```
|
|
250
250
|
|
|
251
|
-
### `
|
|
251
|
+
### `replane.close()`
|
|
252
252
|
|
|
253
|
-
Gracefully shuts down the
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
309
|
+
const freeUserEnabled = replane.get("advanced-features", {
|
|
310
310
|
context: { plan: "free" },
|
|
311
311
|
}); // false
|
|
312
312
|
|
|
313
313
|
// Premium user
|
|
314
|
-
const premiumUserEnabled =
|
|
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
|
|
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 =
|
|
337
|
-
const value2 =
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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 === "ping") continue;
|
|
246
|
+
const event = JSON.parse(sseEvent.payload);
|
|
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() {
|
|
@@ -436,6 +456,7 @@ function toFinalOptions(defaults) {
|
|
|
436
456
|
globalThis.fetch.bind(globalThis),
|
|
437
457
|
requestTimeoutMs: defaults.requestTimeoutMs ?? 2e3,
|
|
438
458
|
initializationTimeoutMs: defaults.initializationTimeoutMs ?? 5e3,
|
|
459
|
+
inactivityTimeoutMs: defaults.inactivityTimeoutMs ?? 6e4,
|
|
439
460
|
logger: defaults.logger ?? console,
|
|
440
461
|
retryDelayMs: defaults.retryDelayMs ?? 200,
|
|
441
462
|
context: {
|
|
@@ -513,9 +534,13 @@ async function* fetchSse(params) {
|
|
|
513
534
|
buffer = frames.pop() ?? "";
|
|
514
535
|
for (const frame of frames) {
|
|
515
536
|
const dataLines = [];
|
|
537
|
+
let isPing = false;
|
|
516
538
|
for (const rawLine of frame.split(/\r?\n/)) {
|
|
517
539
|
if (!rawLine) continue;
|
|
518
|
-
if (rawLine.startsWith(":"))
|
|
540
|
+
if (rawLine.startsWith(":")) {
|
|
541
|
+
isPing = true;
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
519
544
|
if (rawLine.startsWith(SSE_DATA_PREFIX)) {
|
|
520
545
|
const line = rawLine.slice(SSE_DATA_PREFIX.length).replace(/^\s/, "");
|
|
521
546
|
dataLines.push(line);
|
|
@@ -523,7 +548,9 @@ async function* fetchSse(params) {
|
|
|
523
548
|
}
|
|
524
549
|
if (dataLines.length) {
|
|
525
550
|
const payload = dataLines.join("\n");
|
|
526
|
-
yield payload;
|
|
551
|
+
yield { type: "data", payload };
|
|
552
|
+
} else if (isPing) {
|
|
553
|
+
yield { type: "ping" };
|
|
527
554
|
}
|
|
528
555
|
}
|
|
529
556
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -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 60000
|
|
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
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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 === "ping") continue;
|
|
199
|
+
const event = JSON.parse(sseEvent.payload);
|
|
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() {
|
|
@@ -370,6 +392,7 @@ function toFinalOptions(defaults) {
|
|
|
370
392
|
fetchFn: defaults.fetchFn ?? globalThis.fetch.bind(globalThis),
|
|
371
393
|
requestTimeoutMs: defaults.requestTimeoutMs ?? 2e3,
|
|
372
394
|
initializationTimeoutMs: defaults.initializationTimeoutMs ?? 5e3,
|
|
395
|
+
inactivityTimeoutMs: defaults.inactivityTimeoutMs ?? 6e4,
|
|
373
396
|
logger: defaults.logger ?? console,
|
|
374
397
|
retryDelayMs: defaults.retryDelayMs ?? 200,
|
|
375
398
|
context: { ...defaults.context ?? {} },
|
|
@@ -437,9 +460,13 @@ async function* fetchSse(params) {
|
|
|
437
460
|
buffer = frames.pop() ?? "";
|
|
438
461
|
for (const frame of frames) {
|
|
439
462
|
const dataLines = [];
|
|
463
|
+
let isPing = false;
|
|
440
464
|
for (const rawLine of frame.split(/\r?\n/)) {
|
|
441
465
|
if (!rawLine) continue;
|
|
442
|
-
if (rawLine.startsWith(":"))
|
|
466
|
+
if (rawLine.startsWith(":")) {
|
|
467
|
+
isPing = true;
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
443
470
|
if (rawLine.startsWith(SSE_DATA_PREFIX)) {
|
|
444
471
|
const line = rawLine.slice(SSE_DATA_PREFIX.length).replace(/^\s/, "");
|
|
445
472
|
dataLines.push(line);
|
|
@@ -447,8 +474,11 @@ async function* fetchSse(params) {
|
|
|
447
474
|
}
|
|
448
475
|
if (dataLines.length) {
|
|
449
476
|
const payload = dataLines.join("\n");
|
|
450
|
-
yield
|
|
451
|
-
|
|
477
|
+
yield {
|
|
478
|
+
type: "data",
|
|
479
|
+
payload
|
|
480
|
+
};
|
|
481
|
+
} else if (isPing) yield { type: "ping" };
|
|
452
482
|
}
|
|
453
483
|
}
|
|
454
484
|
} finally {
|
package/package.json
CHANGED