@replanejs/sdk 0.5.12 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +10 -609
- package/dist/index.d.ts +201 -46
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +569 -345
- package/dist/index.js.map +1 -0
- package/package.json +6 -13
- package/LICENSE +0 -21
- package/README.md +0 -479
package/dist/index.js
CHANGED
|
@@ -1,144 +1,239 @@
|
|
|
1
|
-
//#region src/
|
|
2
|
-
const SUPPORTED_REPLICATION_STREAM_RECORD_TYPES = Object.keys({
|
|
3
|
-
config_change: true,
|
|
4
|
-
init: true
|
|
5
|
-
});
|
|
1
|
+
//#region src/error.ts
|
|
6
2
|
/**
|
|
7
|
-
*
|
|
3
|
+
* Error codes for ReplaneError
|
|
8
4
|
*/
|
|
9
|
-
function
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
5
|
+
let ReplaneErrorCode = /* @__PURE__ */ function(ReplaneErrorCode$1) {
|
|
6
|
+
ReplaneErrorCode$1["NotFound"] = "not_found";
|
|
7
|
+
ReplaneErrorCode$1["Timeout"] = "timeout";
|
|
8
|
+
ReplaneErrorCode$1["NetworkError"] = "network_error";
|
|
9
|
+
ReplaneErrorCode$1["AuthError"] = "auth_error";
|
|
10
|
+
ReplaneErrorCode$1["Forbidden"] = "forbidden";
|
|
11
|
+
ReplaneErrorCode$1["ServerError"] = "server_error";
|
|
12
|
+
ReplaneErrorCode$1["ClientError"] = "client_error";
|
|
13
|
+
ReplaneErrorCode$1["Closed"] = "closed";
|
|
14
|
+
ReplaneErrorCode$1["NotInitialized"] = "not_initialized";
|
|
15
|
+
ReplaneErrorCode$1["Unknown"] = "unknown";
|
|
16
|
+
return ReplaneErrorCode$1;
|
|
17
|
+
}({});
|
|
18
|
+
/**
|
|
19
|
+
* Custom error class for Replane SDK errors
|
|
20
|
+
*/
|
|
21
|
+
var ReplaneError = class extends Error {
|
|
22
|
+
code;
|
|
23
|
+
constructor(params) {
|
|
24
|
+
super(params.message, { cause: params.cause });
|
|
25
|
+
this.name = "ReplaneError";
|
|
26
|
+
this.code = params.code;
|
|
16
27
|
}
|
|
17
|
-
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region src/utils.ts
|
|
32
|
+
/**
|
|
33
|
+
* Returns a promise that resolves after the specified delay
|
|
34
|
+
*
|
|
35
|
+
* @param ms - Delay in milliseconds
|
|
36
|
+
*/
|
|
37
|
+
async function delay(ms) {
|
|
38
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
18
39
|
}
|
|
19
40
|
/**
|
|
20
|
-
*
|
|
41
|
+
* Returns a promise that resolves after a delay with jitter.
|
|
42
|
+
* The actual delay is the average delay ± 10% (jitter = averageDelay/5).
|
|
43
|
+
*
|
|
44
|
+
* @param averageDelay - The average delay in milliseconds
|
|
21
45
|
*/
|
|
22
|
-
function
|
|
23
|
-
const
|
|
24
|
-
|
|
46
|
+
async function retryDelay(averageDelay) {
|
|
47
|
+
const jitter = averageDelay / 5;
|
|
48
|
+
const delayMs = averageDelay + Math.random() * jitter - jitter / 2;
|
|
49
|
+
await delay(delayMs);
|
|
25
50
|
}
|
|
26
51
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
52
|
+
* Combines multiple abort signals into one.
|
|
53
|
+
* When any of the input signals is aborted, the combined signal will also be aborted.
|
|
54
|
+
*
|
|
55
|
+
* @param signals - Array of AbortSignal instances (can contain undefined/null)
|
|
56
|
+
* @returns An object containing the combined signal and a cleanup function
|
|
29
57
|
*/
|
|
30
|
-
function
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
58
|
+
function combineAbortSignals(signals) {
|
|
59
|
+
const controller = new AbortController();
|
|
60
|
+
const onAbort = () => {
|
|
61
|
+
controller.abort();
|
|
62
|
+
cleanUpSignals();
|
|
63
|
+
};
|
|
64
|
+
const cleanUpSignals = () => {
|
|
65
|
+
for (const s of signals) s?.removeEventListener("abort", onAbort);
|
|
66
|
+
};
|
|
67
|
+
for (const s of signals) s?.addEventListener("abort", onAbort, { once: true });
|
|
68
|
+
if (signals.some((s) => s?.aborted)) onAbort();
|
|
69
|
+
return {
|
|
70
|
+
signal: controller.signal,
|
|
71
|
+
cleanUpSignals
|
|
72
|
+
};
|
|
39
73
|
}
|
|
40
74
|
/**
|
|
41
|
-
*
|
|
75
|
+
* A deferred promise that can be resolved or rejected from outside.
|
|
76
|
+
* Useful for coordinating async operations.
|
|
42
77
|
*/
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const results = condition.conditions.map((c) => evaluateCondition(c, context, logger));
|
|
53
|
-
if (results.some((r) => r === "matched")) return "matched";
|
|
54
|
-
if (results.some((r) => r === "unknown")) return "unknown";
|
|
55
|
-
return "not_matched";
|
|
56
|
-
}
|
|
57
|
-
if (operator === "not") {
|
|
58
|
-
const result = evaluateCondition(condition.condition, context, logger);
|
|
59
|
-
if (result === "matched") return "not_matched";
|
|
60
|
-
if (result === "not_matched") return "matched";
|
|
61
|
-
return "unknown";
|
|
62
|
-
}
|
|
63
|
-
if (operator === "segmentation") {
|
|
64
|
-
const contextValue$1 = context[condition.property];
|
|
65
|
-
if (contextValue$1 === void 0 || contextValue$1 === null) return "unknown";
|
|
66
|
-
const hashInput = String(contextValue$1) + condition.seed;
|
|
67
|
-
const unitValue = fnv1a32ToUnit(hashInput);
|
|
68
|
-
return unitValue >= condition.fromPercentage / 100 && unitValue < condition.toPercentage / 100 ? "matched" : "not_matched";
|
|
78
|
+
var Deferred = class {
|
|
79
|
+
promise;
|
|
80
|
+
resolve;
|
|
81
|
+
reject;
|
|
82
|
+
constructor() {
|
|
83
|
+
this.promise = new Promise((resolve, reject) => {
|
|
84
|
+
this.resolve = resolve;
|
|
85
|
+
this.reject = reject;
|
|
86
|
+
});
|
|
69
87
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
case "greater_than":
|
|
92
|
-
if (typeof contextValue === "number" && typeof castedValue === "number") return contextValue > castedValue ? "matched" : "not_matched";
|
|
93
|
-
if (typeof contextValue === "string" && typeof castedValue === "string") return contextValue > castedValue ? "matched" : "not_matched";
|
|
94
|
-
return "not_matched";
|
|
95
|
-
case "greater_than_or_equal":
|
|
96
|
-
if (typeof contextValue === "number" && typeof castedValue === "number") return contextValue >= castedValue ? "matched" : "not_matched";
|
|
97
|
-
if (typeof contextValue === "string" && typeof castedValue === "string") return contextValue >= castedValue ? "matched" : "not_matched";
|
|
98
|
-
return "not_matched";
|
|
99
|
-
default:
|
|
100
|
-
warnNever(operator, logger, `Unexpected operator: ${operator}`);
|
|
101
|
-
return "unknown";
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/sse.ts
|
|
92
|
+
const SSE_DATA_PREFIX = "data:";
|
|
93
|
+
/**
|
|
94
|
+
* Fetch with timeout support
|
|
95
|
+
*/
|
|
96
|
+
async function fetchWithTimeout(input, init, timeoutMs, fetchFn) {
|
|
97
|
+
if (!fetchFn) throw new Error("Global fetch is not available. Provide options.fetchFn.");
|
|
98
|
+
if (!timeoutMs) return fetchFn(input, init);
|
|
99
|
+
const timeoutController = new AbortController();
|
|
100
|
+
const t = setTimeout(() => timeoutController.abort(), timeoutMs);
|
|
101
|
+
const { signal } = combineAbortSignals([init.signal, timeoutController.signal]);
|
|
102
|
+
try {
|
|
103
|
+
return await fetchFn(input, {
|
|
104
|
+
...init,
|
|
105
|
+
signal
|
|
106
|
+
});
|
|
107
|
+
} finally {
|
|
108
|
+
clearTimeout(t);
|
|
102
109
|
}
|
|
103
110
|
}
|
|
104
|
-
function warnNever(value, logger, message) {
|
|
105
|
-
logger.warn(message, { value });
|
|
106
|
-
}
|
|
107
111
|
/**
|
|
108
|
-
*
|
|
112
|
+
* Ensures the response is successful, throwing ReplaneError if not
|
|
109
113
|
*/
|
|
110
|
-
function
|
|
111
|
-
if (
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
114
|
+
async function ensureSuccessfulResponse(response, message) {
|
|
115
|
+
if (response.status === 404) throw new ReplaneError({
|
|
116
|
+
message: `Not found: ${message}`,
|
|
117
|
+
code: ReplaneErrorCode.NotFound
|
|
118
|
+
});
|
|
119
|
+
if (response.status === 401) throw new ReplaneError({
|
|
120
|
+
message: `Unauthorized access: ${message}`,
|
|
121
|
+
code: ReplaneErrorCode.AuthError
|
|
122
|
+
});
|
|
123
|
+
if (response.status === 403) throw new ReplaneError({
|
|
124
|
+
message: `Forbidden access: ${message}`,
|
|
125
|
+
code: ReplaneErrorCode.Forbidden
|
|
126
|
+
});
|
|
127
|
+
if (!response.ok) {
|
|
128
|
+
let body;
|
|
129
|
+
try {
|
|
130
|
+
body = await response.text();
|
|
131
|
+
} catch {
|
|
132
|
+
body = "<unable to read response body>";
|
|
115
133
|
}
|
|
116
|
-
|
|
134
|
+
const code = response.status >= 500 ? ReplaneErrorCode.ServerError : response.status >= 400 ? ReplaneErrorCode.ClientError : ReplaneErrorCode.Unknown;
|
|
135
|
+
throw new ReplaneError({
|
|
136
|
+
message: `Fetch response isn't successful (${message}): ${response.status} ${response.statusText} - ${body}`,
|
|
137
|
+
code
|
|
138
|
+
});
|
|
117
139
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Fetches a Server-Sent Events (SSE) stream and yields parsed events.
|
|
143
|
+
*
|
|
144
|
+
* @param params - Options for the SSE fetch
|
|
145
|
+
* @yields SseEvent objects containing either data or comment events
|
|
146
|
+
*/
|
|
147
|
+
async function* fetchSse(params) {
|
|
148
|
+
const abortController = new AbortController();
|
|
149
|
+
const { signal, cleanUpSignals } = params.signal ? combineAbortSignals([params.signal, abortController.signal]) : {
|
|
150
|
+
signal: abortController.signal,
|
|
151
|
+
cleanUpSignals: () => {}
|
|
152
|
+
};
|
|
153
|
+
try {
|
|
154
|
+
const res = await fetchWithTimeout(params.url, {
|
|
155
|
+
method: params.method ?? "GET",
|
|
156
|
+
headers: {
|
|
157
|
+
Accept: "text/event-stream",
|
|
158
|
+
...params.headers ?? {}
|
|
159
|
+
},
|
|
160
|
+
body: params.body,
|
|
161
|
+
signal
|
|
162
|
+
}, params.timeoutMs, params.fetchFn);
|
|
163
|
+
await ensureSuccessfulResponse(res, `SSE ${params.url}`);
|
|
164
|
+
const responseContentType = res.headers.get("content-type") ?? "";
|
|
165
|
+
if (!responseContentType.includes("text/event-stream")) throw new ReplaneError({
|
|
166
|
+
message: `Expected text/event-stream, got "${responseContentType}"`,
|
|
167
|
+
code: ReplaneErrorCode.ServerError
|
|
168
|
+
});
|
|
169
|
+
if (!res.body) throw new ReplaneError({
|
|
170
|
+
message: `Failed to fetch SSE ${params.url}: body is empty`,
|
|
171
|
+
code: ReplaneErrorCode.Unknown
|
|
172
|
+
});
|
|
173
|
+
if (params.onConnect) params.onConnect();
|
|
174
|
+
const decoded = res.body.pipeThrough(new TextDecoderStream());
|
|
175
|
+
const reader = decoded.getReader();
|
|
176
|
+
let buffer = "";
|
|
177
|
+
try {
|
|
178
|
+
while (true) {
|
|
179
|
+
const { value, done } = await reader.read();
|
|
180
|
+
if (done) break;
|
|
181
|
+
buffer += value;
|
|
182
|
+
const frames = buffer.split(/\r?\n\r?\n/);
|
|
183
|
+
buffer = frames.pop() ?? "";
|
|
184
|
+
for (const frame of frames) {
|
|
185
|
+
const dataLines = [];
|
|
186
|
+
let comment = null;
|
|
187
|
+
for (const rawLine of frame.split(/\r?\n/)) {
|
|
188
|
+
if (!rawLine) continue;
|
|
189
|
+
if (rawLine.startsWith(":")) {
|
|
190
|
+
comment = rawLine.slice(1);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (rawLine.startsWith(SSE_DATA_PREFIX)) {
|
|
194
|
+
const line = rawLine.slice(SSE_DATA_PREFIX.length).replace(/^\s/, "");
|
|
195
|
+
dataLines.push(line);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (dataLines.length) {
|
|
199
|
+
const data = dataLines.join("\n");
|
|
200
|
+
yield {
|
|
201
|
+
type: "data",
|
|
202
|
+
data
|
|
203
|
+
};
|
|
204
|
+
} else if (comment !== null) yield {
|
|
205
|
+
type: "comment",
|
|
206
|
+
comment
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
} finally {
|
|
211
|
+
try {
|
|
212
|
+
await reader.cancel();
|
|
213
|
+
} catch {}
|
|
214
|
+
abortController.abort();
|
|
122
215
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
if (typeof contextValue === "string") {
|
|
127
|
-
if (typeof expectedValue === "number" || typeof expectedValue === "boolean") return String(expectedValue);
|
|
128
|
-
return expectedValue;
|
|
216
|
+
} finally {
|
|
217
|
+
cleanUpSignals();
|
|
129
218
|
}
|
|
130
|
-
return expectedValue;
|
|
131
|
-
}
|
|
132
|
-
async function delay(ms) {
|
|
133
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
134
|
-
}
|
|
135
|
-
async function retryDelay(averageDelay) {
|
|
136
|
-
const jitter = averageDelay / 5;
|
|
137
|
-
const delayMs = averageDelay + Math.random() * jitter - jitter / 2;
|
|
138
|
-
await delay(delayMs);
|
|
139
219
|
}
|
|
220
|
+
|
|
221
|
+
//#endregion
|
|
222
|
+
//#region src/storage.ts
|
|
223
|
+
const SUPPORTED_REPLICATION_STREAM_RECORD_TYPES = Object.keys({
|
|
224
|
+
config_change: true,
|
|
225
|
+
init: true
|
|
226
|
+
});
|
|
227
|
+
/**
|
|
228
|
+
* Remote storage implementation that connects to the Replane server
|
|
229
|
+
* and streams config updates via SSE.
|
|
230
|
+
*/
|
|
140
231
|
var ReplaneRemoteStorage = class {
|
|
141
232
|
closeController = new AbortController();
|
|
233
|
+
/**
|
|
234
|
+
* Start a replication stream that yields config updates.
|
|
235
|
+
* This method never throws - it retries on failure with exponential backoff.
|
|
236
|
+
*/
|
|
142
237
|
async *startReplicationStream(options) {
|
|
143
238
|
const { signal, cleanUpSignals } = combineAbortSignals([this.closeController.signal, options.signal]);
|
|
144
239
|
try {
|
|
@@ -195,8 +290,8 @@ var ReplaneRemoteStorage = class {
|
|
|
195
290
|
});
|
|
196
291
|
for await (const sseEvent of rawEvents) {
|
|
197
292
|
resetInactivityTimer();
|
|
198
|
-
if (sseEvent.type === "
|
|
199
|
-
const event = JSON.parse(sseEvent.
|
|
293
|
+
if (sseEvent.type === "comment") continue;
|
|
294
|
+
const event = JSON.parse(sseEvent.data);
|
|
200
295
|
if (typeof event === "object" && event !== null && "type" in event && typeof event.type === "string" && SUPPORTED_REPLICATION_STREAM_RECORD_TYPES.includes(event.type)) yield event;
|
|
201
296
|
}
|
|
202
297
|
} finally {
|
|
@@ -204,6 +299,9 @@ var ReplaneRemoteStorage = class {
|
|
|
204
299
|
cleanUpSignals();
|
|
205
300
|
}
|
|
206
301
|
}
|
|
302
|
+
/**
|
|
303
|
+
* Close the storage and abort any active connections
|
|
304
|
+
*/
|
|
207
305
|
close() {
|
|
208
306
|
this.closeController.abort();
|
|
209
307
|
}
|
|
@@ -214,105 +312,224 @@ var ReplaneRemoteStorage = class {
|
|
|
214
312
|
return `${options.baseUrl}/api${path}`;
|
|
215
313
|
}
|
|
216
314
|
};
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
315
|
+
|
|
316
|
+
//#endregion
|
|
317
|
+
//#region src/hash.ts
|
|
318
|
+
/**
|
|
319
|
+
* FNV-1a 32-bit hash function
|
|
320
|
+
*
|
|
321
|
+
* FNV (Fowler–Noll–Vo) is a non-cryptographic hash function known for its
|
|
322
|
+
* speed and good distribution. This implementation uses the FNV-1a variant
|
|
323
|
+
* which XORs before multiplying for better avalanche characteristics.
|
|
324
|
+
*
|
|
325
|
+
* @param input - The string to hash
|
|
326
|
+
* @returns A 32-bit unsigned integer hash value
|
|
327
|
+
*/
|
|
328
|
+
function fnv1a32(input) {
|
|
329
|
+
const encoder = new TextEncoder();
|
|
330
|
+
const bytes = encoder.encode(input);
|
|
331
|
+
let hash = 2166136261;
|
|
332
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
333
|
+
hash ^= bytes[i];
|
|
334
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
335
|
+
}
|
|
336
|
+
return hash >>> 0;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Convert FNV-1a hash to [0, 1) for bucketing.
|
|
340
|
+
*
|
|
341
|
+
* This is useful for percentage-based segmentation where you need
|
|
342
|
+
* to deterministically assign a value to a bucket based on a string input.
|
|
343
|
+
*
|
|
344
|
+
* @param input - The string to hash
|
|
345
|
+
* @returns A number in the range [0, 1)
|
|
346
|
+
*/
|
|
347
|
+
function fnv1a32ToUnit(input) {
|
|
348
|
+
const h = fnv1a32(input);
|
|
349
|
+
return h / 2 ** 32;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
//#endregion
|
|
353
|
+
//#region src/evaluation.ts
|
|
354
|
+
/**
|
|
355
|
+
* Evaluate config overrides based on context.
|
|
356
|
+
* Returns the first matching override's value, or the base value if no override matches.
|
|
357
|
+
*
|
|
358
|
+
* @param baseValue - The default value to return if no override matches
|
|
359
|
+
* @param overrides - Array of overrides to evaluate
|
|
360
|
+
* @param context - The context to evaluate conditions against
|
|
361
|
+
* @param logger - Logger for warnings
|
|
362
|
+
* @returns The evaluated value
|
|
363
|
+
*/
|
|
364
|
+
function evaluateOverrides(baseValue, overrides, context, logger) {
|
|
365
|
+
for (const override of overrides) {
|
|
366
|
+
let overrideResult = "matched";
|
|
367
|
+
const results = override.conditions.map((c) => evaluateCondition(c, context, logger));
|
|
368
|
+
if (results.some((r) => r === "not_matched")) overrideResult = "not_matched";
|
|
369
|
+
else if (results.some((r) => r === "unknown")) overrideResult = "unknown";
|
|
370
|
+
if (overrideResult === "matched") return override.value;
|
|
371
|
+
}
|
|
372
|
+
return baseValue;
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Evaluate a single condition against a context.
|
|
376
|
+
*
|
|
377
|
+
* @param condition - The condition to evaluate
|
|
378
|
+
* @param context - The context to evaluate against
|
|
379
|
+
* @param logger - Logger for warnings
|
|
380
|
+
* @returns The evaluation result
|
|
381
|
+
*/
|
|
382
|
+
function evaluateCondition(condition, context, logger) {
|
|
383
|
+
const operator = condition.operator;
|
|
384
|
+
if (operator === "and") {
|
|
385
|
+
const results = condition.conditions.map((c) => evaluateCondition(c, context, logger));
|
|
386
|
+
if (results.some((r) => r === "not_matched")) return "not_matched";
|
|
387
|
+
if (results.some((r) => r === "unknown")) return "unknown";
|
|
388
|
+
return "matched";
|
|
389
|
+
}
|
|
390
|
+
if (operator === "or") {
|
|
391
|
+
const results = condition.conditions.map((c) => evaluateCondition(c, context, logger));
|
|
392
|
+
if (results.some((r) => r === "matched")) return "matched";
|
|
393
|
+
if (results.some((r) => r === "unknown")) return "unknown";
|
|
394
|
+
return "not_matched";
|
|
395
|
+
}
|
|
396
|
+
if (operator === "not") {
|
|
397
|
+
const result = evaluateCondition(condition.condition, context, logger);
|
|
398
|
+
if (result === "matched") return "not_matched";
|
|
399
|
+
if (result === "not_matched") return "matched";
|
|
400
|
+
return "unknown";
|
|
401
|
+
}
|
|
402
|
+
if (operator === "segmentation") {
|
|
403
|
+
const contextValue$1 = context[condition.property];
|
|
404
|
+
if (contextValue$1 === void 0 || contextValue$1 === null) return "unknown";
|
|
405
|
+
const hashInput = String(contextValue$1) + condition.seed;
|
|
406
|
+
const unitValue = fnv1a32ToUnit(hashInput);
|
|
407
|
+
return unitValue >= condition.fromPercentage / 100 && unitValue < condition.toPercentage / 100 ? "matched" : "not_matched";
|
|
408
|
+
}
|
|
409
|
+
const property = condition.property;
|
|
410
|
+
const contextValue = context[property];
|
|
411
|
+
const expectedValue = condition.value;
|
|
412
|
+
if (contextValue === void 0) return "unknown";
|
|
413
|
+
const castedValue = castToContextType(expectedValue, contextValue);
|
|
414
|
+
switch (operator) {
|
|
415
|
+
case "equals": return contextValue === castedValue ? "matched" : "not_matched";
|
|
416
|
+
case "in":
|
|
417
|
+
if (!Array.isArray(castedValue)) return "unknown";
|
|
418
|
+
return castedValue.includes(contextValue) ? "matched" : "not_matched";
|
|
419
|
+
case "not_in":
|
|
420
|
+
if (!Array.isArray(castedValue)) return "unknown";
|
|
421
|
+
return !castedValue.includes(contextValue) ? "matched" : "not_matched";
|
|
422
|
+
case "less_than":
|
|
423
|
+
if (typeof contextValue === "number" && typeof castedValue === "number") return contextValue < castedValue ? "matched" : "not_matched";
|
|
424
|
+
if (typeof contextValue === "string" && typeof castedValue === "string") return contextValue < castedValue ? "matched" : "not_matched";
|
|
425
|
+
return "not_matched";
|
|
426
|
+
case "less_than_or_equal":
|
|
427
|
+
if (typeof contextValue === "number" && typeof castedValue === "number") return contextValue <= castedValue ? "matched" : "not_matched";
|
|
428
|
+
if (typeof contextValue === "string" && typeof castedValue === "string") return contextValue <= castedValue ? "matched" : "not_matched";
|
|
429
|
+
return "not_matched";
|
|
430
|
+
case "greater_than":
|
|
431
|
+
if (typeof contextValue === "number" && typeof castedValue === "number") return contextValue > castedValue ? "matched" : "not_matched";
|
|
432
|
+
if (typeof contextValue === "string" && typeof castedValue === "string") return contextValue > castedValue ? "matched" : "not_matched";
|
|
433
|
+
return "not_matched";
|
|
434
|
+
case "greater_than_or_equal":
|
|
435
|
+
if (typeof contextValue === "number" && typeof castedValue === "number") return contextValue >= castedValue ? "matched" : "not_matched";
|
|
436
|
+
if (typeof contextValue === "string" && typeof castedValue === "string") return contextValue >= castedValue ? "matched" : "not_matched";
|
|
437
|
+
return "not_matched";
|
|
438
|
+
default:
|
|
439
|
+
warnNever(operator, logger, `Unexpected operator: ${operator}`);
|
|
440
|
+
return "unknown";
|
|
236
441
|
}
|
|
237
|
-
}
|
|
442
|
+
}
|
|
238
443
|
/**
|
|
239
|
-
*
|
|
240
|
-
* Usage:
|
|
241
|
-
* const client = await createReplaneClient({ sdkKey: 'your-sdk-key', baseUrl: 'https://app.replane.dev' })
|
|
242
|
-
* const value = client.getConfig('my-config')
|
|
444
|
+
* Helper to warn about exhaustive check failures
|
|
243
445
|
*/
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
return await _createReplaneClient(toFinalOptions(sdkOptions), storage);
|
|
446
|
+
function warnNever(value, logger, message) {
|
|
447
|
+
logger.warn(message, { value });
|
|
247
448
|
}
|
|
248
449
|
/**
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
450
|
+
* Cast expected value to match context value type.
|
|
451
|
+
* This enables loose matching between different types (e.g., "25" matches 25).
|
|
452
|
+
*
|
|
453
|
+
* @param expectedValue - The value from the condition
|
|
454
|
+
* @param contextValue - The value from the context
|
|
455
|
+
* @returns The expected value cast to match the context value's type
|
|
253
456
|
*/
|
|
254
|
-
function
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
|
|
457
|
+
function castToContextType(expectedValue, contextValue) {
|
|
458
|
+
if (typeof contextValue === "number") {
|
|
459
|
+
if (typeof expectedValue === "string") {
|
|
460
|
+
const num = Number(expectedValue);
|
|
461
|
+
return isNaN(num) ? expectedValue : num;
|
|
462
|
+
}
|
|
463
|
+
return expectedValue;
|
|
464
|
+
}
|
|
465
|
+
if (typeof contextValue === "boolean") {
|
|
466
|
+
if (typeof expectedValue === "string") {
|
|
467
|
+
if (expectedValue === "true") return true;
|
|
468
|
+
if (expectedValue === "false") return false;
|
|
469
|
+
}
|
|
470
|
+
if (typeof expectedValue === "number") return expectedValue !== 0;
|
|
471
|
+
return expectedValue;
|
|
472
|
+
}
|
|
473
|
+
if (typeof contextValue === "string") {
|
|
474
|
+
if (typeof expectedValue === "number" || typeof expectedValue === "boolean") return String(expectedValue);
|
|
475
|
+
return expectedValue;
|
|
476
|
+
}
|
|
477
|
+
return expectedValue;
|
|
269
478
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
479
|
+
|
|
480
|
+
//#endregion
|
|
481
|
+
//#region src/client.ts
|
|
482
|
+
/**
|
|
483
|
+
* Creates the core client logic shared between createReplaneClient and restoreReplaneClient
|
|
484
|
+
*/
|
|
485
|
+
function createClientCore(options) {
|
|
486
|
+
const { initialConfigs, context, logger, storage, streamOptions, requiredConfigs } = options;
|
|
487
|
+
const configs = new Map(initialConfigs.map((config) => [config.name, config]));
|
|
273
488
|
const clientReady = new Deferred();
|
|
274
489
|
const configSubscriptions = new Map();
|
|
275
490
|
const clientSubscriptions = new Set();
|
|
276
|
-
|
|
491
|
+
function processConfigUpdates(updatedConfigs) {
|
|
492
|
+
for (const config of updatedConfigs) {
|
|
493
|
+
configs.set(config.name, {
|
|
494
|
+
name: config.name,
|
|
495
|
+
overrides: config.overrides,
|
|
496
|
+
value: config.value
|
|
497
|
+
});
|
|
498
|
+
for (const callback of clientSubscriptions) callback({
|
|
499
|
+
name: config.name,
|
|
500
|
+
value: config.value
|
|
501
|
+
});
|
|
502
|
+
for (const callback of configSubscriptions.get(config.name) ?? []) callback({
|
|
503
|
+
name: config.name,
|
|
504
|
+
value: config.value
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
async function startStreaming() {
|
|
509
|
+
if (!storage || !streamOptions) return;
|
|
277
510
|
try {
|
|
278
511
|
const replicationStream = storage.startReplicationStream({
|
|
279
|
-
...
|
|
512
|
+
...streamOptions,
|
|
280
513
|
getBody: () => ({
|
|
281
514
|
currentConfigs: [...configs.values()].map((config) => ({
|
|
282
515
|
name: config.name,
|
|
283
516
|
overrides: config.overrides,
|
|
284
|
-
version: config.version,
|
|
285
517
|
value: config.value
|
|
286
518
|
})),
|
|
287
|
-
requiredConfigs
|
|
519
|
+
requiredConfigs
|
|
288
520
|
})
|
|
289
521
|
});
|
|
290
522
|
for await (const event of replicationStream) {
|
|
291
|
-
const updatedConfigs = event.type === "config_change" ? [event] : event.configs;
|
|
292
|
-
|
|
293
|
-
if (config.version <= (configs.get(config.name)?.version ?? -1)) continue;
|
|
294
|
-
configs.set(config.name, {
|
|
295
|
-
name: config.name,
|
|
296
|
-
overrides: config.overrides,
|
|
297
|
-
version: config.version,
|
|
298
|
-
value: config.value
|
|
299
|
-
});
|
|
300
|
-
for (const callback of clientSubscriptions) callback({
|
|
301
|
-
name: config.name,
|
|
302
|
-
value: config.value
|
|
303
|
-
});
|
|
304
|
-
for (const callback of configSubscriptions.get(config.name) ?? []) callback({
|
|
305
|
-
name: config.name,
|
|
306
|
-
value: config.value
|
|
307
|
-
});
|
|
308
|
-
}
|
|
523
|
+
const updatedConfigs = event.type === "config_change" ? [event.config] : event.configs;
|
|
524
|
+
processConfigUpdates(updatedConfigs);
|
|
309
525
|
clientReady.resolve();
|
|
310
526
|
}
|
|
311
527
|
} catch (error) {
|
|
312
|
-
|
|
528
|
+
logger.error("Replane: error in SSE connection:", error);
|
|
313
529
|
clientReady.reject(error);
|
|
530
|
+
throw error;
|
|
314
531
|
}
|
|
315
|
-
}
|
|
532
|
+
}
|
|
316
533
|
function get(configName, getConfigOptions = {}) {
|
|
317
534
|
const config = configs.get(String(configName));
|
|
318
535
|
if (config === void 0) throw new ReplaneError({
|
|
@@ -321,11 +538,11 @@ async function _createReplaneClient(sdkOptions, storage) {
|
|
|
321
538
|
});
|
|
322
539
|
try {
|
|
323
540
|
return evaluateOverrides(config.value, config.overrides, {
|
|
324
|
-
...
|
|
541
|
+
...context,
|
|
325
542
|
...getConfigOptions?.context ?? {}
|
|
326
|
-
},
|
|
543
|
+
}, logger);
|
|
327
544
|
} catch (error) {
|
|
328
|
-
|
|
545
|
+
logger.error(`Replane: error evaluating overrides for config ${String(configName)}:`, error);
|
|
329
546
|
return config.value;
|
|
330
547
|
}
|
|
331
548
|
}
|
|
@@ -355,10 +572,157 @@ async function _createReplaneClient(sdkOptions, storage) {
|
|
|
355
572
|
if (configSubscriptions.get(configName)?.size === 0) configSubscriptions.delete(configName);
|
|
356
573
|
};
|
|
357
574
|
};
|
|
358
|
-
const
|
|
575
|
+
const getSnapshot = () => ({
|
|
576
|
+
configs: [...configs.values()].map((config) => ({
|
|
577
|
+
name: config.name,
|
|
578
|
+
value: config.value,
|
|
579
|
+
overrides: config.overrides.map((override) => ({
|
|
580
|
+
name: override.name,
|
|
581
|
+
conditions: override.conditions,
|
|
582
|
+
value: override.value
|
|
583
|
+
}))
|
|
584
|
+
})),
|
|
585
|
+
context
|
|
586
|
+
});
|
|
587
|
+
const close = () => storage?.close();
|
|
588
|
+
const client = {
|
|
589
|
+
get,
|
|
590
|
+
subscribe,
|
|
591
|
+
getSnapshot,
|
|
592
|
+
close
|
|
593
|
+
};
|
|
594
|
+
return {
|
|
595
|
+
client,
|
|
596
|
+
configs,
|
|
597
|
+
startStreaming,
|
|
598
|
+
clientReady
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Create a Replane client bound to an SDK key.
|
|
603
|
+
*
|
|
604
|
+
* @example
|
|
605
|
+
* ```typescript
|
|
606
|
+
* const client = await createReplaneClient({
|
|
607
|
+
* sdkKey: 'your-sdk-key',
|
|
608
|
+
* baseUrl: 'https://app.replane.dev'
|
|
609
|
+
* });
|
|
610
|
+
* const value = client.get('my-config');
|
|
611
|
+
* ```
|
|
612
|
+
*/
|
|
613
|
+
async function createReplaneClient(sdkOptions) {
|
|
614
|
+
const storage = new ReplaneRemoteStorage();
|
|
615
|
+
return await createReplaneClientInternal(toFinalOptions(sdkOptions), storage);
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Create a Replane client that uses in-memory storage.
|
|
619
|
+
* Useful for testing or when you have static config values.
|
|
620
|
+
*
|
|
621
|
+
* @example
|
|
622
|
+
* ```typescript
|
|
623
|
+
* const client = createInMemoryReplaneClient({ 'my-config': 123 });
|
|
624
|
+
* const value = client.get('my-config'); // 123
|
|
625
|
+
* ```
|
|
626
|
+
*/
|
|
627
|
+
function createInMemoryReplaneClient(initialData) {
|
|
628
|
+
return {
|
|
629
|
+
get: (configName) => {
|
|
630
|
+
const config = initialData[configName];
|
|
631
|
+
if (config === void 0) throw new ReplaneError({
|
|
632
|
+
message: `Config not found: ${String(configName)}`,
|
|
633
|
+
code: ReplaneErrorCode.NotFound
|
|
634
|
+
});
|
|
635
|
+
return config;
|
|
636
|
+
},
|
|
637
|
+
subscribe: () => {
|
|
638
|
+
return () => {};
|
|
639
|
+
},
|
|
640
|
+
getSnapshot: () => ({ configs: Object.entries(initialData).map(([name, value]) => ({
|
|
641
|
+
name,
|
|
642
|
+
value,
|
|
643
|
+
overrides: []
|
|
644
|
+
})) }),
|
|
645
|
+
close: () => {}
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Restore a Replane client from a snapshot.
|
|
650
|
+
* This is useful for SSR/hydration scenarios where the server has already fetched configs.
|
|
651
|
+
*
|
|
652
|
+
* @example
|
|
653
|
+
* ```typescript
|
|
654
|
+
* // On the server:
|
|
655
|
+
* const serverClient = await createReplaneClient({ ... });
|
|
656
|
+
* const snapshot = serverClient.getSnapshot();
|
|
657
|
+
* // Pass snapshot to client via props/serialization
|
|
658
|
+
*
|
|
659
|
+
* // On the client:
|
|
660
|
+
* const client = restoreReplaneClient({
|
|
661
|
+
* snapshot,
|
|
662
|
+
* connection: { sdkKey, baseUrl }
|
|
663
|
+
* });
|
|
664
|
+
* const value = client.get('my-config');
|
|
665
|
+
* ```
|
|
666
|
+
*/
|
|
667
|
+
function restoreReplaneClient(options) {
|
|
668
|
+
const { snapshot, connection } = options;
|
|
669
|
+
const context = options.context ?? snapshot.context ?? {};
|
|
670
|
+
const logger = connection?.logger ?? console;
|
|
671
|
+
const initialConfigs = snapshot.configs.map((config) => ({
|
|
672
|
+
name: config.name,
|
|
673
|
+
value: config.value,
|
|
674
|
+
overrides: config.overrides
|
|
675
|
+
}));
|
|
676
|
+
let storage = null;
|
|
677
|
+
let streamOptions = null;
|
|
678
|
+
if (connection) {
|
|
679
|
+
storage = new ReplaneRemoteStorage();
|
|
680
|
+
streamOptions = {
|
|
681
|
+
sdkKey: connection.sdkKey,
|
|
682
|
+
baseUrl: connection.baseUrl.replace(/\/+$/, ""),
|
|
683
|
+
fetchFn: connection.fetchFn ?? globalThis.fetch.bind(globalThis),
|
|
684
|
+
requestTimeoutMs: connection.requestTimeoutMs ?? 2e3,
|
|
685
|
+
initializationTimeoutMs: 5e3,
|
|
686
|
+
inactivityTimeoutMs: connection.inactivityTimeoutMs ?? 3e4,
|
|
687
|
+
logger,
|
|
688
|
+
retryDelayMs: connection.retryDelayMs ?? 200,
|
|
689
|
+
context,
|
|
690
|
+
requiredConfigs: [],
|
|
691
|
+
fallbacks: []
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
const { client, startStreaming } = createClientCore({
|
|
695
|
+
initialConfigs,
|
|
696
|
+
context,
|
|
697
|
+
logger,
|
|
698
|
+
storage,
|
|
699
|
+
streamOptions,
|
|
700
|
+
requiredConfigs: []
|
|
701
|
+
});
|
|
702
|
+
if (storage && streamOptions) startStreaming().catch((error) => {
|
|
703
|
+
logger.error("Replane: error in restored client SSE connection:", error);
|
|
704
|
+
});
|
|
705
|
+
return client;
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Internal function to create a Replane client with the given options and storage
|
|
709
|
+
*/
|
|
710
|
+
async function createReplaneClientInternal(sdkOptions, storage) {
|
|
711
|
+
if (!sdkOptions.sdkKey) throw new Error("SDK key is required");
|
|
712
|
+
const { client, configs, startStreaming, clientReady } = createClientCore({
|
|
713
|
+
initialConfigs: sdkOptions.fallbacks,
|
|
714
|
+
context: sdkOptions.context,
|
|
715
|
+
logger: sdkOptions.logger,
|
|
716
|
+
storage,
|
|
717
|
+
streamOptions: sdkOptions,
|
|
718
|
+
requiredConfigs: sdkOptions.requiredConfigs
|
|
719
|
+
});
|
|
720
|
+
startStreaming().catch((error) => {
|
|
721
|
+
sdkOptions.logger.error("Replane: error initializing client:", error);
|
|
722
|
+
});
|
|
359
723
|
const initializationTimeoutId = setTimeout(() => {
|
|
360
724
|
if (sdkOptions.fallbacks.length === 0) {
|
|
361
|
-
close();
|
|
725
|
+
client.close();
|
|
362
726
|
clientReady.reject(new ReplaneError({
|
|
363
727
|
message: "Replane client initialization timed out",
|
|
364
728
|
code: ReplaneErrorCode.Timeout
|
|
@@ -368,7 +732,7 @@ async function _createReplaneClient(sdkOptions, storage) {
|
|
|
368
732
|
const missingRequiredConfigs = [];
|
|
369
733
|
for (const requiredConfigName of sdkOptions.requiredConfigs) if (!configs.has(requiredConfigName)) missingRequiredConfigs.push(requiredConfigName);
|
|
370
734
|
if (missingRequiredConfigs.length > 0) {
|
|
371
|
-
close();
|
|
735
|
+
client.close();
|
|
372
736
|
clientReady.reject(new ReplaneError({
|
|
373
737
|
message: `Required configs are missing: ${missingRequiredConfigs.join(", ")}`,
|
|
374
738
|
code: ReplaneErrorCode.NotFound
|
|
@@ -379,12 +743,11 @@ async function _createReplaneClient(sdkOptions, storage) {
|
|
|
379
743
|
}, sdkOptions.initializationTimeoutMs);
|
|
380
744
|
clientReady.promise.then(() => clearTimeout(initializationTimeoutId));
|
|
381
745
|
await clientReady.promise;
|
|
382
|
-
return
|
|
383
|
-
get,
|
|
384
|
-
subscribe,
|
|
385
|
-
close
|
|
386
|
-
};
|
|
746
|
+
return client;
|
|
387
747
|
}
|
|
748
|
+
/**
|
|
749
|
+
* Convert user options to final options with defaults
|
|
750
|
+
*/
|
|
388
751
|
function toFinalOptions(defaults) {
|
|
389
752
|
return {
|
|
390
753
|
sdkKey: defaults.sdkKey,
|
|
@@ -392,7 +755,7 @@ function toFinalOptions(defaults) {
|
|
|
392
755
|
fetchFn: defaults.fetchFn ?? globalThis.fetch.bind(globalThis),
|
|
393
756
|
requestTimeoutMs: defaults.requestTimeoutMs ?? 2e3,
|
|
394
757
|
initializationTimeoutMs: defaults.initializationTimeoutMs ?? 5e3,
|
|
395
|
-
inactivityTimeoutMs: defaults.inactivityTimeoutMs ??
|
|
758
|
+
inactivityTimeoutMs: defaults.inactivityTimeoutMs ?? 3e4,
|
|
396
759
|
logger: defaults.logger ?? console,
|
|
397
760
|
retryDelayMs: defaults.retryDelayMs ?? 200,
|
|
398
761
|
context: { ...defaults.context ?? {} },
|
|
@@ -405,146 +768,7 @@ function toFinalOptions(defaults) {
|
|
|
405
768
|
}))
|
|
406
769
|
};
|
|
407
770
|
}
|
|
408
|
-
async function fetchWithTimeout(input, init, timeoutMs, fetchFn) {
|
|
409
|
-
if (!fetchFn) throw new Error("Global fetch is not available. Provide options.fetchFn.");
|
|
410
|
-
if (!timeoutMs) return fetchFn(input, init);
|
|
411
|
-
const timeoutController = new AbortController();
|
|
412
|
-
const t = setTimeout(() => timeoutController.abort(), timeoutMs);
|
|
413
|
-
const { signal } = combineAbortSignals([init.signal, timeoutController.signal]);
|
|
414
|
-
try {
|
|
415
|
-
return await fetchFn(input, {
|
|
416
|
-
...init,
|
|
417
|
-
signal
|
|
418
|
-
});
|
|
419
|
-
} finally {
|
|
420
|
-
clearTimeout(t);
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
const SSE_DATA_PREFIX = "data:";
|
|
424
|
-
async function* fetchSse(params) {
|
|
425
|
-
const abortController = new AbortController();
|
|
426
|
-
const { signal, cleanUpSignals } = params.signal ? combineAbortSignals([params.signal, abortController.signal]) : {
|
|
427
|
-
signal: abortController.signal,
|
|
428
|
-
cleanUpSignals: () => {}
|
|
429
|
-
};
|
|
430
|
-
try {
|
|
431
|
-
const res = await fetchWithTimeout(params.url, {
|
|
432
|
-
method: params.method ?? "GET",
|
|
433
|
-
headers: {
|
|
434
|
-
Accept: "text/event-stream",
|
|
435
|
-
...params.headers ?? {}
|
|
436
|
-
},
|
|
437
|
-
body: params.body,
|
|
438
|
-
signal
|
|
439
|
-
}, params.timeoutMs, params.fetchFn);
|
|
440
|
-
await ensureSuccessfulResponse(res, `SSE ${params.url}`);
|
|
441
|
-
const responseContentType = res.headers.get("content-type") ?? "";
|
|
442
|
-
if (!responseContentType.includes("text/event-stream")) throw new ReplaneError({
|
|
443
|
-
message: `Expected text/event-stream, got "${responseContentType}"`,
|
|
444
|
-
code: ReplaneErrorCode.ServerError
|
|
445
|
-
});
|
|
446
|
-
if (!res.body) throw new ReplaneError({
|
|
447
|
-
message: `Failed to fetch SSE ${params.url}: body is empty`,
|
|
448
|
-
code: ReplaneErrorCode.Unknown
|
|
449
|
-
});
|
|
450
|
-
if (params.onConnect) params.onConnect();
|
|
451
|
-
const decoded = res.body.pipeThrough(new TextDecoderStream());
|
|
452
|
-
const reader = decoded.getReader();
|
|
453
|
-
let buffer = "";
|
|
454
|
-
try {
|
|
455
|
-
while (true) {
|
|
456
|
-
const { value, done } = await reader.read();
|
|
457
|
-
if (done) break;
|
|
458
|
-
buffer += value;
|
|
459
|
-
const frames = buffer.split(/\r?\n\r?\n/);
|
|
460
|
-
buffer = frames.pop() ?? "";
|
|
461
|
-
for (const frame of frames) {
|
|
462
|
-
const dataLines = [];
|
|
463
|
-
let isPing = false;
|
|
464
|
-
for (const rawLine of frame.split(/\r?\n/)) {
|
|
465
|
-
if (!rawLine) continue;
|
|
466
|
-
if (rawLine.startsWith(":")) {
|
|
467
|
-
isPing = true;
|
|
468
|
-
continue;
|
|
469
|
-
}
|
|
470
|
-
if (rawLine.startsWith(SSE_DATA_PREFIX)) {
|
|
471
|
-
const line = rawLine.slice(SSE_DATA_PREFIX.length).replace(/^\s/, "");
|
|
472
|
-
dataLines.push(line);
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
if (dataLines.length) {
|
|
476
|
-
const payload = dataLines.join("\n");
|
|
477
|
-
yield {
|
|
478
|
-
type: "data",
|
|
479
|
-
payload
|
|
480
|
-
};
|
|
481
|
-
} else if (isPing) yield { type: "ping" };
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
} finally {
|
|
485
|
-
try {
|
|
486
|
-
await reader.cancel();
|
|
487
|
-
} catch {}
|
|
488
|
-
abortController.abort();
|
|
489
|
-
}
|
|
490
|
-
} finally {
|
|
491
|
-
cleanUpSignals();
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
async function ensureSuccessfulResponse(response, message) {
|
|
495
|
-
if (response.status === 404) throw new ReplaneError({
|
|
496
|
-
message: `Not found: ${message}`,
|
|
497
|
-
code: ReplaneErrorCode.NotFound
|
|
498
|
-
});
|
|
499
|
-
if (response.status === 401) throw new ReplaneError({
|
|
500
|
-
message: `Unauthorized access: ${message}`,
|
|
501
|
-
code: ReplaneErrorCode.AuthError
|
|
502
|
-
});
|
|
503
|
-
if (response.status === 403) throw new ReplaneError({
|
|
504
|
-
message: `Forbidden access: ${message}`,
|
|
505
|
-
code: ReplaneErrorCode.Forbidden
|
|
506
|
-
});
|
|
507
|
-
if (!response.ok) {
|
|
508
|
-
let body;
|
|
509
|
-
try {
|
|
510
|
-
body = await response.text();
|
|
511
|
-
} catch {
|
|
512
|
-
body = "<unable to read response body>";
|
|
513
|
-
}
|
|
514
|
-
const code = response.status >= 500 ? ReplaneErrorCode.ServerError : response.status >= 400 ? ReplaneErrorCode.ClientError : ReplaneErrorCode.Unknown;
|
|
515
|
-
throw new ReplaneError({
|
|
516
|
-
message: `Fetch response isn't successful (${message}): ${response.status} ${response.statusText} - ${body}`,
|
|
517
|
-
code
|
|
518
|
-
});
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
function combineAbortSignals(signals) {
|
|
522
|
-
const controller = new AbortController();
|
|
523
|
-
const onAbort = () => {
|
|
524
|
-
controller.abort();
|
|
525
|
-
cleanUpSignals();
|
|
526
|
-
};
|
|
527
|
-
const cleanUpSignals = () => {
|
|
528
|
-
for (const s of signals) s?.removeEventListener("abort", onAbort);
|
|
529
|
-
};
|
|
530
|
-
for (const s of signals) s?.addEventListener("abort", onAbort, { once: true });
|
|
531
|
-
if (signals.some((s) => s?.aborted)) onAbort();
|
|
532
|
-
return {
|
|
533
|
-
signal: controller.signal,
|
|
534
|
-
cleanUpSignals
|
|
535
|
-
};
|
|
536
|
-
}
|
|
537
|
-
var Deferred = class {
|
|
538
|
-
promise;
|
|
539
|
-
resolve;
|
|
540
|
-
reject;
|
|
541
|
-
constructor() {
|
|
542
|
-
this.promise = new Promise((resolve, reject) => {
|
|
543
|
-
this.resolve = resolve;
|
|
544
|
-
this.reject = reject;
|
|
545
|
-
});
|
|
546
|
-
}
|
|
547
|
-
};
|
|
548
771
|
|
|
549
772
|
//#endregion
|
|
550
|
-
export { ReplaneError, createInMemoryReplaneClient, createReplaneClient };
|
|
773
|
+
export { ReplaneError, ReplaneErrorCode, createInMemoryReplaneClient, createReplaneClient, restoreReplaneClient };
|
|
774
|
+
//# sourceMappingURL=index.js.map
|