@configdirector/nuxt-sdk 0.1.0
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 +136 -0
- package/dist/module.d.mts +85 -0
- package/dist/module.json +8 -0
- package/dist/module.mjs +66 -0
- package/dist/runtime/app/composables/useConfigDirectorClient.d.mts +8 -0
- package/dist/runtime/app/composables/useConfigDirectorClient.d.ts +8 -0
- package/dist/runtime/app/composables/useConfigDirectorClient.mjs +8 -0
- package/dist/runtime/app/composables/useConfigDirectorContext.d.mts +12 -0
- package/dist/runtime/app/composables/useConfigDirectorContext.d.ts +12 -0
- package/dist/runtime/app/composables/useConfigDirectorContext.mjs +30 -0
- package/dist/runtime/app/composables/useConfigDirectorStatus.d.mts +10 -0
- package/dist/runtime/app/composables/useConfigDirectorStatus.d.ts +10 -0
- package/dist/runtime/app/composables/useConfigDirectorStatus.mjs +13 -0
- package/dist/runtime/app/composables/useConfigDirectorValue.d.mts +12 -0
- package/dist/runtime/app/composables/useConfigDirectorValue.d.ts +12 -0
- package/dist/runtime/app/composables/useConfigDirectorValue.mjs +19 -0
- package/dist/runtime/nitro/middleware.d.mts +130 -0
- package/dist/runtime/nitro/middleware.d.ts +130 -0
- package/dist/runtime/nitro/middleware.mjs +8 -0
- package/dist/runtime/nitro/plugin.d.mts +12 -0
- package/dist/runtime/nitro/plugin.d.ts +12 -0
- package/dist/runtime/nitro/plugin.mjs +1397 -0
- package/dist/runtime/plugin.client.d.mts +4 -0
- package/dist/runtime/plugin.client.mjs +1162 -0
- package/dist/runtime/plugin.server.d.mts +4 -0
- package/dist/runtime/plugin.server.mjs +106 -0
- package/dist/runtime/server/composables/useConfigDirectorClient.d.mts +6 -0
- package/dist/runtime/server/composables/useConfigDirectorClient.d.ts +6 -0
- package/dist/runtime/server/composables/useConfigDirectorClient.mjs +9 -0
- package/dist/runtime/types-CQWNx-gI.d.mts +34 -0
- package/dist/runtime/types-CkzBXJwA.d.mts +4 -0
- package/dist/runtime/types-D9MFb76Q.d.mts +85 -0
- package/dist/runtime/types-KSnINlzh.d.mts +135 -0
- package/package.json +76 -0
|
@@ -0,0 +1,1397 @@
|
|
|
1
|
+
import EventEmitter, { EventEmitter as EventEmitter$1 } from "node:events";
|
|
2
|
+
import { findByPointer } from "@jsonjoy.com/json-pointer";
|
|
3
|
+
import * as semver from "semver";
|
|
4
|
+
import { rapidhash } from "rapidhash-js";
|
|
5
|
+
import { defineNitroPlugin, useRuntimeConfig } from "nitropack/runtime";
|
|
6
|
+
import { consola } from "consola";
|
|
7
|
+
//#region ../shared/src/errors.ts
|
|
8
|
+
var ConfigDirectorConnectionError = class ConfigDirectorConnectionError extends Error {
|
|
9
|
+
name = "ConfigDirectorConnectionError";
|
|
10
|
+
status;
|
|
11
|
+
constructor(message, status) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.status = status;
|
|
14
|
+
Object.setPrototypeOf(this, ConfigDirectorConnectionError.prototype);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
var ConfigDirectorValidationError = class ConfigDirectorValidationError extends Error {
|
|
18
|
+
name = "ConfigDirectorValidationError";
|
|
19
|
+
constructor(message) {
|
|
20
|
+
super(message);
|
|
21
|
+
Object.setPrototypeOf(this, ConfigDirectorValidationError.prototype);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
var ConfigDirectorInitializationError = class ConfigDirectorInitializationError extends Error {
|
|
25
|
+
name = "ConfigDirectorInitializationError";
|
|
26
|
+
constructor(message) {
|
|
27
|
+
super(message);
|
|
28
|
+
Object.setPrototypeOf(this, ConfigDirectorInitializationError.prototype);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region ../eventsource/src/errors.ts
|
|
33
|
+
var StreamClosedError = class StreamClosedError extends Error {
|
|
34
|
+
name = "StreamClosedError";
|
|
35
|
+
constructor(message) {
|
|
36
|
+
super(message);
|
|
37
|
+
Object.setPrototypeOf(this, StreamClosedError.prototype);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
var MissingResponseBodyError = class MissingResponseBodyError extends Error {
|
|
41
|
+
name = "MissingResponseBodyError";
|
|
42
|
+
constructor(message) {
|
|
43
|
+
super(message);
|
|
44
|
+
Object.setPrototypeOf(this, MissingResponseBodyError.prototype);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
var InvalidOptionError = class InvalidOptionError extends Error {
|
|
48
|
+
name = "InvalidOptionError";
|
|
49
|
+
constructor(message) {
|
|
50
|
+
super(message);
|
|
51
|
+
Object.setPrototypeOf(this, InvalidOptionError.prototype);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
var ValueOutOfRangeError = class ValueOutOfRangeError extends Error {
|
|
55
|
+
name = "ValueOutOfRangeError";
|
|
56
|
+
constructor(message) {
|
|
57
|
+
super(message);
|
|
58
|
+
Object.setPrototypeOf(this, ValueOutOfRangeError.prototype);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
//#endregion
|
|
62
|
+
//#region ../eventsource/src/types.ts
|
|
63
|
+
let ReadyState = /* @__PURE__ */ function(ReadyState) {
|
|
64
|
+
ReadyState["OPEN"] = "open";
|
|
65
|
+
ReadyState["CLOSED"] = "closed";
|
|
66
|
+
ReadyState["CONNECTING"] = "connecting";
|
|
67
|
+
return ReadyState;
|
|
68
|
+
}({});
|
|
69
|
+
//#endregion
|
|
70
|
+
//#region ../eventsource/src/EventSourceParser.ts
|
|
71
|
+
var EventSourceParser = class {
|
|
72
|
+
isFirstChunk = true;
|
|
73
|
+
bufferedInput = "";
|
|
74
|
+
currentEvent = { data: "" };
|
|
75
|
+
lastEventId;
|
|
76
|
+
onEvent;
|
|
77
|
+
onRetry;
|
|
78
|
+
onComment;
|
|
79
|
+
constructor(options) {
|
|
80
|
+
this.currentEvent = { data: "" };
|
|
81
|
+
this.onEvent = options.onEvent ?? (() => {});
|
|
82
|
+
this.onRetry = options.onRetry;
|
|
83
|
+
this.onComment = options.onComment;
|
|
84
|
+
}
|
|
85
|
+
parse(chunk) {
|
|
86
|
+
let input = chunk;
|
|
87
|
+
if (this.isFirstChunk) {
|
|
88
|
+
this.isFirstChunk = false;
|
|
89
|
+
if (input.startsWith("")) input = input.slice(1);
|
|
90
|
+
else if (input.startsWith("")) input = input.slice(3);
|
|
91
|
+
}
|
|
92
|
+
const lines = this.scanLines(this.bufferedInput + input);
|
|
93
|
+
for (const line of lines) this.dispatchLine(line);
|
|
94
|
+
}
|
|
95
|
+
finish() {
|
|
96
|
+
this.bufferedInput = "";
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Scans `text` character by character to extract complete lines, recognizing
|
|
100
|
+
* CR, LF, and CRLF as line terminators per the SSE spec. Any unterminated
|
|
101
|
+
* trailing text is buffered and prepended to the next chunk.
|
|
102
|
+
*/
|
|
103
|
+
scanLines(text) {
|
|
104
|
+
const lines = [];
|
|
105
|
+
let lineStart = 0;
|
|
106
|
+
let i = 0;
|
|
107
|
+
while (i < text.length) {
|
|
108
|
+
const ch = text[i];
|
|
109
|
+
if (ch === "\r" || ch === "\n") {
|
|
110
|
+
lines.push(text.slice(lineStart, i));
|
|
111
|
+
if (ch === "\r" && text[i + 1] === "\n") i++;
|
|
112
|
+
i++;
|
|
113
|
+
lineStart = i;
|
|
114
|
+
} else i++;
|
|
115
|
+
}
|
|
116
|
+
this.bufferedInput = text.slice(lineStart);
|
|
117
|
+
return lines;
|
|
118
|
+
}
|
|
119
|
+
dispatchLine(line) {
|
|
120
|
+
if (line.startsWith(":")) {
|
|
121
|
+
this.onComment?.(this.extractValue(line, 1));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (line === "") {
|
|
125
|
+
this.emitEvent();
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
this.applyField(this.parseField(line));
|
|
129
|
+
}
|
|
130
|
+
emitEvent() {
|
|
131
|
+
const data = this.currentEvent.data.endsWith("\n") ? this.currentEvent.data.slice(0, -1) : this.currentEvent.data;
|
|
132
|
+
if (data !== "") this.onEvent({
|
|
133
|
+
...this.currentEvent,
|
|
134
|
+
id: this.lastEventId,
|
|
135
|
+
data
|
|
136
|
+
});
|
|
137
|
+
this.currentEvent = { data: "" };
|
|
138
|
+
}
|
|
139
|
+
applyField(parsed) {
|
|
140
|
+
switch (parsed.field) {
|
|
141
|
+
case "event":
|
|
142
|
+
this.currentEvent.type = parsed.value;
|
|
143
|
+
break;
|
|
144
|
+
case "data":
|
|
145
|
+
this.currentEvent.data += parsed.value + "\n";
|
|
146
|
+
break;
|
|
147
|
+
case "id":
|
|
148
|
+
if (!parsed.value.includes("\0")) this.lastEventId = parsed.value;
|
|
149
|
+
break;
|
|
150
|
+
case "retry":
|
|
151
|
+
if (/^\d+$/.test(parsed.value)) this.onRetry?.(Number.parseInt(parsed.value, 10));
|
|
152
|
+
break;
|
|
153
|
+
default: break;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
parseField(line) {
|
|
157
|
+
const colon = line.indexOf(":");
|
|
158
|
+
if (colon === -1) return {
|
|
159
|
+
field: line,
|
|
160
|
+
value: ""
|
|
161
|
+
};
|
|
162
|
+
return {
|
|
163
|
+
field: line.slice(0, colon),
|
|
164
|
+
value: this.extractValue(line, colon + 1)
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/** Returns the value portion of a line, stripping a single leading space per spec. */
|
|
168
|
+
extractValue(line, from) {
|
|
169
|
+
return line[from] === " " ? line.slice(from + 1) : line.slice(from);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
//#endregion
|
|
173
|
+
//#region ../eventsource/src/EventSourceClient.ts
|
|
174
|
+
var EventSourceClient = class {
|
|
175
|
+
DEFAULT_SHOULD_RECONNECT = () => true;
|
|
176
|
+
DEFAULT_CALCULATE_RECONNECT_DELAY = () => this.serverReconnectionTime;
|
|
177
|
+
url;
|
|
178
|
+
headers;
|
|
179
|
+
requestOptions;
|
|
180
|
+
lastEventId;
|
|
181
|
+
reconnectAttempt = 0;
|
|
182
|
+
abortController = new AbortController();
|
|
183
|
+
reconnectTimeout;
|
|
184
|
+
_readyState = ReadyState.CLOSED;
|
|
185
|
+
serverReconnectionTime = 2e3;
|
|
186
|
+
_fetch;
|
|
187
|
+
_onError;
|
|
188
|
+
_onMessage;
|
|
189
|
+
_onComment;
|
|
190
|
+
_onConnect;
|
|
191
|
+
_onDisconnect;
|
|
192
|
+
_shouldReconnect;
|
|
193
|
+
_calculateReconnectDelay;
|
|
194
|
+
constructor(options) {
|
|
195
|
+
this.url = options.url.toString();
|
|
196
|
+
this.lastEventId = options.lastEventId;
|
|
197
|
+
this.headers = {
|
|
198
|
+
Accept: "text/event-stream",
|
|
199
|
+
...options.headers
|
|
200
|
+
};
|
|
201
|
+
this.requestOptions = {
|
|
202
|
+
body: options.body,
|
|
203
|
+
method: options.method ?? "GET",
|
|
204
|
+
mode: options.mode,
|
|
205
|
+
credentials: options.credentials,
|
|
206
|
+
redirect: options.redirect,
|
|
207
|
+
referrer: options.referrer,
|
|
208
|
+
referrerPolicy: options.referrerPolicy
|
|
209
|
+
};
|
|
210
|
+
this._fetch = options.fetch ?? ((url, init) => fetch(url, init));
|
|
211
|
+
this._onError = this.resolveCallback(options.onError, () => {}, "onError");
|
|
212
|
+
this._onConnect = this.resolveCallback(options.onConnect, () => {}, "onConnect");
|
|
213
|
+
this._onDisconnect = this.resolveCallback(options.onDisconnect, () => {}, "onDisconnect");
|
|
214
|
+
this._onMessage = this.resolveCallback(options.onMessage, () => {}, "onMessage");
|
|
215
|
+
this._onComment = this.resolveCallback(options.onComment, () => {}, "onComment");
|
|
216
|
+
this._shouldReconnect = this.resolveCallback(options.shouldReconnect, this.DEFAULT_SHOULD_RECONNECT, "shouldReconnect");
|
|
217
|
+
this._calculateReconnectDelay = this.resolveCallback(options.calculateReconnectDelay, this.DEFAULT_CALCULATE_RECONNECT_DELAY, "calculateReconnectDelay");
|
|
218
|
+
}
|
|
219
|
+
set onError(value) {
|
|
220
|
+
this._onError = this.resolveCallback(value, () => {}, "onError");
|
|
221
|
+
}
|
|
222
|
+
set onMessage(value) {
|
|
223
|
+
this._onMessage = this.resolveCallback(value, () => {}, "onMessage");
|
|
224
|
+
}
|
|
225
|
+
set onComment(value) {
|
|
226
|
+
this._onComment = this.resolveCallback(value, () => {}, "onComment");
|
|
227
|
+
}
|
|
228
|
+
set onConnect(value) {
|
|
229
|
+
this._onConnect = this.resolveCallback(value, () => {}, "onConnect");
|
|
230
|
+
}
|
|
231
|
+
set onDisconnect(value) {
|
|
232
|
+
this._onDisconnect = this.resolveCallback(value, () => {}, "onDisconnect");
|
|
233
|
+
}
|
|
234
|
+
set shouldReconnect(value) {
|
|
235
|
+
this._shouldReconnect = this.resolveCallback(value, this.DEFAULT_SHOULD_RECONNECT, "shouldReconnect");
|
|
236
|
+
}
|
|
237
|
+
set calculateReconnectDelay(value) {
|
|
238
|
+
this._calculateReconnectDelay = this.resolveCallback(value, this.DEFAULT_CALCULATE_RECONNECT_DELAY, "shouldReconnect");
|
|
239
|
+
}
|
|
240
|
+
get readyState() {
|
|
241
|
+
return this._readyState;
|
|
242
|
+
}
|
|
243
|
+
connect() {
|
|
244
|
+
if (this._readyState !== ReadyState.CLOSED) return;
|
|
245
|
+
this.reconnectAttempt = 0;
|
|
246
|
+
this.initiateConnection();
|
|
247
|
+
}
|
|
248
|
+
close() {
|
|
249
|
+
clearTimeout(this.reconnectTimeout);
|
|
250
|
+
this._readyState = ReadyState.CLOSED;
|
|
251
|
+
this.abortController.abort();
|
|
252
|
+
}
|
|
253
|
+
initiateConnection() {
|
|
254
|
+
this.abortController = new AbortController();
|
|
255
|
+
this._readyState = ReadyState.CONNECTING;
|
|
256
|
+
this._fetch(this.url, {
|
|
257
|
+
...this.requestOptions,
|
|
258
|
+
headers: this.buildRequestHeaders(),
|
|
259
|
+
signal: this.abortController.signal,
|
|
260
|
+
cache: "no-store"
|
|
261
|
+
}).then((response) => this.handleFetchResponse(response)).catch((error) => {
|
|
262
|
+
if (error.name === "AbortError" || this.abortController.signal.aborted) {
|
|
263
|
+
this.close();
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
this._onError(error);
|
|
267
|
+
this.scheduleReconnect({ error });
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
async handleFetchResponse(response) {
|
|
271
|
+
const { body, status } = response;
|
|
272
|
+
if (status === 204) {
|
|
273
|
+
this.disconnected();
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (status >= 400) {
|
|
277
|
+
this.scheduleReconnect({ status });
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (!body) throw new MissingResponseBodyError("The server response did not have a body");
|
|
281
|
+
this._readyState = ReadyState.OPEN;
|
|
282
|
+
this._onConnect();
|
|
283
|
+
this.reconnectAttempt = 0;
|
|
284
|
+
const parser = new EventSourceParser({
|
|
285
|
+
onEvent: (event) => {
|
|
286
|
+
if (typeof event.id === "string") this.lastEventId = event.id;
|
|
287
|
+
this._onMessage(event);
|
|
288
|
+
},
|
|
289
|
+
onComment: (comment) => this._onComment(comment),
|
|
290
|
+
onRetry: (retryDelay) => this.serverReconnectionTime = retryDelay
|
|
291
|
+
});
|
|
292
|
+
await this.readResponseStream(body, parser);
|
|
293
|
+
this.scheduleReconnect({
|
|
294
|
+
status,
|
|
295
|
+
error: new StreamClosedError("The server response stream was closed")
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
async readResponseStream(body, parser) {
|
|
299
|
+
const reader = body.getReader();
|
|
300
|
+
const decoder = new TextDecoder();
|
|
301
|
+
let done = false;
|
|
302
|
+
while (!done) {
|
|
303
|
+
const result = await reader.read();
|
|
304
|
+
if (result.value) parser.parse(decoder.decode(result.value, { stream: true }));
|
|
305
|
+
done = result.done;
|
|
306
|
+
}
|
|
307
|
+
parser.finish();
|
|
308
|
+
}
|
|
309
|
+
disconnected() {
|
|
310
|
+
clearTimeout(this.reconnectTimeout);
|
|
311
|
+
this._readyState = ReadyState.CLOSED;
|
|
312
|
+
this._onDisconnect();
|
|
313
|
+
}
|
|
314
|
+
buildRequestHeaders() {
|
|
315
|
+
const lastEventIdHeader = this.lastEventId ? { "Last-Event-ID": this.lastEventId } : void 0;
|
|
316
|
+
return {
|
|
317
|
+
...this.headers,
|
|
318
|
+
...lastEventIdHeader
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
scheduleReconnect(state) {
|
|
322
|
+
if (this.abortController.signal.aborted) return;
|
|
323
|
+
clearTimeout(this.reconnectTimeout);
|
|
324
|
+
this.reconnectAttempt += 1;
|
|
325
|
+
const reconnectionState = {
|
|
326
|
+
attempt: this.reconnectAttempt,
|
|
327
|
+
status: state.status,
|
|
328
|
+
error: state.error,
|
|
329
|
+
serverReconnectionTime: this.serverReconnectionTime
|
|
330
|
+
};
|
|
331
|
+
if (!this._shouldReconnect(reconnectionState)) {
|
|
332
|
+
this.disconnected();
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
this._readyState = ReadyState.CONNECTING;
|
|
336
|
+
let delay = this._calculateReconnectDelay(reconnectionState);
|
|
337
|
+
if (!Number.isFinite(delay) || delay < 1 || delay > 1e3 * 60 * 60) {
|
|
338
|
+
this._onError(new ValueOutOfRangeError(`The calculated reconnect delay is out of range: ${delay}. Defaulting to ${this.serverReconnectionTime}`));
|
|
339
|
+
delay = this.serverReconnectionTime;
|
|
340
|
+
}
|
|
341
|
+
this.reconnectTimeout = setTimeout(() => this.initiateConnection(), delay);
|
|
342
|
+
}
|
|
343
|
+
resolveCallback(provided, fallback, optionName) {
|
|
344
|
+
if (provided == null) return fallback;
|
|
345
|
+
if (typeof provided !== "function") throw new InvalidOptionError(`${optionName} must be a function`);
|
|
346
|
+
return provided;
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
//#endregion
|
|
350
|
+
//#region ../eventsource/src/index.ts
|
|
351
|
+
const createEventSourceClient = (options) => {
|
|
352
|
+
return new EventSourceClient(options);
|
|
353
|
+
};
|
|
354
|
+
//#endregion
|
|
355
|
+
//#region ../js-server-sdk/src/StreamingTransport.ts
|
|
356
|
+
const MAX_EXPONENTIAL_DELAY = 9;
|
|
357
|
+
var StreamingTransport = class {
|
|
358
|
+
logger;
|
|
359
|
+
eventSource;
|
|
360
|
+
eventEmitter = new EventEmitter$1();
|
|
361
|
+
url;
|
|
362
|
+
constructor(options) {
|
|
363
|
+
this.options = options;
|
|
364
|
+
this.options = options;
|
|
365
|
+
this.logger = options.logger;
|
|
366
|
+
this.url = new URL("server/sse/v1", options.baseUrl);
|
|
367
|
+
}
|
|
368
|
+
async connect(timeout) {
|
|
369
|
+
if (this.eventSource) this.close();
|
|
370
|
+
const eventSourcePromise = new Promise((resolve, reject) => {
|
|
371
|
+
this.eventSource = createEventSourceClient({
|
|
372
|
+
url: this.url,
|
|
373
|
+
method: "POST",
|
|
374
|
+
headers: { "Content-Type": "application/json" },
|
|
375
|
+
body: JSON.stringify({
|
|
376
|
+
metaContext: this.options.metaContext,
|
|
377
|
+
serverSdkKey: this.options.serverSdkKey
|
|
378
|
+
}),
|
|
379
|
+
onMessage: ({ data }) => {
|
|
380
|
+
this.dispatchMessage(data);
|
|
381
|
+
},
|
|
382
|
+
onConnect: () => {
|
|
383
|
+
this.logger.debug("[EventSourceTransport] Connected");
|
|
384
|
+
resolve(this);
|
|
385
|
+
},
|
|
386
|
+
shouldReconnect: (state) => {
|
|
387
|
+
const reconnect = !this.isStatusFatal(state.status);
|
|
388
|
+
if (!reconnect) reject(this.prepareFatalError(state.status, state.error));
|
|
389
|
+
return reconnect;
|
|
390
|
+
},
|
|
391
|
+
calculateReconnectDelay: (state) => {
|
|
392
|
+
const delay = Math.pow(2, Math.min(state.attempt, MAX_EXPONENTIAL_DELAY)) * 1e3;
|
|
393
|
+
this.logger.warn(`[EventSourceTransport] Scheduling reconnect in ${delay}ms.`);
|
|
394
|
+
return delay;
|
|
395
|
+
},
|
|
396
|
+
onDisconnect: () => {
|
|
397
|
+
this.logger.debug("[EventSourceTransport] Disconnected");
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
this.eventSource.connect();
|
|
401
|
+
});
|
|
402
|
+
return Promise.race([eventSourcePromise, new Promise((resolve) => {
|
|
403
|
+
setTimeout(() => resolve(this), timeout);
|
|
404
|
+
})]);
|
|
405
|
+
}
|
|
406
|
+
dispatchMessage(data) {
|
|
407
|
+
try {
|
|
408
|
+
const json = JSON.parse(data);
|
|
409
|
+
this.eventEmitter.emit("configBundleReceived", json);
|
|
410
|
+
} catch (error) {
|
|
411
|
+
this.logger.error("[EventSourceTransport] Error parsing and dispatching config data update: ", error);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
prepareFatalError(responseStatus, error) {
|
|
415
|
+
const status = responseStatus ?? 0;
|
|
416
|
+
return new ConfigDirectorConnectionError(`${`Connection failed with status: ${responseStatus ?? "unknown"}.`}${error ? ` Error: ${error}.` : ""} This is an unrecoverable error, will not attempt to reconnect.`, status);
|
|
417
|
+
}
|
|
418
|
+
isStatusFatal(status) {
|
|
419
|
+
return !!status && status >= 400 && status < 500;
|
|
420
|
+
}
|
|
421
|
+
get isConnected() {
|
|
422
|
+
return this.eventSource?.readyState == "open";
|
|
423
|
+
}
|
|
424
|
+
on(name, handler) {
|
|
425
|
+
this.eventEmitter.on(name, handler);
|
|
426
|
+
}
|
|
427
|
+
off(name, handler) {
|
|
428
|
+
if (handler) this.eventEmitter.off(name, handler);
|
|
429
|
+
else this.eventEmitter.removeAllListeners(name);
|
|
430
|
+
}
|
|
431
|
+
clear() {
|
|
432
|
+
this.eventEmitter.removeAllListeners();
|
|
433
|
+
}
|
|
434
|
+
close() {
|
|
435
|
+
this.eventSource?.close();
|
|
436
|
+
}
|
|
437
|
+
dispose() {
|
|
438
|
+
this.close();
|
|
439
|
+
this.clear();
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
//#endregion
|
|
443
|
+
//#region ../config-evaluator/src/text-comparison.ts
|
|
444
|
+
const compareText = (value, operator, targetValues) => {
|
|
445
|
+
switch (operator.toLowerCase()) {
|
|
446
|
+
case "=":
|
|
447
|
+
case "equals": return value == targetValues[0];
|
|
448
|
+
case "!=":
|
|
449
|
+
case "does not equal": return value != targetValues[0];
|
|
450
|
+
case "is one of": return targetValues.includes(value);
|
|
451
|
+
case "is not one of": return !targetValues.includes(value);
|
|
452
|
+
case "starts with any of": return targetValues.findIndex((v) => value?.startsWith(v)) >= 0;
|
|
453
|
+
case "does not start with any of": return targetValues.findIndex((v) => value?.startsWith(v)) < 0;
|
|
454
|
+
case "ends with any of": return targetValues.findIndex((v) => value?.endsWith(v)) >= 0;
|
|
455
|
+
case "does not end with any of": return targetValues.findIndex((v) => value?.endsWith(v)) < 0;
|
|
456
|
+
case "matches regex": return matchesRegex(targetValues[0], value);
|
|
457
|
+
case "does not match regex": return !matchesRegex(targetValues[0], value);
|
|
458
|
+
default: return false;
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
const matchesRegex = (regexString, value) => {
|
|
462
|
+
try {
|
|
463
|
+
return new RegExp(regexString).test(value);
|
|
464
|
+
} catch {
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
//#endregion
|
|
469
|
+
//#region ../config-evaluator/src/numeric-comparison.ts
|
|
470
|
+
const compareNumeric = (givenValue, operator, targetValues) => {
|
|
471
|
+
if (typeof givenValue == "boolean") return false;
|
|
472
|
+
const lowercaseOperator = operator.toLowerCase();
|
|
473
|
+
if (givenValue == null) return lowercaseOperator == "!=" || lowercaseOperator == "does not equal";
|
|
474
|
+
const { value, target } = parseValueAndTarget(givenValue, targetValues[0]);
|
|
475
|
+
switch (lowercaseOperator) {
|
|
476
|
+
case "=":
|
|
477
|
+
case "equals": return value == target;
|
|
478
|
+
case "!=":
|
|
479
|
+
case "does not equal": return value != target;
|
|
480
|
+
case "<": return value < target;
|
|
481
|
+
case "<=": return value <= target;
|
|
482
|
+
case ">": return value > target;
|
|
483
|
+
case ">=": return value >= target;
|
|
484
|
+
default: return false;
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
const parseValueAndTarget = (value, targetValue) => {
|
|
488
|
+
if (typeof value == "number") return {
|
|
489
|
+
value,
|
|
490
|
+
target: parseAsNumber(targetValue)
|
|
491
|
+
};
|
|
492
|
+
else if (typeof value == "bigint") return {
|
|
493
|
+
value,
|
|
494
|
+
target: BigInt(targetValue)
|
|
495
|
+
};
|
|
496
|
+
else return {
|
|
497
|
+
value: parseAsNumber(value),
|
|
498
|
+
target: parseAsNumber(targetValue)
|
|
499
|
+
};
|
|
500
|
+
};
|
|
501
|
+
const parseAsNumber = (value) => {
|
|
502
|
+
const n = Number.parseFloat(value.toString());
|
|
503
|
+
if (isNaN(n) || !isFinite(n)) throw new TypeError(`Invalid numeric value: ${value}`);
|
|
504
|
+
return n;
|
|
505
|
+
};
|
|
506
|
+
//#endregion
|
|
507
|
+
//#region ../config-evaluator/src/date-comparison.ts
|
|
508
|
+
const compareDate = (value, operator, targetValues) => {
|
|
509
|
+
if (value == null) return false;
|
|
510
|
+
try {
|
|
511
|
+
const dateValue = new Date(value);
|
|
512
|
+
const dateTarget = new Date(targetValues[0]);
|
|
513
|
+
switch (operator) {
|
|
514
|
+
case "is after": return dateValue > dateTarget;
|
|
515
|
+
case "is before": return dateValue < dateTarget;
|
|
516
|
+
default: return false;
|
|
517
|
+
}
|
|
518
|
+
} catch {
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
//#endregion
|
|
523
|
+
//#region ../config-evaluator/src/semver-comparison.ts
|
|
524
|
+
const compareSemver = (value, operator, targetValues) => {
|
|
525
|
+
const lowercaseOperator = operator.toLowerCase();
|
|
526
|
+
if (value == null || value.trim().length == 0) return lowercaseOperator == "is not one of";
|
|
527
|
+
const semverValue = semver.coerce(value) ?? value;
|
|
528
|
+
const semverTargets = targetValues.map((v) => semver.coerce(v) ?? v);
|
|
529
|
+
try {
|
|
530
|
+
switch (lowercaseOperator) {
|
|
531
|
+
case "=": return safeEquals(semverValue, semverTargets[0]);
|
|
532
|
+
case "<": return semver.lt(semverValue, semverTargets[0]);
|
|
533
|
+
case "<=": return semver.lte(semverValue, semverTargets[0]);
|
|
534
|
+
case ">": return semver.gt(semverValue, semverTargets[0]);
|
|
535
|
+
case ">=": return semver.gte(semverValue, semverTargets[0]);
|
|
536
|
+
case "is one of": return semverTargets.findIndex((v) => safeEquals(semverValue, v)) >= 0;
|
|
537
|
+
case "is not one of": return semverTargets.findIndex((v) => safeEquals(semverValue, v)) < 0;
|
|
538
|
+
default: return false;
|
|
539
|
+
}
|
|
540
|
+
} catch {
|
|
541
|
+
return false;
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
const safeEquals = (v1, v2) => {
|
|
545
|
+
try {
|
|
546
|
+
return semver.eq(v1, v2);
|
|
547
|
+
} catch {
|
|
548
|
+
return false;
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
//#endregion
|
|
552
|
+
//#region ../config-evaluator/src/array-comparison.ts
|
|
553
|
+
const compareArray = (givenValue, operator, targetValues) => {
|
|
554
|
+
const lowercaseOperator = operator.toLowerCase();
|
|
555
|
+
if (!Array.isArray(givenValue)) return lowercaseOperator == "does not contain any of";
|
|
556
|
+
switch (lowercaseOperator) {
|
|
557
|
+
case "contains any of": return givenValue.some((v) => targetValues.includes(v));
|
|
558
|
+
case "does not contain any of": return !givenValue.some((v) => targetValues.includes(v));
|
|
559
|
+
default: return false;
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
//#endregion
|
|
563
|
+
//#region ../config-evaluator/src/ConditionEvaluator.ts
|
|
564
|
+
var ConditionEvaluator = class {
|
|
565
|
+
evaluate(condition, context) {
|
|
566
|
+
if (!this.isConditionValidForComparison(condition)) return false;
|
|
567
|
+
switch (condition.attribute) {
|
|
568
|
+
case "identifier": return this.compare(context?.context?.id, condition);
|
|
569
|
+
case "name": return this.compare(context?.context?.name, condition);
|
|
570
|
+
case "appName": return this.compare(context?.metadata?.appName, condition);
|
|
571
|
+
case "appVersion": return this.compare(context?.metadata?.appVersion, condition);
|
|
572
|
+
case "traits": if (condition.trait) {
|
|
573
|
+
const value = this.findTraitValue(context?.context?.traits, condition.trait);
|
|
574
|
+
return this.compare(value, condition);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return false;
|
|
578
|
+
}
|
|
579
|
+
compare(value, condition) {
|
|
580
|
+
switch (condition.targetType) {
|
|
581
|
+
case "text": return compareText(value?.toString(), condition.operator, condition.targetValues);
|
|
582
|
+
case "number": return compareNumeric(value, condition.operator, condition.targetValues);
|
|
583
|
+
case "datetime": return compareDate(value?.toString(), condition.operator, condition.targetValues);
|
|
584
|
+
case "semver": return compareSemver(value?.toString(), condition.operator, condition.targetValues);
|
|
585
|
+
case "array": return compareArray(value, condition.operator, condition.targetValues);
|
|
586
|
+
}
|
|
587
|
+
return false;
|
|
588
|
+
}
|
|
589
|
+
findTraitValue(traits, path) {
|
|
590
|
+
try {
|
|
591
|
+
return findByPointer(path, traits).val;
|
|
592
|
+
} catch {
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
isConditionValidForComparison(condition) {
|
|
597
|
+
return Array.isArray(condition.targetValues) && condition.targetValues.length > 0;
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
//#endregion
|
|
601
|
+
//#region ../config-evaluator/src/percent-hashing.ts
|
|
602
|
+
const SEED = 15426857351n;
|
|
603
|
+
const assignPercentage = (data) => {
|
|
604
|
+
const value = `${data.contextIdentifier}-${data.configId}`;
|
|
605
|
+
return Number(rapidhash(value, { seed: SEED }) % 1000n) / 10;
|
|
606
|
+
};
|
|
607
|
+
//#endregion
|
|
608
|
+
//#region ../config-evaluator/src/ConfigEvaluator.ts
|
|
609
|
+
var ConfigEvaluator = class {
|
|
610
|
+
conditionEvaluator = new ConditionEvaluator();
|
|
611
|
+
constructor(logger) {
|
|
612
|
+
this.logger = logger;
|
|
613
|
+
if (!logger || !logger.warn || !logger.error || !logger.info) throw new TypeError("The provided logger is not a valid 'ConfigDirectorLogger'");
|
|
614
|
+
this.logger = logger;
|
|
615
|
+
}
|
|
616
|
+
evaluate(config, context) {
|
|
617
|
+
return {
|
|
618
|
+
id: config.id,
|
|
619
|
+
key: config.key,
|
|
620
|
+
type: config.type,
|
|
621
|
+
value: this.getConfigValue(config, context)
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
getConfigValue(config, context) {
|
|
625
|
+
const rules = [...config.target?.rules ?? []].sort((a, b) => (a.order ?? Number.MAX_SAFE_INTEGER) - (b.order ?? Number.MAX_SAFE_INTEGER));
|
|
626
|
+
for (const rule of rules) {
|
|
627
|
+
const result = this.evaluateRule(rule, config, context);
|
|
628
|
+
if (result.success) return result.value;
|
|
629
|
+
}
|
|
630
|
+
return config.target?.defaultValue;
|
|
631
|
+
}
|
|
632
|
+
evaluateRule(rule, config, context) {
|
|
633
|
+
try {
|
|
634
|
+
if (rule.type == "percentage") return this.evaluatePercentageRule(rule, config, context);
|
|
635
|
+
else if (rule.type == "conditional") return this.evaluateConditionalRule(rule, config, context);
|
|
636
|
+
} catch (error) {
|
|
637
|
+
this.logger.warn(`There was an error while evaluating a targeting rule '${rule?.id}' for '${config?.key}'. The rule will be disregarded.`, {
|
|
638
|
+
error,
|
|
639
|
+
configKey: config?.key,
|
|
640
|
+
ruleId: rule?.id
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
return { success: false };
|
|
644
|
+
}
|
|
645
|
+
evaluatePercentageRule(rule, config, context) {
|
|
646
|
+
return this.evaluatePercentage(rule.percentages ?? [], config, context);
|
|
647
|
+
}
|
|
648
|
+
evaluatePercentage(percentages, config, context) {
|
|
649
|
+
const assignedPercentage = assignPercentage({
|
|
650
|
+
configId: config.id,
|
|
651
|
+
contextIdentifier: context?.context?.id ?? crypto.randomUUID()
|
|
652
|
+
});
|
|
653
|
+
let index = 0;
|
|
654
|
+
let sum = 0;
|
|
655
|
+
let bucket = void 0;
|
|
656
|
+
while (!bucket && index < percentages.length) {
|
|
657
|
+
if (assignedPercentage <= percentages[index].percentage + sum) bucket = percentages[index];
|
|
658
|
+
sum += percentages[index].percentage;
|
|
659
|
+
index += 1;
|
|
660
|
+
}
|
|
661
|
+
if (bucket?.value != null) return {
|
|
662
|
+
success: true,
|
|
663
|
+
value: bucket.value.toString()
|
|
664
|
+
};
|
|
665
|
+
return { success: false };
|
|
666
|
+
}
|
|
667
|
+
evaluateConditionalRule(rule, config, context) {
|
|
668
|
+
const conditions = rule.conditions ?? [];
|
|
669
|
+
let index = 0;
|
|
670
|
+
let condition = void 0;
|
|
671
|
+
while (!condition && index < conditions.length) {
|
|
672
|
+
if (this.conditionEvaluator.evaluate(conditions[index], context)) condition = conditions[index];
|
|
673
|
+
index += 1;
|
|
674
|
+
}
|
|
675
|
+
if (condition && rule.target == "value" && rule.value != null) return {
|
|
676
|
+
success: true,
|
|
677
|
+
value: rule.value.toString()
|
|
678
|
+
};
|
|
679
|
+
else if (condition && rule.target == "percentage") return this.evaluatePercentage(rule.percentages ?? [], config, context);
|
|
680
|
+
return { success: false };
|
|
681
|
+
}
|
|
682
|
+
};
|
|
683
|
+
//#endregion
|
|
684
|
+
//#region ../shared/src/value-parser.ts
|
|
685
|
+
const getRequestedType = (defaultValue) => {
|
|
686
|
+
const baseType = typeof defaultValue;
|
|
687
|
+
if (baseType === "object") try {
|
|
688
|
+
return defaultValue.constructor?.name ?? baseType;
|
|
689
|
+
} catch {
|
|
690
|
+
return baseType;
|
|
691
|
+
}
|
|
692
|
+
else if (baseType === "function") try {
|
|
693
|
+
const functionName = defaultValue.name;
|
|
694
|
+
return functionName ? `function: ${functionName}` : baseType;
|
|
695
|
+
} catch {
|
|
696
|
+
return baseType;
|
|
697
|
+
}
|
|
698
|
+
return baseType;
|
|
699
|
+
};
|
|
700
|
+
const isNumericNativeType = (requestedType) => {
|
|
701
|
+
return requestedType === "number" || requestedType === "bigint";
|
|
702
|
+
};
|
|
703
|
+
const parseConfigValue = (configState, defaultValue) => {
|
|
704
|
+
const value = configState.value;
|
|
705
|
+
const requestedType = getRequestedType(defaultValue);
|
|
706
|
+
if (!value) return {
|
|
707
|
+
parsedValue: defaultValue,
|
|
708
|
+
requestedType,
|
|
709
|
+
usedDefault: true,
|
|
710
|
+
reason: "value-missing"
|
|
711
|
+
};
|
|
712
|
+
if (typeof defaultValue === "string") return {
|
|
713
|
+
parsedValue: value,
|
|
714
|
+
requestedType,
|
|
715
|
+
usedDefault: false,
|
|
716
|
+
reason: "found-match"
|
|
717
|
+
};
|
|
718
|
+
if (typeof defaultValue === "boolean" && configState.type === "boolean") {
|
|
719
|
+
const boolValue = parseConfigBoolean(value);
|
|
720
|
+
const hasBoolean = typeof boolValue === "boolean";
|
|
721
|
+
return {
|
|
722
|
+
parsedValue: hasBoolean ? boolValue : defaultValue,
|
|
723
|
+
requestedType,
|
|
724
|
+
usedDefault: !hasBoolean,
|
|
725
|
+
reason: hasBoolean ? "found-match" : "invalid-boolean"
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
if (isNumericNativeType(typeof defaultValue) && configState.type === "integer") {
|
|
729
|
+
const numValue = parseConfigInteger(value);
|
|
730
|
+
const hasNumber = typeof numValue === "number";
|
|
731
|
+
return {
|
|
732
|
+
parsedValue: hasNumber ? numValue : defaultValue,
|
|
733
|
+
requestedType,
|
|
734
|
+
usedDefault: !hasNumber,
|
|
735
|
+
reason: hasNumber ? "found-match" : "invalid-number"
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
if (isNumericNativeType(typeof defaultValue) && (configState.type === "float" || configState.type === "enum")) {
|
|
739
|
+
const numValue = parseConfigFloat(value);
|
|
740
|
+
const hasNumber = typeof numValue === "number";
|
|
741
|
+
return {
|
|
742
|
+
parsedValue: hasNumber ? numValue : defaultValue,
|
|
743
|
+
requestedType,
|
|
744
|
+
usedDefault: !hasNumber,
|
|
745
|
+
reason: hasNumber ? "found-match" : "invalid-number"
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
return {
|
|
749
|
+
parsedValue: value,
|
|
750
|
+
requestedType,
|
|
751
|
+
usedDefault: false,
|
|
752
|
+
reason: "found-match"
|
|
753
|
+
};
|
|
754
|
+
};
|
|
755
|
+
const parseConfigBoolean = (value) => {
|
|
756
|
+
if (!value) return;
|
|
757
|
+
const lowerValue = value.toLowerCase();
|
|
758
|
+
if (lowerValue != "true" && lowerValue != "false") return;
|
|
759
|
+
return lowerValue === "true";
|
|
760
|
+
};
|
|
761
|
+
const parseConfigInteger = (value) => {
|
|
762
|
+
if (!value) return;
|
|
763
|
+
const num = Number.parseInt(value);
|
|
764
|
+
if (isNaN(num) || !isFinite(num)) return;
|
|
765
|
+
return num;
|
|
766
|
+
};
|
|
767
|
+
const parseConfigFloat = (value) => {
|
|
768
|
+
if (!value) return;
|
|
769
|
+
const num = Number.parseFloat(value);
|
|
770
|
+
if (isNaN(num) || !isFinite(num)) return;
|
|
771
|
+
return num;
|
|
772
|
+
};
|
|
773
|
+
//#endregion
|
|
774
|
+
//#region ../shared/src/logger.ts
|
|
775
|
+
const LEVELS = {
|
|
776
|
+
off: -1,
|
|
777
|
+
error: 0,
|
|
778
|
+
warn: 1,
|
|
779
|
+
info: 2,
|
|
780
|
+
debug: 3
|
|
781
|
+
};
|
|
782
|
+
const buildDateFormatter = () => {
|
|
783
|
+
const baseOptions = {
|
|
784
|
+
year: "numeric",
|
|
785
|
+
month: "2-digit",
|
|
786
|
+
day: "2-digit",
|
|
787
|
+
hour: "2-digit",
|
|
788
|
+
minute: "2-digit",
|
|
789
|
+
second: "2-digit",
|
|
790
|
+
timeZoneName: "short"
|
|
791
|
+
};
|
|
792
|
+
try {
|
|
793
|
+
return new Intl.DateTimeFormat(void 0, {
|
|
794
|
+
...baseOptions,
|
|
795
|
+
fractionalSecondDigits: 3
|
|
796
|
+
});
|
|
797
|
+
} catch {
|
|
798
|
+
return new Intl.DateTimeFormat(void 0, baseOptions);
|
|
799
|
+
}
|
|
800
|
+
};
|
|
801
|
+
var DefaultConsoleLogger = class {
|
|
802
|
+
dateFormatter;
|
|
803
|
+
constructor(level, decorator) {
|
|
804
|
+
this.level = level;
|
|
805
|
+
this.decorator = decorator;
|
|
806
|
+
this.level = level;
|
|
807
|
+
this.decorator = decorator;
|
|
808
|
+
this.dateFormatter = buildDateFormatter();
|
|
809
|
+
}
|
|
810
|
+
debug(message, ...args) {
|
|
811
|
+
this.log(console.debug, "debug", message, ...args);
|
|
812
|
+
}
|
|
813
|
+
info(message, ...args) {
|
|
814
|
+
this.log(console.info, "info", message, ...args);
|
|
815
|
+
}
|
|
816
|
+
warn(message, ...args) {
|
|
817
|
+
this.log(console.warn, "warn", message, ...args);
|
|
818
|
+
}
|
|
819
|
+
error(message, ...args) {
|
|
820
|
+
this.log(console.error, "error", message, ...args);
|
|
821
|
+
}
|
|
822
|
+
log(loggerFunction, level, message, ...args) {
|
|
823
|
+
if (LEVELS[this.level] >= LEVELS[level]) loggerFunction(`[${this.dateFormatter.format(/* @__PURE__ */ new Date())}] ${this.decorator?.decorateMessage(message)}`, ...args);
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
//#endregion
|
|
827
|
+
//#region ../js-server-sdk/src/logger.ts
|
|
828
|
+
var LogMessageDecorator = class {
|
|
829
|
+
decorateMessage(message) {
|
|
830
|
+
return `[ConfigDirector:js-server-sdk] ${message}`;
|
|
831
|
+
}
|
|
832
|
+
};
|
|
833
|
+
const createDefaultLogger$1 = (level, messageDecorator) => {
|
|
834
|
+
return new DefaultConsoleLogger(level ?? "warn", messageDecorator ?? new LogMessageDecorator());
|
|
835
|
+
};
|
|
836
|
+
//#endregion
|
|
837
|
+
//#region ../shared/src/url.ts
|
|
838
|
+
const defaultUrlFactory = (input, base) => new URL(input, base?.toString());
|
|
839
|
+
//#endregion
|
|
840
|
+
//#region ../shared/src/telemetry/utils.ts
|
|
841
|
+
const CONFIG_VALUE_MAX_LENGTH = 500;
|
|
842
|
+
const sanitizeValue = (value, type) => {
|
|
843
|
+
if (type === "json") try {
|
|
844
|
+
return djb2Hash(JSON.stringify(value));
|
|
845
|
+
} catch {
|
|
846
|
+
return value.toString().slice(0, CONFIG_VALUE_MAX_LENGTH);
|
|
847
|
+
}
|
|
848
|
+
return value.toString().slice(0, CONFIG_VALUE_MAX_LENGTH);
|
|
849
|
+
};
|
|
850
|
+
const djb2Hash = (data) => {
|
|
851
|
+
return djb2(new TextEncoder().encode(data)).toString(16).padStart(8, "0");
|
|
852
|
+
};
|
|
853
|
+
const djb2 = (bytes) => {
|
|
854
|
+
let hash = 5381;
|
|
855
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
856
|
+
hash = (hash << 5) + hash + (bytes[i] ?? 0);
|
|
857
|
+
hash = hash >>> 0;
|
|
858
|
+
}
|
|
859
|
+
return hash;
|
|
860
|
+
};
|
|
861
|
+
const isEventListEmpty = (eventList) => {
|
|
862
|
+
const keys = Object.keys(eventList);
|
|
863
|
+
if (keys.length == 0) return true;
|
|
864
|
+
for (const key of keys) if ((eventList[key]?.length ?? 0) > 0) return false;
|
|
865
|
+
return true;
|
|
866
|
+
};
|
|
867
|
+
const isDroppedEventsEmpty = (droppedEvents) => {
|
|
868
|
+
if (!droppedEvents) return true;
|
|
869
|
+
const keys = Object.keys(droppedEvents);
|
|
870
|
+
if (keys.length == 0) return true;
|
|
871
|
+
for (const key of keys) if ((droppedEvents[key] ?? 0) > 0) return false;
|
|
872
|
+
return true;
|
|
873
|
+
};
|
|
874
|
+
//#endregion
|
|
875
|
+
//#region ../shared/src/fetchWithTimeout.ts
|
|
876
|
+
const fetchWithTimeout = async (timeout, resource, options, logger) => {
|
|
877
|
+
const abortController = new AbortController();
|
|
878
|
+
const abortTimeoutId = setTimeout(() => {
|
|
879
|
+
logger.debug("[fetchWithTimeout] Reached timeout, aborting request");
|
|
880
|
+
abortController.abort();
|
|
881
|
+
}, timeout);
|
|
882
|
+
try {
|
|
883
|
+
const response = await fetch(typeof resource === "string" || resource instanceof Request ? resource : resource.toString(), {
|
|
884
|
+
...options,
|
|
885
|
+
signal: abortController.signal
|
|
886
|
+
});
|
|
887
|
+
clearTimeout(abortTimeoutId);
|
|
888
|
+
return response;
|
|
889
|
+
} catch (error) {
|
|
890
|
+
logger.warn("[fetchWithTimeout] Fetch error: ", error);
|
|
891
|
+
clearTimeout(abortTimeoutId);
|
|
892
|
+
throw error;
|
|
893
|
+
}
|
|
894
|
+
};
|
|
895
|
+
//#endregion
|
|
896
|
+
//#region ../js-server-sdk/src/telemetry/ServerEventReporter.ts
|
|
897
|
+
var ServerEventReporter = class {
|
|
898
|
+
sdkKey;
|
|
899
|
+
logger;
|
|
900
|
+
url;
|
|
901
|
+
executeRequests = true;
|
|
902
|
+
constructor(options) {
|
|
903
|
+
this.sdkKey = options.sdkKey;
|
|
904
|
+
this.logger = options.logger;
|
|
905
|
+
this.url = new URL("server/telemetry/v1", options.baseUrl.toString());
|
|
906
|
+
}
|
|
907
|
+
async report({ discreteEvents, aggregatedEvents, droppedEvents }) {
|
|
908
|
+
if (!this.executeRequests) return {
|
|
909
|
+
success: false,
|
|
910
|
+
fatalError: true
|
|
911
|
+
};
|
|
912
|
+
const eventReport = {
|
|
913
|
+
serverSdkKey: this.sdkKey,
|
|
914
|
+
discreteEvents,
|
|
915
|
+
aggregatedEvents,
|
|
916
|
+
droppedEvents
|
|
917
|
+
};
|
|
918
|
+
if (this.isReportEmpty(eventReport)) return {
|
|
919
|
+
success: true,
|
|
920
|
+
fatalError: false
|
|
921
|
+
};
|
|
922
|
+
const response = await this.sendReport(eventReport);
|
|
923
|
+
if (response.fatalError) this.executeRequests = false;
|
|
924
|
+
return response;
|
|
925
|
+
}
|
|
926
|
+
isReportEmpty(eventReport) {
|
|
927
|
+
return isEventListEmpty(eventReport.discreteEvents) && isEventListEmpty(eventReport.aggregatedEvents) && isDroppedEventsEmpty(eventReport.droppedEvents);
|
|
928
|
+
}
|
|
929
|
+
async sendReport(eventReport) {
|
|
930
|
+
try {
|
|
931
|
+
const response = await fetchWithTimeout(5e3, this.url, {
|
|
932
|
+
method: "POST",
|
|
933
|
+
body: JSON.stringify(eventReport)
|
|
934
|
+
}, this.logger);
|
|
935
|
+
const isFatalError = this.isStatusFatal(response.status);
|
|
936
|
+
if (isFatalError) this.logger.warn(`[EventReporter] Received a fatal status response (${response.status}) from the telemetry endpoint. No more telemetry data will be sent.`);
|
|
937
|
+
return {
|
|
938
|
+
success: response.ok,
|
|
939
|
+
fatalError: isFatalError
|
|
940
|
+
};
|
|
941
|
+
} catch (error) {
|
|
942
|
+
this.logger.warn(`[EventReporter] Error attempting to send telemetry data: ${error}`, { error });
|
|
943
|
+
return {
|
|
944
|
+
success: false,
|
|
945
|
+
fatalError: false
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
isStatusFatal(status) {
|
|
950
|
+
return !!status && status >= 400 && status < 500;
|
|
951
|
+
}
|
|
952
|
+
};
|
|
953
|
+
//#endregion
|
|
954
|
+
//#region ../shared/src/telemetry/EventAggregator.ts
|
|
955
|
+
var EventAggregator = class {
|
|
956
|
+
aggregate(snapshot) {
|
|
957
|
+
if (snapshot.events.length == 0) return [];
|
|
958
|
+
const map = /* @__PURE__ */ new Map();
|
|
959
|
+
for (const event of snapshot.events) {
|
|
960
|
+
const serializedEvent = JSON.stringify(event);
|
|
961
|
+
const count = map.get(serializedEvent)?.count || 0;
|
|
962
|
+
map.set(serializedEvent, {
|
|
963
|
+
event,
|
|
964
|
+
count: count + 1
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
return Array.from(map).map(([, v]) => {
|
|
968
|
+
return {
|
|
969
|
+
startTime: snapshot.startTime,
|
|
970
|
+
endTime: snapshot.endTime,
|
|
971
|
+
count: v.count,
|
|
972
|
+
event: v.event
|
|
973
|
+
};
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
};
|
|
977
|
+
//#endregion
|
|
978
|
+
//#region ../shared/src/telemetry/EventQueue.ts
|
|
979
|
+
var EventQueue = class {
|
|
980
|
+
limit;
|
|
981
|
+
startTime;
|
|
982
|
+
_events = [];
|
|
983
|
+
_droppedEventCount = 0;
|
|
984
|
+
constructor(limit) {
|
|
985
|
+
this.limit = limit ?? 1e3;
|
|
986
|
+
}
|
|
987
|
+
get events() {
|
|
988
|
+
return this._events;
|
|
989
|
+
}
|
|
990
|
+
get reachedLimit() {
|
|
991
|
+
return this._events.length >= this.limit;
|
|
992
|
+
}
|
|
993
|
+
get droppedEventCount() {
|
|
994
|
+
return this._droppedEventCount;
|
|
995
|
+
}
|
|
996
|
+
push(...newEvents) {
|
|
997
|
+
if (!this.startTime) this.startTime = /* @__PURE__ */ new Date();
|
|
998
|
+
const newEventsCountToDrop = Math.max(0, newEvents.length - this.limit);
|
|
999
|
+
const eventsToPush = newEvents.slice(newEventsCountToDrop, newEvents.length);
|
|
1000
|
+
this._droppedEventCount += newEventsCountToDrop;
|
|
1001
|
+
if (this._events.length + eventsToPush.length > this.limit) {
|
|
1002
|
+
const eventCountToDrop = this._events.length + eventsToPush.length - this.limit;
|
|
1003
|
+
this._events.splice(0, eventCountToDrop);
|
|
1004
|
+
this._droppedEventCount += eventCountToDrop;
|
|
1005
|
+
}
|
|
1006
|
+
return this._events.push(...eventsToPush);
|
|
1007
|
+
}
|
|
1008
|
+
takeSnapshot() {
|
|
1009
|
+
const endTime = /* @__PURE__ */ new Date();
|
|
1010
|
+
const startTime = this.startTime ?? endTime;
|
|
1011
|
+
const eventsSnapshot = this._events.splice(0);
|
|
1012
|
+
const droppedCount = this._droppedEventCount;
|
|
1013
|
+
this.startTime = void 0;
|
|
1014
|
+
this._droppedEventCount = 0;
|
|
1015
|
+
return {
|
|
1016
|
+
startTime,
|
|
1017
|
+
endTime,
|
|
1018
|
+
events: eventsSnapshot,
|
|
1019
|
+
droppedCount
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
clear() {
|
|
1023
|
+
this._events = [];
|
|
1024
|
+
this.startTime = void 0;
|
|
1025
|
+
this._droppedEventCount = 0;
|
|
1026
|
+
}
|
|
1027
|
+
};
|
|
1028
|
+
//#endregion
|
|
1029
|
+
//#region ../shared/src/telemetry/TelemetryEventCollector.ts
|
|
1030
|
+
var TelemetryEventCollector = class {
|
|
1031
|
+
logger;
|
|
1032
|
+
evaluationEventQueue;
|
|
1033
|
+
aggregator = new EventAggregator();
|
|
1034
|
+
flushIntervalDelay;
|
|
1035
|
+
flushTimeout;
|
|
1036
|
+
collectEvents = true;
|
|
1037
|
+
constructor(options) {
|
|
1038
|
+
this.logger = options.logger;
|
|
1039
|
+
this.evaluationEventQueue = new EventQueue(options.evaluationQueueLimit ?? 1e3);
|
|
1040
|
+
this.flushIntervalDelay = options.flushIntervalDelay ?? 3e4;
|
|
1041
|
+
const initialDelay = options.initialFlushIntervalDelay ?? 5e3;
|
|
1042
|
+
this.flushTimeout = setTimeout(() => this.flushAndScheduleNext(), initialDelay);
|
|
1043
|
+
}
|
|
1044
|
+
sanitizeValue(value, type) {
|
|
1045
|
+
return sanitizeValue(value, type);
|
|
1046
|
+
}
|
|
1047
|
+
async flushAndScheduleNext() {
|
|
1048
|
+
if ((await this.flush()).fatalError) {
|
|
1049
|
+
this.collectEvents = false;
|
|
1050
|
+
this.close();
|
|
1051
|
+
this.logger.warn("[TelemetryEventCollector] Received a fatal error from telemetry collection. No longer collecting events.");
|
|
1052
|
+
} else this.flushTimeout = setTimeout(() => this.flushAndScheduleNext(), this.flushIntervalDelay);
|
|
1053
|
+
}
|
|
1054
|
+
async flush() {
|
|
1055
|
+
if (!this.reporter) return {
|
|
1056
|
+
success: false,
|
|
1057
|
+
fatalError: false
|
|
1058
|
+
};
|
|
1059
|
+
const evaluationSnapshot = this.evaluationEventQueue.takeSnapshot();
|
|
1060
|
+
return await this.reporter?.report({
|
|
1061
|
+
context: this._context,
|
|
1062
|
+
discreteEvents: {},
|
|
1063
|
+
aggregatedEvents: { evaluatedConfig: this.aggregator.aggregate(evaluationSnapshot) },
|
|
1064
|
+
droppedEvents: { evaluatedConfig: evaluationSnapshot.droppedCount }
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
cancelScheduledFlush() {
|
|
1068
|
+
clearTimeout(this.flushTimeout);
|
|
1069
|
+
}
|
|
1070
|
+
async close() {
|
|
1071
|
+
this.collectEvents = false;
|
|
1072
|
+
clearTimeout(this.flushTimeout);
|
|
1073
|
+
await this.flush();
|
|
1074
|
+
this.evaluationEventQueue.clear();
|
|
1075
|
+
}
|
|
1076
|
+
dispose() {
|
|
1077
|
+
this.close();
|
|
1078
|
+
}
|
|
1079
|
+
};
|
|
1080
|
+
//#endregion
|
|
1081
|
+
//#region ../shared/src/LimitedMap.ts
|
|
1082
|
+
var LimitedMap = class extends Map {
|
|
1083
|
+
limit;
|
|
1084
|
+
_droppedCount = 0;
|
|
1085
|
+
constructor(limit, entries) {
|
|
1086
|
+
super(entries);
|
|
1087
|
+
this.limit = limit;
|
|
1088
|
+
this.enforceLimit();
|
|
1089
|
+
}
|
|
1090
|
+
set(key, value) {
|
|
1091
|
+
super.set(key, value);
|
|
1092
|
+
this.enforceLimit();
|
|
1093
|
+
return this;
|
|
1094
|
+
}
|
|
1095
|
+
get droppedCount() {
|
|
1096
|
+
return this._droppedCount;
|
|
1097
|
+
}
|
|
1098
|
+
clearAndReset() {
|
|
1099
|
+
this.clear();
|
|
1100
|
+
this._droppedCount = 0;
|
|
1101
|
+
}
|
|
1102
|
+
enforceLimit() {
|
|
1103
|
+
while (this.size > this.limit) {
|
|
1104
|
+
const oldestKey = this.keys().next().value;
|
|
1105
|
+
if (oldestKey !== void 0) {
|
|
1106
|
+
this.delete(oldestKey);
|
|
1107
|
+
this._droppedCount += 1;
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
};
|
|
1112
|
+
//#endregion
|
|
1113
|
+
//#region ../js-server-sdk/src/telemetry/ServerTelemetryEventCollector.ts
|
|
1114
|
+
const DEFAULT_CONTEXTS_LIMIT = 1e3;
|
|
1115
|
+
var ServerTelemetryEventCollector = class extends TelemetryEventCollector {
|
|
1116
|
+
reporter;
|
|
1117
|
+
_context;
|
|
1118
|
+
contexts;
|
|
1119
|
+
constructor(options) {
|
|
1120
|
+
super(options);
|
|
1121
|
+
this.reporter = new ServerEventReporter(options);
|
|
1122
|
+
this.contexts = new LimitedMap(options.contextLimit ?? DEFAULT_CONTEXTS_LIMIT);
|
|
1123
|
+
}
|
|
1124
|
+
evaluatedConfig(payload) {
|
|
1125
|
+
if (!this.collectEvents) return;
|
|
1126
|
+
if (payload.context?.id) this.contexts.set(payload.context.id, payload.context);
|
|
1127
|
+
const event = {
|
|
1128
|
+
...payload.evaluation,
|
|
1129
|
+
contextId: payload.context?.id
|
|
1130
|
+
};
|
|
1131
|
+
this.evaluationEventQueue.push(this.sanitizeEvaluatedConfigEvent(event));
|
|
1132
|
+
}
|
|
1133
|
+
sanitizeEvaluatedConfigEvent(event) {
|
|
1134
|
+
return {
|
|
1135
|
+
...event,
|
|
1136
|
+
defaultValue: this.sanitizeValue(event.defaultValue, event.type),
|
|
1137
|
+
evaluatedValue: this.sanitizeValue(event.evaluatedValue, event.type)
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
snapshotContexts() {
|
|
1141
|
+
const contexts = Array.from(this.contexts.values());
|
|
1142
|
+
const droppedCount = this.contexts.droppedCount;
|
|
1143
|
+
this.contexts.clearAndReset();
|
|
1144
|
+
return {
|
|
1145
|
+
contexts,
|
|
1146
|
+
droppedCount
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
async flush() {
|
|
1150
|
+
if (!this.reporter) return {
|
|
1151
|
+
success: false,
|
|
1152
|
+
fatalError: false
|
|
1153
|
+
};
|
|
1154
|
+
const evaluationSnapshot = this.evaluationEventQueue.takeSnapshot();
|
|
1155
|
+
const contextsSnapshot = this.snapshotContexts();
|
|
1156
|
+
return await this.reporter?.report({
|
|
1157
|
+
context: this._context,
|
|
1158
|
+
discreteEvents: { capturedContexts: contextsSnapshot.contexts },
|
|
1159
|
+
aggregatedEvents: { evaluatedConfig: this.aggregator.aggregate(evaluationSnapshot) },
|
|
1160
|
+
droppedEvents: {
|
|
1161
|
+
evaluatedConfig: evaluationSnapshot.droppedCount,
|
|
1162
|
+
capturedContexts: contextsSnapshot.droppedCount
|
|
1163
|
+
}
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
};
|
|
1167
|
+
//#endregion
|
|
1168
|
+
//#region ../js-server-sdk/src/DefaultConfigDirectorClient.ts
|
|
1169
|
+
const defaultBaseUrl = new URL("https://server-sdk-api.configdirector.com");
|
|
1170
|
+
const DEFAULT_FLUSH_INTERVAL = 3e4;
|
|
1171
|
+
const DEFAULT_EVENT_QUEUE_LIMIT = 5e3;
|
|
1172
|
+
var DefaultConfigDirectorClient = class {
|
|
1173
|
+
logger;
|
|
1174
|
+
usageEventCollector;
|
|
1175
|
+
configSet;
|
|
1176
|
+
handlersMap = /* @__PURE__ */ new Map();
|
|
1177
|
+
transport;
|
|
1178
|
+
eventEmitter = new EventEmitter();
|
|
1179
|
+
timeout;
|
|
1180
|
+
ready = false;
|
|
1181
|
+
readyPromise;
|
|
1182
|
+
readyResolve;
|
|
1183
|
+
streaming;
|
|
1184
|
+
configEvaluator;
|
|
1185
|
+
metaContext;
|
|
1186
|
+
constructor(serverSdkKey, sdkOptions, clientOptions) {
|
|
1187
|
+
this.logger = clientOptions?.logger ?? createDefaultLogger$1();
|
|
1188
|
+
this.timeout = clientOptions?.connection?.timeout ?? 3e3;
|
|
1189
|
+
this.metaContext = clientOptions?.metadata ?? {};
|
|
1190
|
+
this.configEvaluator = new ConfigEvaluator(this.logger);
|
|
1191
|
+
const baseUrl = this.parseUrl(clientOptions?.connection?.url) ?? defaultBaseUrl;
|
|
1192
|
+
this.streaming = clientOptions?.connection?.streaming === false ? false : true;
|
|
1193
|
+
const transportConstructor = StreamingTransport;
|
|
1194
|
+
const queueLimit = clientOptions?.telemetry?.eventQueueLimit ?? DEFAULT_EVENT_QUEUE_LIMIT;
|
|
1195
|
+
this.usageEventCollector = new ServerTelemetryEventCollector({
|
|
1196
|
+
sdkKey: serverSdkKey,
|
|
1197
|
+
logger: this.logger,
|
|
1198
|
+
baseUrl,
|
|
1199
|
+
flushIntervalDelay: clientOptions?.telemetry?.flushInterval ?? DEFAULT_FLUSH_INTERVAL,
|
|
1200
|
+
evaluationQueueLimit: Math.ceil(queueLimit * .7),
|
|
1201
|
+
contextLimit: Math.floor(queueLimit * .3),
|
|
1202
|
+
urlFactory: defaultUrlFactory
|
|
1203
|
+
});
|
|
1204
|
+
this.transport = new transportConstructor({
|
|
1205
|
+
serverSdkKey,
|
|
1206
|
+
baseUrl,
|
|
1207
|
+
metaContext: {
|
|
1208
|
+
...clientOptions?.metadata,
|
|
1209
|
+
sdkName: sdkOptions.sdkName,
|
|
1210
|
+
sdkVersion: sdkOptions.sdkVersion
|
|
1211
|
+
},
|
|
1212
|
+
logger: this.logger
|
|
1213
|
+
});
|
|
1214
|
+
this.transport.on("configBundleReceived", (configBundle) => {
|
|
1215
|
+
this.readyResolve?.();
|
|
1216
|
+
const configKeys = Object.keys(configBundle.configs);
|
|
1217
|
+
if (!this.configSet || configBundle.kind == "full") {
|
|
1218
|
+
this.configSet = configBundle;
|
|
1219
|
+
this.eventEmitter.emit("configsUpdated", { keys: configKeys });
|
|
1220
|
+
this.updateWatchers(configBundle.configs);
|
|
1221
|
+
} else {
|
|
1222
|
+
this.configSet.configs = {
|
|
1223
|
+
...this.configSet.configs,
|
|
1224
|
+
...configBundle.configs
|
|
1225
|
+
};
|
|
1226
|
+
this.eventEmitter.emit("configsUpdated", { keys: configKeys });
|
|
1227
|
+
this.updateWatchers(configBundle.configs);
|
|
1228
|
+
}
|
|
1229
|
+
this.logger.debug(`[ConfigDirectorClient] ConfigSet updated from server with ${configKeys.length} key(s)`, { keys: configKeys });
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
async initialize() {
|
|
1233
|
+
try {
|
|
1234
|
+
this.ready = false;
|
|
1235
|
+
this.readyPromise = new Promise((resolve) => {
|
|
1236
|
+
this.readyResolve = resolve;
|
|
1237
|
+
}).then(() => {
|
|
1238
|
+
this.ready = true;
|
|
1239
|
+
this.eventEmitter.emit("clientReady");
|
|
1240
|
+
this.logger.debug("[ConfigDirectorClient] Received initial payload from the server, client is ready");
|
|
1241
|
+
});
|
|
1242
|
+
const startTime = (/* @__PURE__ */ new Date()).getTime();
|
|
1243
|
+
await this.transport.connect(this.timeout);
|
|
1244
|
+
const elapsedTime = (/* @__PURE__ */ new Date()).getTime() - startTime;
|
|
1245
|
+
const remainingTimeout = this.timeout - elapsedTime;
|
|
1246
|
+
if (remainingTimeout > 0) await Promise.race([this.readyPromise, new Promise((resolve) => {
|
|
1247
|
+
setTimeout(() => resolve(), remainingTimeout);
|
|
1248
|
+
})]);
|
|
1249
|
+
if (!this.ready) {
|
|
1250
|
+
const warningDetails = this.streaming ? "The client will continue to retry since there were no fatal errors detected. Configs will return the default value until the connection succeeds." : "Since the client was configured without streaming, configs may not update and always return the default value.";
|
|
1251
|
+
this.logger.warn(`[ConfigDirectorClient] Timed out waiting for initialization after ${this.timeout}ms. ${warningDetails}`);
|
|
1252
|
+
}
|
|
1253
|
+
} catch (error) {
|
|
1254
|
+
this.logger.error("[ConfigDirectorClient] An error occurred during initialization: ", error);
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
updateWatchers(configsMap) {
|
|
1258
|
+
Object.values(configsMap).forEach((v) => this.updateWatchersForConfig(v));
|
|
1259
|
+
}
|
|
1260
|
+
updateWatchersForConfig(configDefinition) {
|
|
1261
|
+
this.handlersMap.get(configDefinition.key)?.forEach((h) => {
|
|
1262
|
+
const value = this.getValueFromConfigDefinition(configDefinition.key, configDefinition, h.defaultValue, h.context);
|
|
1263
|
+
h.handler(value);
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
watch(configKey, defaultValue, callback, context) {
|
|
1267
|
+
this.validateDefaultValue(defaultValue);
|
|
1268
|
+
const handlers = this.handlersMap.get(configKey);
|
|
1269
|
+
const handlerWithOptions = {
|
|
1270
|
+
handler: callback,
|
|
1271
|
+
defaultValue,
|
|
1272
|
+
requestedType: typeof defaultValue,
|
|
1273
|
+
context
|
|
1274
|
+
};
|
|
1275
|
+
if (handlers) handlers.push(handlerWithOptions);
|
|
1276
|
+
else this.handlersMap.set(configKey, [handlerWithOptions]);
|
|
1277
|
+
return () => this.unwatch(configKey, callback);
|
|
1278
|
+
}
|
|
1279
|
+
unwatch(configKey, callback) {
|
|
1280
|
+
const handlers = this.handlersMap.get(configKey);
|
|
1281
|
+
if (!handlers) return;
|
|
1282
|
+
if (callback) {
|
|
1283
|
+
const index = handlers.findIndex((h) => h.handler == callback);
|
|
1284
|
+
if (index >= 0) handlers?.splice(index, 1);
|
|
1285
|
+
} else handlers.splice(0);
|
|
1286
|
+
}
|
|
1287
|
+
getValue(configKey, defaultValue, context) {
|
|
1288
|
+
this.validateDefaultValue(defaultValue);
|
|
1289
|
+
const configDefinition = this.configSet?.configs[configKey];
|
|
1290
|
+
return this.getValueFromConfigDefinition(configKey, configDefinition, defaultValue, context);
|
|
1291
|
+
}
|
|
1292
|
+
getValueFromConfigDefinition(configKey, configDefinition, defaultValue, context) {
|
|
1293
|
+
if (configDefinition) {
|
|
1294
|
+
const parseResult = parseConfigValue(this.configEvaluator.evaluate(configDefinition, {
|
|
1295
|
+
context,
|
|
1296
|
+
metadata: this.metaContext
|
|
1297
|
+
}), defaultValue);
|
|
1298
|
+
this.usageEventCollector.evaluatedConfig({
|
|
1299
|
+
context,
|
|
1300
|
+
evaluation: {
|
|
1301
|
+
key: configKey,
|
|
1302
|
+
defaultValue,
|
|
1303
|
+
requestedType: parseResult.requestedType,
|
|
1304
|
+
evaluatedValue: parseResult.parsedValue,
|
|
1305
|
+
usedDefault: parseResult.usedDefault,
|
|
1306
|
+
evaluationReason: parseResult.reason
|
|
1307
|
+
}
|
|
1308
|
+
});
|
|
1309
|
+
this.logger.debug(`[ConfigDirectorClient] Evaluated '${configKey}' to '${parseResult.parsedValue}'`);
|
|
1310
|
+
return parseResult.parsedValue;
|
|
1311
|
+
} else {
|
|
1312
|
+
this.logger.debug(`[ConfigDirectorClient] No config state found for '${configKey}', returning default value '${defaultValue}'`);
|
|
1313
|
+
this.usageEventCollector.evaluatedConfig({
|
|
1314
|
+
context,
|
|
1315
|
+
evaluation: {
|
|
1316
|
+
key: configKey,
|
|
1317
|
+
defaultValue,
|
|
1318
|
+
requestedType: getRequestedType(defaultValue),
|
|
1319
|
+
evaluatedValue: defaultValue,
|
|
1320
|
+
usedDefault: true,
|
|
1321
|
+
evaluationReason: "config-state-missing"
|
|
1322
|
+
}
|
|
1323
|
+
});
|
|
1324
|
+
return defaultValue;
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
parseUrl(url) {
|
|
1328
|
+
if (!url) return;
|
|
1329
|
+
try {
|
|
1330
|
+
return new URL(url);
|
|
1331
|
+
} catch (error) {
|
|
1332
|
+
throw new ConfigDirectorValidationError(`Invalid base URL '${url}'. Parsing failed: ${error}`);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
validateDefaultValue(defaultValue) {
|
|
1336
|
+
if (defaultValue === void 0 || defaultValue === null) throw new ConfigDirectorValidationError("Invalid default value. The default value for a config must be defined and non-null.");
|
|
1337
|
+
if (typeof defaultValue === "function") throw new ConfigDirectorValidationError("Invalid default value. The default value for a config cannot be a function.");
|
|
1338
|
+
}
|
|
1339
|
+
get isReady() {
|
|
1340
|
+
return this.ready;
|
|
1341
|
+
}
|
|
1342
|
+
on(eventName, handler) {
|
|
1343
|
+
this.eventEmitter.on(eventName, handler);
|
|
1344
|
+
}
|
|
1345
|
+
off(eventName, handler) {
|
|
1346
|
+
if (handler) this.eventEmitter.off(eventName, handler);
|
|
1347
|
+
else this.eventEmitter.removeAllListeners(eventName);
|
|
1348
|
+
}
|
|
1349
|
+
removeAllObservers() {
|
|
1350
|
+
this.logger.debug("[ConfigDirectorClient] removeAllObservers() has been called, removing all observers");
|
|
1351
|
+
this.eventEmitter.removeAllListeners();
|
|
1352
|
+
this.handlersMap.clear();
|
|
1353
|
+
}
|
|
1354
|
+
unwatchAll() {
|
|
1355
|
+
this.handlersMap.clear();
|
|
1356
|
+
}
|
|
1357
|
+
closeConnection() {
|
|
1358
|
+
this.logger.debug("[ConfigDirectorClient] closeConnection() has been called, closing connection to server");
|
|
1359
|
+
this.transport.close();
|
|
1360
|
+
this.ready = false;
|
|
1361
|
+
}
|
|
1362
|
+
dispose() {
|
|
1363
|
+
this.removeAllObservers();
|
|
1364
|
+
this.closeConnection();
|
|
1365
|
+
}
|
|
1366
|
+
};
|
|
1367
|
+
//#endregion
|
|
1368
|
+
//#region src/runtime/logger.ts
|
|
1369
|
+
const createDefaultLogger = (level) => {
|
|
1370
|
+
const logger = consola.withTag("configdirector-nuxt-sdk");
|
|
1371
|
+
if (level !== void 0) logger.level = level;
|
|
1372
|
+
return logger;
|
|
1373
|
+
};
|
|
1374
|
+
//#endregion
|
|
1375
|
+
//#region src/runtime/nitro/plugin.ts
|
|
1376
|
+
var plugin_default = defineNitroPlugin((nitroApp) => {
|
|
1377
|
+
const runtimeConfig = useRuntimeConfig();
|
|
1378
|
+
const logger = createDefaultLogger(runtimeConfig.configdirector?.logLevel);
|
|
1379
|
+
if (!runtimeConfig.configdirector.serverSdkKey) {
|
|
1380
|
+
const message = "The ConfigDirector serverSdkKey must be configured for the plugin to initialize. You can provide it in nuxt.config.ts under 'runtimeConfig.configdirector.serverSdkKey' or as a runtime environment variable named NUXT_CONFIGDIRECTOR_SERVER_SDK_KEY";
|
|
1381
|
+
logger.error(message);
|
|
1382
|
+
throw new ConfigDirectorInitializationError(message);
|
|
1383
|
+
}
|
|
1384
|
+
logger.debug("Initializing ConfigDirector Nitro plugin");
|
|
1385
|
+
const { serverSdkKey, baseUrl } = runtimeConfig.configdirector;
|
|
1386
|
+
const client = new DefaultConfigDirectorClient(serverSdkKey, {
|
|
1387
|
+
sdkName: "nuxt-sdk",
|
|
1388
|
+
sdkVersion: "0.1.0"
|
|
1389
|
+
}, {
|
|
1390
|
+
logger,
|
|
1391
|
+
...baseUrl ? { connection: { url: baseUrl } } : {}
|
|
1392
|
+
});
|
|
1393
|
+
nitroApp.configDirectorClient = client;
|
|
1394
|
+
client.initialize();
|
|
1395
|
+
});
|
|
1396
|
+
//#endregion
|
|
1397
|
+
export { plugin_default as default };
|