@camunda/ccma-api 0.1.0 → 0.4.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/dist/index.d.ts +31 -7
- package/dist/index.js +56 -2
- package/dist/index.js.map +2 -2
- package/dist/services/accounts/admin-tools.service.d.ts +27 -0
- package/dist/services/accounts/admin-tools.service.js +70 -0
- package/dist/services/accounts/admin-tools.service.js.map +7 -0
- package/dist/services/accounts/connections.service.d.ts +85 -0
- package/dist/services/accounts/connections.service.js +150 -0
- package/dist/services/accounts/connections.service.js.map +2 -2
- package/dist/services/accounts/early-access-features.service.d.ts +59 -0
- package/dist/services/accounts/early-access-features.service.js +142 -0
- package/dist/services/accounts/early-access-features.service.js.map +2 -2
- package/dist/services/accounts/invites.service.d.ts +28 -0
- package/dist/services/accounts/invites.service.js +53 -0
- package/dist/services/accounts/invites.service.js.map +7 -0
- package/dist/services/accounts/orgs.service.d.ts +42 -0
- package/dist/services/accounts/orgs.service.js +123 -0
- package/dist/services/accounts/orgs.service.js.map +2 -2
- package/dist/services/accounts/orgs.v2.service.d.ts +6 -0
- package/dist/services/accounts/orgs.v2.service.js +34 -0
- package/dist/services/accounts/orgs.v2.service.js.map +2 -2
- package/dist/services/accounts/pdf-watermark.service.d.ts +93 -0
- package/dist/services/accounts/pdf-watermark.service.js +147 -0
- package/dist/services/accounts/pdf-watermark.service.js.map +7 -0
- package/dist/services/accounts/release-train.service.d.ts +25 -0
- package/dist/services/accounts/release-train.service.js +46 -0
- package/dist/services/accounts/release-train.service.js.map +7 -0
- package/dist/services/accounts/salesplan.service.d.ts +45 -0
- package/dist/services/accounts/salesplan.service.js +185 -0
- package/dist/services/accounts/salesplan.service.js.map +2 -2
- package/dist/services/accounts/token-issuer.service.d.ts +31 -0
- package/dist/services/accounts/token-issuer.service.js +63 -0
- package/dist/services/accounts/token-issuer.service.js.map +7 -0
- package/dist/services/accounts/users.service.d.ts +59 -0
- package/dist/services/accounts/users.service.js +296 -4
- package/dist/services/accounts/users.service.js.map +2 -2
- package/dist/services/console/console.admin.service.d.ts +21 -1
- package/dist/services/console/console.admin.service.js +75 -0
- package/dist/services/console/console.admin.service.js.map +2 -2
- package/dist/services/console/console.channel.service.d.ts +49 -0
- package/dist/services/console/console.channel.service.js +123 -0
- package/dist/services/console/console.channel.service.js.map +7 -0
- package/dist/services/console/console.cluster.service.d.ts +23 -11
- package/dist/services/console/console.cluster.service.js +36 -1
- package/dist/services/console/console.cluster.service.js.map +2 -2
- package/dist/services/console/console.cluster.v2.service.d.ts +18 -1
- package/dist/services/console/console.cluster.v2.service.js +95 -0
- package/dist/services/console/console.cluster.v2.service.js.map +2 -2
- package/dist/services/console/console.clusterplan.service.d.ts +21 -0
- package/dist/services/console/console.clusterplan.service.js +92 -0
- package/dist/services/console/console.clusterplan.service.js.map +2 -2
- package/dist/services/console/console.clusterplantype.service.d.ts +54 -0
- package/dist/services/console/console.clusterplantype.service.js +152 -0
- package/dist/services/console/console.clusterplantype.service.js.map +7 -0
- package/dist/services/console/console.k8scontext.service.d.ts +10 -0
- package/dist/services/console/console.k8scontext.service.js +41 -0
- package/dist/services/console/console.k8scontext.service.js.map +7 -0
- package/dist/services/console/console.storybook-config.service.d.ts +10 -0
- package/dist/services/console/console.storybook-config.service.js +40 -0
- package/dist/services/console/console.storybook-config.service.js.map +7 -0
- package/dist/services/console/generation.service.d.ts +45 -0
- package/dist/services/console/generation.service.js +143 -0
- package/dist/services/console/generation.service.js.map +2 -2
- package/dist/services/external/external.accounts.service.d.ts +27 -0
- package/dist/services/external/external.accounts.service.js +80 -0
- package/dist/services/external/external.accounts.service.js.map +7 -0
- package/dist/services/external/external.console.service.d.ts +37 -0
- package/dist/services/external/external.console.service.js +81 -0
- package/dist/services/external/external.console.service.js.map +7 -0
- package/dist/services/external/external.notifications.service.d.ts +78 -0
- package/dist/services/external/external.notifications.service.js +220 -0
- package/dist/services/external/external.notifications.service.js.map +7 -0
- package/dist/services/external/external.services.spec.d.ts +1 -0
- package/dist/services/notifications/notifications.service.d.ts +45 -1
- package/dist/services/notifications/notifications.service.js +168 -1
- package/dist/services/notifications/notifications.service.js.map +2 -2
- package/dist/types/general.types.d.ts +9 -1
- package/dist/utils/api.d.ts +14 -1
- package/dist/utils/api.js +71 -6
- package/dist/utils/api.js.map +2 -2
- package/dist/utils/api.request-headers.spec.d.ts +1 -0
- package/dist/utils/api.transient-retry.spec.d.ts +1 -0
- package/dist/utils/sse.d.ts +51 -0
- package/dist/utils/sse.js +254 -0
- package/dist/utils/sse.js.map +7 -0
- package/dist/utils/sse.spec.d.ts +1 -0
- package/dist/utils/traceparent.d.ts +30 -0
- package/dist/utils/traceparent.js +46 -0
- package/dist/utils/traceparent.js.map +7 -0
- package/dist/utils/traceparent.spec.d.ts +1 -0
- package/package.json +2 -2
package/dist/utils/api.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { getEnvParam } from "./environments.js";
|
|
2
2
|
import { frontendCache } from "./frontendCache.js";
|
|
3
|
+
const TRANSIENT_RETRY_STATUS = [502, 503, 504];
|
|
4
|
+
const MAX_TRANSIENT_RETRIES = 3;
|
|
3
5
|
function sendMetrics(payload, type, options) {
|
|
4
6
|
if (payload.config.metrics) {
|
|
5
7
|
if (payload.config.metrics.types.includes(type)) {
|
|
@@ -97,6 +99,34 @@ function rateLimitWaitingTime(attempt) {
|
|
|
97
99
|
const waitingTime = 2 ** attempt * 1e3 + noise;
|
|
98
100
|
return Math.floor(waitingTime);
|
|
99
101
|
}
|
|
102
|
+
function transientRetryWaitingTime(attempt) {
|
|
103
|
+
const noise = Math.random() * 100;
|
|
104
|
+
return Math.floor(2 ** attempt * 250 + noise);
|
|
105
|
+
}
|
|
106
|
+
function canRetryTransient(payload) {
|
|
107
|
+
if (!payload.retryTransientErrors) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
const remaining = payload.transientRetry?.remaining ?? MAX_TRANSIENT_RETRIES;
|
|
111
|
+
return remaining > 0;
|
|
112
|
+
}
|
|
113
|
+
async function scheduleTransientRetry(payload) {
|
|
114
|
+
const attempts = payload.transientRetry?.attempts ?? 1;
|
|
115
|
+
const remaining = payload.transientRetry?.remaining ?? MAX_TRANSIENT_RETRIES;
|
|
116
|
+
const waitingTime = transientRetryWaitingTime(attempts);
|
|
117
|
+
log(
|
|
118
|
+
payload,
|
|
119
|
+
"debug",
|
|
120
|
+
`transient error, attempt ${attempts}, remaining ${remaining}, waiting ${waitingTime}ms`
|
|
121
|
+
);
|
|
122
|
+
await new Promise((resolve) => {
|
|
123
|
+
setTimeout(resolve, waitingTime);
|
|
124
|
+
});
|
|
125
|
+
payload.transientRetry = {
|
|
126
|
+
attempts: attempts + 1,
|
|
127
|
+
remaining: remaining - 1
|
|
128
|
+
};
|
|
129
|
+
}
|
|
100
130
|
async function request(payload) {
|
|
101
131
|
log(payload, "debug", "starting request");
|
|
102
132
|
sendMetrics(payload, "request");
|
|
@@ -110,7 +140,17 @@ async function request(payload) {
|
|
|
110
140
|
if (payload.config.auth) {
|
|
111
141
|
token = await payload.config.auth.getToken();
|
|
112
142
|
}
|
|
113
|
-
|
|
143
|
+
let dynamicHeaders = {};
|
|
144
|
+
if (payload.metricsDetails?.target?.type !== "external") {
|
|
145
|
+
try {
|
|
146
|
+
dynamicHeaders = payload.config.getDynamicHeaders?.() ?? {};
|
|
147
|
+
} catch (_error) {
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const headers = {
|
|
151
|
+
...dynamicHeaders,
|
|
152
|
+
...payload.headers ?? {}
|
|
153
|
+
};
|
|
114
154
|
if (token) {
|
|
115
155
|
headers.Authorization = `Bearer ${token}`;
|
|
116
156
|
}
|
|
@@ -296,10 +336,13 @@ async function request(payload) {
|
|
|
296
336
|
case "text":
|
|
297
337
|
response.body = await fetchResponse.text();
|
|
298
338
|
break;
|
|
339
|
+
case "blob":
|
|
340
|
+
response.body = await fetchResponse.blob();
|
|
341
|
+
break;
|
|
299
342
|
default:
|
|
300
343
|
response.body = await fetchResponse.text();
|
|
301
344
|
}
|
|
302
|
-
if (payload.method === "get" && payload.config.frontendCaching?.enabled && response.body !== void 0) {
|
|
345
|
+
if (payload.method === "get" && payload.config.frontendCaching?.enabled && response.body !== void 0 && responseType !== "blob") {
|
|
303
346
|
try {
|
|
304
347
|
await frontendCache.set("get", url, response.body);
|
|
305
348
|
log(payload, "debug", "cache store successful");
|
|
@@ -321,6 +364,15 @@ async function request(payload) {
|
|
|
321
364
|
baseUrl: base
|
|
322
365
|
}
|
|
323
366
|
);
|
|
367
|
+
if (TRANSIENT_RETRY_STATUS.includes(response.status) && canRetryTransient(payload)) {
|
|
368
|
+
sendMetrics(payload, "transient-retry", {
|
|
369
|
+
duration,
|
|
370
|
+
statusCode: String(fetchResponse.status),
|
|
371
|
+
error: statusText
|
|
372
|
+
});
|
|
373
|
+
await scheduleTransientRetry(payload);
|
|
374
|
+
return request(payload);
|
|
375
|
+
}
|
|
324
376
|
let type2 = "error";
|
|
325
377
|
const noErrorStatusCodes = [];
|
|
326
378
|
switch (payload.metricsDetails.target.name) {
|
|
@@ -365,10 +417,6 @@ async function request(payload) {
|
|
|
365
417
|
}
|
|
366
418
|
return response;
|
|
367
419
|
} catch (error) {
|
|
368
|
-
log(payload, "error", `request failed with no status`, {
|
|
369
|
-
baseUrl: base,
|
|
370
|
-
error
|
|
371
|
-
});
|
|
372
420
|
const duration = performance.now() - start;
|
|
373
421
|
let errorMessage;
|
|
374
422
|
if (error?.cause?.code) {
|
|
@@ -376,6 +424,23 @@ async function request(payload) {
|
|
|
376
424
|
} else if (error?.message) {
|
|
377
425
|
errorMessage = error?.message;
|
|
378
426
|
}
|
|
427
|
+
if (canRetryTransient(payload)) {
|
|
428
|
+
log(payload, "debug", `request failed with no status, retrying`, {
|
|
429
|
+
baseUrl: base,
|
|
430
|
+
error
|
|
431
|
+
});
|
|
432
|
+
sendMetrics(payload, "transient-retry", {
|
|
433
|
+
statusCode: "fetch-error",
|
|
434
|
+
duration,
|
|
435
|
+
error: errorMessage
|
|
436
|
+
});
|
|
437
|
+
await scheduleTransientRetry(payload);
|
|
438
|
+
return request(payload);
|
|
439
|
+
}
|
|
440
|
+
log(payload, "error", `request failed with no status`, {
|
|
441
|
+
baseUrl: base,
|
|
442
|
+
error
|
|
443
|
+
});
|
|
379
444
|
sendMetrics(payload, "error", {
|
|
380
445
|
statusCode: "fetch-error",
|
|
381
446
|
duration,
|
package/dist/utils/api.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/utils/api.ts"],
|
|
4
|
-
"sourcesContent": ["/*\n * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH\n * under one or more contributor license agreements. Licensed under a commercial license.\n * You may not use this file except in compliance with the commercial license.\n */\n\nimport type {\n RequestBaseCustomMetricsDetailsType,\n RequestBaseMethodType,\n RequestBaseParams,\n RequestBaseServiceContext,\n} from '../types/general.types.js'\nimport { getEnvParam, type ServiceName } from './environments.js'\nimport { frontendCache } from './frontendCache.js'\n\nexport interface RequestPayload extends RequestBaseParams {\n base?: {\n id: ServiceName\n }\n url: string\n method: RequestBaseMethodType\n headers?: { [key: string]: string }\n type?: 'json' | 'text' | 'form' | 'none'\n responseType?: 'json' | 'text' | 'none'\n retry?: {\n attempts: number\n remaining: number\n }\n body?: any\n metricsDetails: RequestBaseServiceContext\n}\n\nexport interface Response<T> {\n body?: T\n status: number\n statusText?: string\n ok: boolean\n meta: {\n duration: number\n }\n}\n\nfunction sendMetrics(\n payload: RequestPayload,\n type: RequestBaseCustomMetricsDetailsType,\n options?: {\n duration?: number\n statusCode?: string\n error?: string\n },\n) {\n if (payload.config.metrics) {\n if (payload.config.metrics.types.includes(type)) {\n let durationBucket: string | undefined\n if (options?.duration) {\n if (options.duration < 100) {\n durationBucket = '0 | <100ms'\n } else if (options.duration < 200) {\n durationBucket = '1 | <200ms'\n } else if (options.duration < 500) {\n durationBucket = '2 | <500ms'\n } else if (options.duration < 1000) {\n durationBucket = '3 | <1s'\n } else {\n durationBucket = '4 | >=1s'\n }\n }\n\n // only add normalized errors as labels\n let error = 'other'\n if (options?.error) {\n try {\n const errorMessage = options.error\n if (errorMessage.startsWith('<!DOCTYPE html>')) {\n error = 'html-response'\n } else if (errorMessage.includes('UND_ERR_CONNECT_TIMEOUT')) {\n error = 'connect-timeout'\n }\n } catch (_error) {\n // ignore it\n }\n } else {\n error = 'unset'\n }\n\n const metrics: RequestBaseServiceContext = {\n ...payload.metricsDetails,\n eventType: type,\n durationBucket,\n statusCode: options?.statusCode,\n error,\n }\n try {\n payload.config.metrics.callback(metrics)\n } catch (metricsCallbackError) {\n log(payload, 'debug', `Failed to send metrics`, {\n error: metricsCallbackError,\n metrics,\n })\n }\n }\n }\n}\n\nexport function log(\n payload: RequestPayload,\n type: 'debug' | 'info' | 'error',\n message: string,\n additionalData?: any,\n) {\n if (payload.config.logger) {\n const messageParts: string[] = []\n messageParts.push(payload.metricsDetails?.method?.name)\n messageParts.push(\n `${payload.metricsDetails?.target?.name} (${payload.metricsDetails?.target?.type})`,\n )\n messageParts.push(message)\n let body: any\n if (payload.body) {\n body = JSON.parse(JSON.stringify(payload.body))\n for (const key of Object.keys(body)) {\n if (\n key.toLowerCase().includes('password') ||\n key.toLowerCase().includes('secret') ||\n key.toLowerCase().includes('token')\n ) {\n body[key] = '********'\n }\n }\n }\n\n let fullUrl = 'unset'\n\n if (payload.base) {\n try {\n const base = getEnvParam(payload.base.id, payload.config)\n fullUrl = base ? `${base}/${payload.url}` : payload.url\n } catch (_error) {\n fullUrl = `failed to get base url for ${payload.base.id}`\n }\n }\n\n let url = `${payload.url}`.toLowerCase()\n if (url.includes('secret') || url.includes('password')) {\n url = url.split('?')[0]\n }\n\n const data = {\n base: payload.base,\n method: payload.method,\n url,\n fullUrl,\n retry: payload.retry,\n body,\n context: payload.metricsDetails,\n ...additionalData,\n }\n payload.config.logger[type](messageParts.join(' > '), { data })\n }\n}\n\nfunction rateLimitWaitingTime(attempt: number): number {\n const noise = Math.random() * 100\n const waitingTime = 2 ** attempt * 1000 + noise\n return Math.floor(waitingTime)\n}\n\nexport async function request<T>(\n payload: RequestPayload,\n): Promise<Response<T>> {\n log(payload, 'debug', 'starting request')\n sendMetrics(payload, 'request')\n // configure frontend cache if enabled (browser only)\n try {\n if (payload.config.frontendCaching?.enabled) {\n frontendCache.configure(payload.config.frontendCaching)\n }\n } catch (_cacheConfigError) {\n // ignore configuration errors silently\n }\n let token\n if (payload.config.auth) {\n token = await payload.config.auth.getToken()\n }\n const headers: { [key: string]: string } = payload.headers ?? {}\n if (token) {\n headers.Authorization = `Bearer ${token}`\n }\n const type = payload.type ?? 'json'\n let body\n if (type) {\n switch (type) {\n case 'json':\n headers['Content-Type'] = 'application/json'\n body = payload.body ? JSON.stringify(payload.body) : undefined\n break\n case 'text':\n headers['Content-Type'] = 'text/plain'\n body = payload.body ? JSON.stringify(payload.body) : undefined\n break\n case 'form':\n if (payload.body) {\n const formData = new URLSearchParams()\n Object.keys(payload.body).forEach((key) => {\n formData.append(key, payload.body[key])\n })\n body = formData\n }\n break\n }\n }\n let base\n if (payload.base) {\n try {\n base = getEnvParam(payload.base.id, payload.config)\n } catch (error) {\n log(payload, 'error', `Failed to get base url for ${payload.base.id}`, {\n error,\n })\n throw error\n }\n }\n const url = base ? `${base}/${payload.url}` : payload.url\n\n // Attempt cache read for GET requests (stale-while-revalidate when callback supplied)\n let serveCachedAndReturnAfterNetwork = false\n if (payload.method === 'get' && payload.config.frontendCaching?.enabled) {\n try {\n const cached = await frontendCache.get<T>('get', url)\n if (cached) {\n const fresh = frontendCache.isFresh(cached)\n if (payload.onCachedResponse) {\n payload.onCachedResponse(cached.payload, { fresh, source: 'cache' })\n if (!fresh) {\n log(payload, 'debug', 'cache stale, refreshing in background')\n serveCachedAndReturnAfterNetwork = true\n } else {\n log(payload, 'debug', 'cache fresh, returning early')\n sendMetrics(payload, 'success', { duration: 0, statusCode: '200' })\n return {\n // eslint-disable-next-line id-length\n ok: true,\n status: 200,\n statusText: 'cached',\n body: cached.payload,\n meta: { duration: 0 },\n }\n }\n } else if (fresh) {\n // no callback but fresh -> return directly\n log(payload, 'debug', 'cache hit, returning cached response')\n sendMetrics(payload, 'success', { duration: 0, statusCode: '200' })\n return {\n // eslint-disable-next-line id-length\n ok: true,\n status: 200,\n statusText: 'cached',\n body: cached.payload,\n meta: { duration: 0 },\n }\n } else {\n log(payload, 'debug', 'cache stale, proceeding with network request')\n }\n }\n } catch (_cacheGetError) {\n // ignore cache errors\n }\n }\n const start = performance.now()\n const proxyAgent = payload.config.proxyAgent ?? {}\n try {\n const fetchResponse = await fetch(url, {\n signal: payload.config.timeout\n ? AbortSignal.timeout(payload.config.timeout)\n : undefined,\n method: payload.method.toUpperCase(),\n headers,\n body,\n ...proxyAgent,\n })\n const end = performance.now()\n const duration = end - start\n\n if ([429].includes(fetchResponse.status)) {\n // rate limiting, retry\n log(payload, 'info', 'rate limited')\n sendMetrics(payload, 'rate-limit-retry', {\n duration,\n statusCode: String(fetchResponse.status),\n })\n const remaining = payload.retry?.remaining ?? 8\n const attempts = payload.retry?.attempts ?? 1\n if (remaining > 0) {\n let waitingTime = rateLimitWaitingTime(attempts)\n if (fetchResponse.headers.has('x-ratelimit-reset')) {\n const rateLimitHeader = fetchResponse.headers.get('x-ratelimit-reset')\n log(\n payload,\n 'debug',\n `rate limited, found x-ratelimit-reset header: ${rateLimitHeader}`,\n )\n const rateLimitHeaderNumber = Number(rateLimitHeader)\n if (Number.isNaN(rateLimitHeaderNumber)) {\n waitingTime = Date.parse(rateLimitHeader ?? '0') - Date.now()\n } else {\n waitingTime = rateLimitHeaderNumber - Date.now()\n }\n }\n if (waitingTime < 0 || waitingTime > 180000) {\n waitingTime = rateLimitWaitingTime(attempts)\n }\n log(\n payload,\n 'debug',\n `rate limited, attempt ${attempts}, remaining ${remaining}, waiting ${waitingTime}ms`,\n )\n await new Promise((resolve) => {\n setTimeout(resolve, waitingTime)\n })\n // eslint-disable-next-line require-atomic-updates\n payload.retry = {\n attempts: attempts + 1,\n remaining: remaining - 1,\n }\n return request(payload)\n }\n log(payload, 'debug', `rate limited, no more retries`)\n }\n\n const response: Response<T> = {\n // eslint-disable-next-line id-length\n ok: fetchResponse.ok,\n status: fetchResponse.status,\n statusText: fetchResponse.statusText,\n meta: {\n duration,\n },\n }\n\n const responseType = payload.responseType ?? type\n\n if (response.ok) {\n log(payload, 'debug', `request successful in ${Math.floor(duration)}ms`, {\n baseUrl: base,\n })\n sendMetrics(payload, 'success', {\n duration,\n statusCode: String(fetchResponse.status),\n })\n switch (responseType) {\n case 'json':\n try {\n response.body = await fetchResponse.json()\n } catch (_error) {\n log(\n payload,\n 'debug',\n `Failed to parse json body. Returning empty object.`,\n {\n baseUrl: base,\n },\n )\n response.body = {} as T\n try {\n const responseBodyText = await fetchResponse.text()\n log(payload, 'debug', `Response body text`, {\n baseUrl: base,\n responseBodyText,\n status: fetchResponse.status,\n })\n } catch (textError) {\n // this is fine\n log(payload, 'debug', `Failed to parse response body text.`, {\n baseUrl: base,\n status: fetchResponse.status,\n error: textError,\n })\n }\n }\n break\n case 'text':\n response.body = (await fetchResponse.text()) as T\n break\n default:\n response.body = (await fetchResponse.text()) as T\n }\n // store in cache after parsing\n if (\n payload.method === 'get' &&\n payload.config.frontendCaching?.enabled &&\n response.body !== undefined\n ) {\n try {\n await frontendCache.set('get', url, response.body as any)\n log(payload, 'debug', 'cache store successful')\n } catch (_cacheSetError) {\n // ignore cache set error\n }\n }\n } else {\n let statusText = fetchResponse.statusText\n try {\n statusText = await fetchResponse.text()\n } catch (_error) {\n // this is fine\n }\n response.statusText = statusText\n log(\n payload,\n 'debug',\n `request failed with status ${response.status}: ${statusText}`,\n {\n baseUrl: base,\n },\n )\n\n // Special Handling for dedicated target systems to not count special status codes as errors\n // eslint-disable-next-line no-shadow\n let type: RequestBaseCustomMetricsDetailsType = 'error'\n const noErrorStatusCodes: number[] = []\n switch (payload.metricsDetails.target.name) {\n case 'auth0':\n noErrorStatusCodes.push(...[400, 404, 409])\n break\n case 'accounts':\n case 'console':\n noErrorStatusCodes.push(...[404, 409])\n if (payload.metricsDetails.method.name === 'createTemporaryCluster') {\n // case: cannot create temporary cluster\n noErrorStatusCodes.push(...[400, 403, 412])\n }\n if (\n [\n 'ConsoleM2MService.verifyZeebeClient',\n 'ConsoleM2MService.verifyApiClient',\n ].includes(payload.metricsDetails.method.name)\n ) {\n // case: api/zeebe client credentials are invalid\n noErrorStatusCodes.push(...[401])\n }\n if (\n payload.metricsDetails.method.name ===\n 'OrgsService.createAndReturnSocialLoginUser'\n ) {\n noErrorStatusCodes.push(\n ...[\n // onboard user, user already has an org\n 400,\n // existing extSSO-connection\n 412,\n ],\n )\n }\n break\n }\n\n if (noErrorStatusCodes.includes(response.status)) {\n type = 'success'\n }\n\n sendMetrics(payload, type, {\n duration,\n statusCode: String(fetchResponse.status),\n error: statusText,\n })\n }\n if (serveCachedAndReturnAfterNetwork && response.ok) {\n // when we served stale via callback earlier, we now also return the fresh response\n log(payload, 'debug', 'stale cache updated, returning new data')\n }\n return response\n } catch (error: any) {\n log(payload, 'error', `request failed with no status`, {\n baseUrl: base,\n error,\n })\n const duration = performance.now() - start\n let errorMessage: string | undefined\n if (error?.cause?.code) {\n errorMessage = error?.cause?.code\n } else if (error?.message) {\n errorMessage = error?.message\n }\n sendMetrics(payload, 'error', {\n statusCode: 'fetch-error',\n duration,\n error: errorMessage,\n })\n const response: Response<T> = {\n // eslint-disable-next-line id-length\n ok: false,\n status: 500,\n statusText: 'fetch error',\n meta: {\n duration,\n },\n }\n return response\n }\n}\n"],
|
|
5
|
-
"mappings": "AAYA,SAAS,mBAAqC;AAC9C,SAAS,qBAAqB;
|
|
4
|
+
"sourcesContent": ["/*\n * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH\n * under one or more contributor license agreements. Licensed under a commercial license.\n * You may not use this file except in compliance with the commercial license.\n */\n\nimport type {\n RequestBaseCustomMetricsDetailsType,\n RequestBaseMethodType,\n RequestBaseParams,\n RequestBaseServiceContext,\n} from '../types/general.types.js'\nimport { getEnvParam, type ServiceName } from './environments.js'\nimport { frontendCache } from './frontendCache.js'\n\nexport interface RequestPayload extends RequestBaseParams {\n base?: {\n id: ServiceName\n }\n url: string\n method: RequestBaseMethodType\n headers?: { [key: string]: string }\n type?: 'json' | 'text' | 'form' | 'none'\n responseType?: 'json' | 'text' | 'blob' | 'none'\n retry?: {\n attempts: number\n remaining: number\n }\n /**\n * Opt-in: retry transient gateway failures (502/503/504 and network/fetch\n * errors) with backoff, mirroring the 429 retry. Retried attempts emit a\n * `transient-retry` metric instead of `error`, so a healed blip (e.g. an\n * envoy connection reset during a pod restart) doesn't count toward the\n * internal error-rate alert. Only enable for idempotent-safe calls \u2014 a\n * retried POST may execute twice.\n */\n retryTransientErrors?: boolean\n transientRetry?: {\n attempts: number\n remaining: number\n }\n body?: any\n metricsDetails: RequestBaseServiceContext\n}\n\nconst TRANSIENT_RETRY_STATUS = [502, 503, 504]\nconst MAX_TRANSIENT_RETRIES = 3\n\nexport interface Response<T> {\n body?: T\n status: number\n statusText?: string\n ok: boolean\n meta: {\n duration: number\n }\n}\n\nfunction sendMetrics(\n payload: RequestPayload,\n type: RequestBaseCustomMetricsDetailsType,\n options?: {\n duration?: number\n statusCode?: string\n error?: string\n },\n) {\n if (payload.config.metrics) {\n if (payload.config.metrics.types.includes(type)) {\n let durationBucket: string | undefined\n if (options?.duration) {\n if (options.duration < 100) {\n durationBucket = '0 | <100ms'\n } else if (options.duration < 200) {\n durationBucket = '1 | <200ms'\n } else if (options.duration < 500) {\n durationBucket = '2 | <500ms'\n } else if (options.duration < 1000) {\n durationBucket = '3 | <1s'\n } else {\n durationBucket = '4 | >=1s'\n }\n }\n\n // only add normalized errors as labels\n let error = 'other'\n if (options?.error) {\n try {\n const errorMessage = options.error\n if (errorMessage.startsWith('<!DOCTYPE html>')) {\n error = 'html-response'\n } else if (errorMessage.includes('UND_ERR_CONNECT_TIMEOUT')) {\n error = 'connect-timeout'\n }\n } catch (_error) {\n // ignore it\n }\n } else {\n error = 'unset'\n }\n\n const metrics: RequestBaseServiceContext = {\n ...payload.metricsDetails,\n eventType: type,\n durationBucket,\n statusCode: options?.statusCode,\n error,\n }\n try {\n payload.config.metrics.callback(metrics)\n } catch (metricsCallbackError) {\n log(payload, 'debug', `Failed to send metrics`, {\n error: metricsCallbackError,\n metrics,\n })\n }\n }\n }\n}\n\nexport function log(\n payload: RequestPayload,\n type: 'debug' | 'info' | 'error',\n message: string,\n additionalData?: any,\n) {\n if (payload.config.logger) {\n const messageParts: string[] = []\n messageParts.push(payload.metricsDetails?.method?.name)\n messageParts.push(\n `${payload.metricsDetails?.target?.name} (${payload.metricsDetails?.target?.type})`,\n )\n messageParts.push(message)\n let body: any\n if (payload.body) {\n body = JSON.parse(JSON.stringify(payload.body))\n for (const key of Object.keys(body)) {\n if (\n key.toLowerCase().includes('password') ||\n key.toLowerCase().includes('secret') ||\n key.toLowerCase().includes('token')\n ) {\n body[key] = '********'\n }\n }\n }\n\n let fullUrl = 'unset'\n\n if (payload.base) {\n try {\n const base = getEnvParam(payload.base.id, payload.config)\n fullUrl = base ? `${base}/${payload.url}` : payload.url\n } catch (_error) {\n fullUrl = `failed to get base url for ${payload.base.id}`\n }\n }\n\n let url = `${payload.url}`.toLowerCase()\n if (url.includes('secret') || url.includes('password')) {\n url = url.split('?')[0]\n }\n\n const data = {\n base: payload.base,\n method: payload.method,\n url,\n fullUrl,\n retry: payload.retry,\n body,\n context: payload.metricsDetails,\n ...additionalData,\n }\n payload.config.logger[type](messageParts.join(' > '), { data })\n }\n}\n\nfunction rateLimitWaitingTime(attempt: number): number {\n const noise = Math.random() * 100\n const waitingTime = 2 ** attempt * 1000 + noise\n return Math.floor(waitingTime)\n}\n\nfunction transientRetryWaitingTime(attempt: number): number {\n const noise = Math.random() * 100\n // 500ms, 1s, 2s (+jitter) \u2014 bounded so a fire-and-forget call heals fast.\n return Math.floor(2 ** attempt * 250 + noise)\n}\n\nfunction canRetryTransient(payload: RequestPayload): boolean {\n if (!payload.retryTransientErrors) {\n return false\n }\n const remaining = payload.transientRetry?.remaining ?? MAX_TRANSIENT_RETRIES\n return remaining > 0\n}\n\nasync function scheduleTransientRetry(payload: RequestPayload): Promise<void> {\n const attempts = payload.transientRetry?.attempts ?? 1\n const remaining = payload.transientRetry?.remaining ?? MAX_TRANSIENT_RETRIES\n const waitingTime = transientRetryWaitingTime(attempts)\n log(\n payload,\n 'debug',\n `transient error, attempt ${attempts}, remaining ${remaining}, waiting ${waitingTime}ms`,\n )\n await new Promise((resolve) => {\n setTimeout(resolve, waitingTime)\n })\n // eslint-disable-next-line require-atomic-updates\n payload.transientRetry = {\n attempts: attempts + 1,\n remaining: remaining - 1,\n }\n}\n\nexport async function request<T>(\n payload: RequestPayload,\n): Promise<Response<T>> {\n log(payload, 'debug', 'starting request')\n sendMetrics(payload, 'request')\n // configure frontend cache if enabled (browser only)\n try {\n if (payload.config.frontendCaching?.enabled) {\n frontendCache.configure(payload.config.frontendCaching)\n }\n } catch (_cacheConfigError) {\n // ignore configuration errors silently\n }\n let token\n if (payload.config.auth) {\n token = await payload.config.auth.getToken()\n }\n // Dynamic (e.g. correlation) headers first, then explicit per-request headers\n // override them, then auth/content-type below take final precedence.\n //\n // Correlation headers (traceparent, x-cc-session-id) identify our internal\n // request/session, so they must only ever reach Camunda-owned systems. Every\n // third-party target is marked `type: 'external'`, so we gate on that here:\n // external targets never receive dynamic headers even if the caller's config\n // sets getDynamicHeaders. This makes propagation safe-by-default and removes\n // the footgun of a shared config that is used for both internal and\n // third-party calls (e.g. BASIC_API_CONFIG \u2192 token-issuer vs Slack).\n let dynamicHeaders: Record<string, string> = {}\n if (payload.metricsDetails?.target?.type !== 'external') {\n try {\n dynamicHeaders = payload.config.getDynamicHeaders?.() ?? {}\n } catch (_error) {\n // never let header evaluation break a request\n }\n }\n const headers: { [key: string]: string } = {\n ...dynamicHeaders,\n ...(payload.headers ?? {}),\n }\n if (token) {\n headers.Authorization = `Bearer ${token}`\n }\n const type = payload.type ?? 'json'\n let body\n if (type) {\n switch (type) {\n case 'json':\n headers['Content-Type'] = 'application/json'\n body = payload.body ? JSON.stringify(payload.body) : undefined\n break\n case 'text':\n headers['Content-Type'] = 'text/plain'\n body = payload.body ? JSON.stringify(payload.body) : undefined\n break\n case 'form':\n if (payload.body) {\n const formData = new URLSearchParams()\n Object.keys(payload.body).forEach((key) => {\n formData.append(key, payload.body[key])\n })\n body = formData\n }\n break\n }\n }\n let base\n if (payload.base) {\n try {\n base = getEnvParam(payload.base.id, payload.config)\n } catch (error) {\n log(payload, 'error', `Failed to get base url for ${payload.base.id}`, {\n error,\n })\n throw error\n }\n }\n const url = base ? `${base}/${payload.url}` : payload.url\n\n // Attempt cache read for GET requests (stale-while-revalidate when callback supplied)\n let serveCachedAndReturnAfterNetwork = false\n if (payload.method === 'get' && payload.config.frontendCaching?.enabled) {\n try {\n const cached = await frontendCache.get<T>('get', url)\n if (cached) {\n const fresh = frontendCache.isFresh(cached)\n if (payload.onCachedResponse) {\n payload.onCachedResponse(cached.payload, { fresh, source: 'cache' })\n if (!fresh) {\n log(payload, 'debug', 'cache stale, refreshing in background')\n serveCachedAndReturnAfterNetwork = true\n } else {\n log(payload, 'debug', 'cache fresh, returning early')\n sendMetrics(payload, 'success', { duration: 0, statusCode: '200' })\n return {\n // eslint-disable-next-line id-length\n ok: true,\n status: 200,\n statusText: 'cached',\n body: cached.payload,\n meta: { duration: 0 },\n }\n }\n } else if (fresh) {\n // no callback but fresh -> return directly\n log(payload, 'debug', 'cache hit, returning cached response')\n sendMetrics(payload, 'success', { duration: 0, statusCode: '200' })\n return {\n // eslint-disable-next-line id-length\n ok: true,\n status: 200,\n statusText: 'cached',\n body: cached.payload,\n meta: { duration: 0 },\n }\n } else {\n log(payload, 'debug', 'cache stale, proceeding with network request')\n }\n }\n } catch (_cacheGetError) {\n // ignore cache errors\n }\n }\n const start = performance.now()\n const proxyAgent = payload.config.proxyAgent ?? {}\n try {\n const fetchResponse = await fetch(url, {\n signal: payload.config.timeout\n ? AbortSignal.timeout(payload.config.timeout)\n : undefined,\n method: payload.method.toUpperCase(),\n headers,\n body,\n ...proxyAgent,\n })\n const end = performance.now()\n const duration = end - start\n\n if ([429].includes(fetchResponse.status)) {\n // rate limiting, retry\n log(payload, 'info', 'rate limited')\n sendMetrics(payload, 'rate-limit-retry', {\n duration,\n statusCode: String(fetchResponse.status),\n })\n const remaining = payload.retry?.remaining ?? 8\n const attempts = payload.retry?.attempts ?? 1\n if (remaining > 0) {\n let waitingTime = rateLimitWaitingTime(attempts)\n if (fetchResponse.headers.has('x-ratelimit-reset')) {\n const rateLimitHeader = fetchResponse.headers.get('x-ratelimit-reset')\n log(\n payload,\n 'debug',\n `rate limited, found x-ratelimit-reset header: ${rateLimitHeader}`,\n )\n const rateLimitHeaderNumber = Number(rateLimitHeader)\n if (Number.isNaN(rateLimitHeaderNumber)) {\n waitingTime = Date.parse(rateLimitHeader ?? '0') - Date.now()\n } else {\n waitingTime = rateLimitHeaderNumber - Date.now()\n }\n }\n if (waitingTime < 0 || waitingTime > 180000) {\n waitingTime = rateLimitWaitingTime(attempts)\n }\n log(\n payload,\n 'debug',\n `rate limited, attempt ${attempts}, remaining ${remaining}, waiting ${waitingTime}ms`,\n )\n await new Promise((resolve) => {\n setTimeout(resolve, waitingTime)\n })\n // eslint-disable-next-line require-atomic-updates\n payload.retry = {\n attempts: attempts + 1,\n remaining: remaining - 1,\n }\n return request(payload)\n }\n log(payload, 'debug', `rate limited, no more retries`)\n }\n\n const response: Response<T> = {\n // eslint-disable-next-line id-length\n ok: fetchResponse.ok,\n status: fetchResponse.status,\n statusText: fetchResponse.statusText,\n meta: {\n duration,\n },\n }\n\n const responseType = payload.responseType ?? type\n\n if (response.ok) {\n log(payload, 'debug', `request successful in ${Math.floor(duration)}ms`, {\n baseUrl: base,\n })\n sendMetrics(payload, 'success', {\n duration,\n statusCode: String(fetchResponse.status),\n })\n switch (responseType) {\n case 'json':\n try {\n response.body = await fetchResponse.json()\n } catch (_error) {\n log(\n payload,\n 'debug',\n `Failed to parse json body. Returning empty object.`,\n {\n baseUrl: base,\n },\n )\n response.body = {} as T\n try {\n const responseBodyText = await fetchResponse.text()\n log(payload, 'debug', `Response body text`, {\n baseUrl: base,\n responseBodyText,\n status: fetchResponse.status,\n })\n } catch (textError) {\n // this is fine\n log(payload, 'debug', `Failed to parse response body text.`, {\n baseUrl: base,\n status: fetchResponse.status,\n error: textError,\n })\n }\n }\n break\n case 'text':\n response.body = (await fetchResponse.text()) as T\n break\n case 'blob':\n // Binary payloads (file downloads, e.g. database dumps). Not cached.\n response.body = (await fetchResponse.blob()) as T\n break\n default:\n response.body = (await fetchResponse.text()) as T\n }\n // store in cache after parsing\n if (\n payload.method === 'get' &&\n payload.config.frontendCaching?.enabled &&\n response.body !== undefined &&\n responseType !== 'blob'\n ) {\n try {\n await frontendCache.set('get', url, response.body as any)\n log(payload, 'debug', 'cache store successful')\n } catch (_cacheSetError) {\n // ignore cache set error\n }\n }\n } else {\n let statusText = fetchResponse.statusText\n try {\n statusText = await fetchResponse.text()\n } catch (_error) {\n // this is fine\n }\n response.statusText = statusText\n log(\n payload,\n 'debug',\n `request failed with status ${response.status}: ${statusText}`,\n {\n baseUrl: base,\n },\n )\n\n // Transient gateway failure (e.g. envoy \"connection termination\" 503\n // during a pod restart). When opted in, retry with backoff and record it\n // as `transient-retry` rather than `error`, so a healed blip stays off\n // the internal error-rate alert.\n if (\n TRANSIENT_RETRY_STATUS.includes(response.status) &&\n canRetryTransient(payload)\n ) {\n sendMetrics(payload, 'transient-retry', {\n duration,\n statusCode: String(fetchResponse.status),\n error: statusText,\n })\n await scheduleTransientRetry(payload)\n return request(payload)\n }\n\n // Special Handling for dedicated target systems to not count special status codes as errors\n // eslint-disable-next-line no-shadow\n let type: RequestBaseCustomMetricsDetailsType = 'error'\n const noErrorStatusCodes: number[] = []\n switch (payload.metricsDetails.target.name) {\n case 'auth0':\n noErrorStatusCodes.push(...[400, 404, 409])\n break\n case 'accounts':\n case 'console':\n noErrorStatusCodes.push(...[404, 409])\n if (payload.metricsDetails.method.name === 'createTemporaryCluster') {\n // case: cannot create temporary cluster\n noErrorStatusCodes.push(...[400, 403, 412])\n }\n if (\n [\n 'ConsoleM2MService.verifyZeebeClient',\n 'ConsoleM2MService.verifyApiClient',\n ].includes(payload.metricsDetails.method.name)\n ) {\n // case: api/zeebe client credentials are invalid\n noErrorStatusCodes.push(...[401])\n }\n if (\n payload.metricsDetails.method.name ===\n 'OrgsService.createAndReturnSocialLoginUser'\n ) {\n noErrorStatusCodes.push(\n ...[\n // onboard user, user already has an org\n 400,\n // existing extSSO-connection\n 412,\n ],\n )\n }\n break\n }\n\n if (noErrorStatusCodes.includes(response.status)) {\n type = 'success'\n }\n\n sendMetrics(payload, type, {\n duration,\n statusCode: String(fetchResponse.status),\n error: statusText,\n })\n }\n if (serveCachedAndReturnAfterNetwork && response.ok) {\n // when we served stale via callback earlier, we now also return the fresh response\n log(payload, 'debug', 'stale cache updated, returning new data')\n }\n return response\n } catch (error: any) {\n const duration = performance.now() - start\n let errorMessage: string | undefined\n if (error?.cause?.code) {\n errorMessage = error?.cause?.code\n } else if (error?.message) {\n errorMessage = error?.message\n }\n // Network/fetch error with no HTTP status (e.g. connection reset, timeout).\n // Retry-on-opt-in before treating it as an error (see else branch above).\n if (canRetryTransient(payload)) {\n log(payload, 'debug', `request failed with no status, retrying`, {\n baseUrl: base,\n error,\n })\n sendMetrics(payload, 'transient-retry', {\n statusCode: 'fetch-error',\n duration,\n error: errorMessage,\n })\n await scheduleTransientRetry(payload)\n return request(payload)\n }\n log(payload, 'error', `request failed with no status`, {\n baseUrl: base,\n error,\n })\n sendMetrics(payload, 'error', {\n statusCode: 'fetch-error',\n duration,\n error: errorMessage,\n })\n const response: Response<T> = {\n // eslint-disable-next-line id-length\n ok: false,\n status: 500,\n statusText: 'fetch error',\n meta: {\n duration,\n },\n }\n return response\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["type"]
|
|
7
7
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { RequestBaseParamsConfig } from '../types/general.types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Dependency-free Server-Sent Events client built on `fetch` + `ReadableStream`.
|
|
4
|
+
*
|
|
5
|
+
* Why not the native `EventSource`? It cannot send an `Authorization` header,
|
|
6
|
+
* which every Camunda stream requires. This helper reuses the same
|
|
7
|
+
* `config.auth.getToken()` contract as `request()`, so it always sends a fresh
|
|
8
|
+
* bearer token — including on every reconnect, which is how token rotation is
|
|
9
|
+
* handled (no separate refresh timer needed on the consumer side).
|
|
10
|
+
*
|
|
11
|
+
* It is isomorphic (browser + Node 22+), auto-reconnects with exponential
|
|
12
|
+
* backoff + full jitter, honours `Last-Event-ID`, and aborts+reconnects when
|
|
13
|
+
* the stream goes silent past `heartbeatTimeoutMs` (comment/keep-alive frames
|
|
14
|
+
* count as activity, so a well-behaved server keeps the connection pinned).
|
|
15
|
+
*/
|
|
16
|
+
export type SseConnectionState = 'connecting' | 'open' | 'reconnecting' | 'closed';
|
|
17
|
+
export interface SseMessage {
|
|
18
|
+
/** Concatenated `data:` lines (newline-joined), excluding the trailing newline. */
|
|
19
|
+
data: string;
|
|
20
|
+
/** The `event:` field, if the server set one. */
|
|
21
|
+
event?: string;
|
|
22
|
+
/** The `id:` field, if the server set one. Echoed back as `Last-Event-ID`. */
|
|
23
|
+
id?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface SseConnectionOptions {
|
|
26
|
+
/** Absolute URL of the event stream. */
|
|
27
|
+
url: string;
|
|
28
|
+
/** Reuses `auth.getToken` (called per (re)connect) and `logger`. */
|
|
29
|
+
config: RequestBaseParamsConfig;
|
|
30
|
+
/** Called for each complete, non-empty SSE event (comment-only frames excluded). */
|
|
31
|
+
onMessage: (message: SseMessage) => void;
|
|
32
|
+
/** Observability hook for connection lifecycle transitions. */
|
|
33
|
+
onStateChange?: (state: SseConnectionState) => void;
|
|
34
|
+
/**
|
|
35
|
+
* Abort + reconnect when no bytes (including comments / keep-alives) arrive
|
|
36
|
+
* for this long. Default 120_000 ms.
|
|
37
|
+
*/
|
|
38
|
+
heartbeatTimeoutMs?: number;
|
|
39
|
+
/** Backoff tuning. Defaults: base 1_000 ms, max 30_000 ms, full jitter. */
|
|
40
|
+
reconnect?: {
|
|
41
|
+
baseDelayMs?: number;
|
|
42
|
+
maxDelayMs?: number;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export interface SseConnection {
|
|
46
|
+
/** Idempotent. Aborts any in-flight fetch, cancels timers, emits no further events. */
|
|
47
|
+
close: () => void;
|
|
48
|
+
/** Current lifecycle state. */
|
|
49
|
+
readonly state: SseConnectionState;
|
|
50
|
+
}
|
|
51
|
+
export declare function connectSse(options: SseConnectionOptions): SseConnection;
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
const DEFAULT_HEARTBEAT_TIMEOUT_MS = 12e4;
|
|
2
|
+
const DEFAULT_BASE_DELAY_MS = 1e3;
|
|
3
|
+
const DEFAULT_MAX_DELAY_MS = 3e4;
|
|
4
|
+
class SseParser {
|
|
5
|
+
constructor(onMessage, lastEventId) {
|
|
6
|
+
this.onMessage = onMessage;
|
|
7
|
+
this.buffer = "";
|
|
8
|
+
this.dataLines = [];
|
|
9
|
+
this.lastEventId = lastEventId;
|
|
10
|
+
}
|
|
11
|
+
getLastEventId() {
|
|
12
|
+
return this.lastEventId;
|
|
13
|
+
}
|
|
14
|
+
/** Feed a decoded text chunk. Returns nothing; dispatches via `onMessage`. */
|
|
15
|
+
push(chunk) {
|
|
16
|
+
this.buffer += chunk;
|
|
17
|
+
this.buffer = this.buffer.replace(/\r\n?/g, "\n");
|
|
18
|
+
let newlineIndex = this.buffer.indexOf("\n");
|
|
19
|
+
while (newlineIndex !== -1) {
|
|
20
|
+
const line = this.buffer.slice(0, newlineIndex);
|
|
21
|
+
this.buffer = this.buffer.slice(newlineIndex + 1);
|
|
22
|
+
this.handleLine(line);
|
|
23
|
+
newlineIndex = this.buffer.indexOf("\n");
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
handleLine(line) {
|
|
27
|
+
if (line === "") {
|
|
28
|
+
this.dispatch();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (line.startsWith(":")) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const colonIndex = line.indexOf(":");
|
|
35
|
+
const field = colonIndex === -1 ? line : line.slice(0, colonIndex);
|
|
36
|
+
let value = colonIndex === -1 ? "" : line.slice(colonIndex + 1);
|
|
37
|
+
if (value.startsWith(" ")) {
|
|
38
|
+
value = value.slice(1);
|
|
39
|
+
}
|
|
40
|
+
switch (field) {
|
|
41
|
+
case "data":
|
|
42
|
+
this.dataLines.push(value);
|
|
43
|
+
break;
|
|
44
|
+
case "event":
|
|
45
|
+
this.eventName = value;
|
|
46
|
+
break;
|
|
47
|
+
case "id":
|
|
48
|
+
if (!value.includes("\0")) {
|
|
49
|
+
this.lastEventId = value;
|
|
50
|
+
}
|
|
51
|
+
break;
|
|
52
|
+
// `retry:` and unknown fields are intentionally ignored.
|
|
53
|
+
default:
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
dispatch() {
|
|
58
|
+
if (this.dataLines.length === 0 && this.eventName === void 0) {
|
|
59
|
+
this.reset();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
this.onMessage({
|
|
63
|
+
data: this.dataLines.join("\n"),
|
|
64
|
+
event: this.eventName,
|
|
65
|
+
id: this.lastEventId
|
|
66
|
+
});
|
|
67
|
+
this.reset();
|
|
68
|
+
}
|
|
69
|
+
reset() {
|
|
70
|
+
this.dataLines = [];
|
|
71
|
+
this.eventName = void 0;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function computeBackoff(attempt, baseDelayMs, maxDelayMs) {
|
|
75
|
+
const exponential = Math.min(maxDelayMs, baseDelayMs * 2 ** attempt);
|
|
76
|
+
return Math.floor(Math.random() * exponential);
|
|
77
|
+
}
|
|
78
|
+
function connectSse(options) {
|
|
79
|
+
const heartbeatTimeoutMs = options.heartbeatTimeoutMs ?? DEFAULT_HEARTBEAT_TIMEOUT_MS;
|
|
80
|
+
const baseDelayMs = options.reconnect?.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
|
|
81
|
+
const maxDelayMs = options.reconnect?.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
|
|
82
|
+
const logger = options.config.logger;
|
|
83
|
+
let state = "connecting";
|
|
84
|
+
let closed = false;
|
|
85
|
+
let attempt = 0;
|
|
86
|
+
let lastEventId;
|
|
87
|
+
let controller;
|
|
88
|
+
let heartbeatTimer;
|
|
89
|
+
let reconnectTimer;
|
|
90
|
+
let authRetryUsed = false;
|
|
91
|
+
const setState = (next) => {
|
|
92
|
+
if (state === next || closed) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
state = next;
|
|
96
|
+
try {
|
|
97
|
+
options.onStateChange?.(next);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
logger?.debug("connectSse onStateChange callback threw", { error });
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
const clearHeartbeat = () => {
|
|
103
|
+
if (heartbeatTimer !== void 0) {
|
|
104
|
+
clearTimeout(heartbeatTimer);
|
|
105
|
+
heartbeatTimer = void 0;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
const armHeartbeat = () => {
|
|
109
|
+
clearHeartbeat();
|
|
110
|
+
heartbeatTimer = setTimeout(() => {
|
|
111
|
+
logger?.debug("connectSse heartbeat timeout, reconnecting", {
|
|
112
|
+
url: options.url
|
|
113
|
+
});
|
|
114
|
+
controller?.abort();
|
|
115
|
+
}, heartbeatTimeoutMs);
|
|
116
|
+
};
|
|
117
|
+
const scheduleReconnect = () => {
|
|
118
|
+
if (closed) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (reconnectTimer !== void 0) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
setState("reconnecting");
|
|
125
|
+
const delay = computeBackoff(attempt, baseDelayMs, maxDelayMs);
|
|
126
|
+
attempt += 1;
|
|
127
|
+
reconnectTimer = setTimeout(() => {
|
|
128
|
+
reconnectTimer = void 0;
|
|
129
|
+
void run();
|
|
130
|
+
}, delay);
|
|
131
|
+
};
|
|
132
|
+
const run = async () => {
|
|
133
|
+
if (closed) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
controller = new AbortController();
|
|
137
|
+
let token;
|
|
138
|
+
try {
|
|
139
|
+
token = options.config.auth ? await options.config.auth.getToken() : void 0;
|
|
140
|
+
} catch (error) {
|
|
141
|
+
logger?.debug("connectSse failed to get token", { error });
|
|
142
|
+
scheduleReconnect();
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (closed) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const headers = {
|
|
149
|
+
accept: "text/event-stream"
|
|
150
|
+
};
|
|
151
|
+
if (options.config.getDynamicHeaders) {
|
|
152
|
+
try {
|
|
153
|
+
Object.assign(headers, options.config.getDynamicHeaders());
|
|
154
|
+
} catch (error) {
|
|
155
|
+
logger?.debug("connectSse getDynamicHeaders threw", { error });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (token) {
|
|
159
|
+
headers.authorization = `Bearer ${token}`;
|
|
160
|
+
}
|
|
161
|
+
if (lastEventId !== void 0) {
|
|
162
|
+
headers["last-event-id"] = lastEventId;
|
|
163
|
+
}
|
|
164
|
+
const parser = new SseParser(options.onMessage, lastEventId);
|
|
165
|
+
try {
|
|
166
|
+
const response = await fetch(options.url, {
|
|
167
|
+
method: "GET",
|
|
168
|
+
headers,
|
|
169
|
+
signal: controller.signal,
|
|
170
|
+
...options.config.proxyAgent ?? {}
|
|
171
|
+
});
|
|
172
|
+
if (!response.ok) {
|
|
173
|
+
if ((response.status === 401 || response.status === 403) && !authRetryUsed) {
|
|
174
|
+
authRetryUsed = true;
|
|
175
|
+
logger?.debug(
|
|
176
|
+
"connectSse auth error, retrying once with fresh token",
|
|
177
|
+
{
|
|
178
|
+
status: response.status
|
|
179
|
+
}
|
|
180
|
+
);
|
|
181
|
+
setState("reconnecting");
|
|
182
|
+
void run();
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
logger?.debug("connectSse non-ok response", { status: response.status });
|
|
186
|
+
scheduleReconnect();
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (!response.body) {
|
|
190
|
+
logger?.debug("connectSse response has no body");
|
|
191
|
+
scheduleReconnect();
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
attempt = 0;
|
|
195
|
+
authRetryUsed = false;
|
|
196
|
+
setState("open");
|
|
197
|
+
armHeartbeat();
|
|
198
|
+
const reader = response.body.getReader();
|
|
199
|
+
const decoder = new TextDecoder();
|
|
200
|
+
while (!closed) {
|
|
201
|
+
const { done, value } = await reader.read();
|
|
202
|
+
if (done) {
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
armHeartbeat();
|
|
206
|
+
parser.push(decoder.decode(value, { stream: true }));
|
|
207
|
+
lastEventId = parser.getLastEventId();
|
|
208
|
+
}
|
|
209
|
+
if (!closed) {
|
|
210
|
+
scheduleReconnect();
|
|
211
|
+
}
|
|
212
|
+
} catch (error) {
|
|
213
|
+
if (closed) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
logger?.debug("connectSse stream error, reconnecting", { error });
|
|
217
|
+
scheduleReconnect();
|
|
218
|
+
} finally {
|
|
219
|
+
clearHeartbeat();
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
const close = () => {
|
|
223
|
+
if (closed) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
closed = true;
|
|
227
|
+
clearHeartbeat();
|
|
228
|
+
if (reconnectTimer !== void 0) {
|
|
229
|
+
clearTimeout(reconnectTimer);
|
|
230
|
+
reconnectTimer = void 0;
|
|
231
|
+
}
|
|
232
|
+
controller?.abort();
|
|
233
|
+
setStateClosed();
|
|
234
|
+
};
|
|
235
|
+
function setStateClosed() {
|
|
236
|
+
state = "closed";
|
|
237
|
+
try {
|
|
238
|
+
options.onStateChange?.("closed");
|
|
239
|
+
} catch (error) {
|
|
240
|
+
logger?.debug("connectSse onStateChange callback threw", { error });
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
void run();
|
|
244
|
+
return {
|
|
245
|
+
close,
|
|
246
|
+
get state() {
|
|
247
|
+
return state;
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
export {
|
|
252
|
+
connectSse
|
|
253
|
+
};
|
|
254
|
+
//# sourceMappingURL=sse.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/utils/sse.ts"],
|
|
4
|
+
"sourcesContent": ["/*\n * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH\n * under one or more contributor license agreements. Licensed under a commercial license.\n * You may not use this file except in compliance with the commercial license.\n */\n\nimport type { RequestBaseParamsConfig } from '../types/general.types.js'\n\n/**\n * Dependency-free Server-Sent Events client built on `fetch` + `ReadableStream`.\n *\n * Why not the native `EventSource`? It cannot send an `Authorization` header,\n * which every Camunda stream requires. This helper reuses the same\n * `config.auth.getToken()` contract as `request()`, so it always sends a fresh\n * bearer token \u2014 including on every reconnect, which is how token rotation is\n * handled (no separate refresh timer needed on the consumer side).\n *\n * It is isomorphic (browser + Node 22+), auto-reconnects with exponential\n * backoff + full jitter, honours `Last-Event-ID`, and aborts+reconnects when\n * the stream goes silent past `heartbeatTimeoutMs` (comment/keep-alive frames\n * count as activity, so a well-behaved server keeps the connection pinned).\n */\n\nexport type SseConnectionState =\n | 'connecting'\n | 'open'\n | 'reconnecting'\n | 'closed'\n\nexport interface SseMessage {\n /** Concatenated `data:` lines (newline-joined), excluding the trailing newline. */\n data: string\n /** The `event:` field, if the server set one. */\n event?: string\n /** The `id:` field, if the server set one. Echoed back as `Last-Event-ID`. */\n id?: string\n}\n\nexport interface SseConnectionOptions {\n /** Absolute URL of the event stream. */\n url: string\n /** Reuses `auth.getToken` (called per (re)connect) and `logger`. */\n config: RequestBaseParamsConfig\n /** Called for each complete, non-empty SSE event (comment-only frames excluded). */\n onMessage: (message: SseMessage) => void\n /** Observability hook for connection lifecycle transitions. */\n onStateChange?: (state: SseConnectionState) => void\n /**\n * Abort + reconnect when no bytes (including comments / keep-alives) arrive\n * for this long. Default 120_000 ms.\n */\n heartbeatTimeoutMs?: number\n /** Backoff tuning. Defaults: base 1_000 ms, max 30_000 ms, full jitter. */\n reconnect?: {\n baseDelayMs?: number\n maxDelayMs?: number\n }\n}\n\nexport interface SseConnection {\n /** Idempotent. Aborts any in-flight fetch, cancels timers, emits no further events. */\n close: () => void\n /** Current lifecycle state. */\n readonly state: SseConnectionState\n}\n\nconst DEFAULT_HEARTBEAT_TIMEOUT_MS = 120_000\nconst DEFAULT_BASE_DELAY_MS = 1_000\nconst DEFAULT_MAX_DELAY_MS = 30_000\n\n/**\n * Incrementally parses a `text/event-stream` byte stream into complete events.\n * Events are separated by a blank line; `data:` lines accumulate; `event:`/`id:`\n * set the corresponding fields; lines starting with `:` are comments (ignored,\n * but still count as stream activity for the heartbeat).\n */\nclass SseParser {\n private buffer = ''\n private dataLines: string[] = []\n private eventName: string | undefined\n private lastEventId: string | undefined\n\n constructor(\n private readonly onMessage: (message: SseMessage) => void,\n /** Seed used to resume: exposed back so the connection can send Last-Event-ID. */\n lastEventId: string | undefined,\n ) {\n this.lastEventId = lastEventId\n }\n\n getLastEventId(): string | undefined {\n return this.lastEventId\n }\n\n /** Feed a decoded text chunk. Returns nothing; dispatches via `onMessage`. */\n push(chunk: string): void {\n this.buffer += chunk\n // Normalize CRLF / CR to LF so line splitting is uniform.\n this.buffer = this.buffer.replace(/\\r\\n?/g, '\\n')\n\n let newlineIndex = this.buffer.indexOf('\\n')\n while (newlineIndex !== -1) {\n const line = this.buffer.slice(0, newlineIndex)\n this.buffer = this.buffer.slice(newlineIndex + 1)\n this.handleLine(line)\n newlineIndex = this.buffer.indexOf('\\n')\n }\n }\n\n private handleLine(line: string): void {\n // Blank line: dispatch the accumulated event.\n if (line === '') {\n this.dispatch()\n return\n }\n\n // Comment line (keep-alive) \u2014 ignored.\n if (line.startsWith(':')) {\n return\n }\n\n const colonIndex = line.indexOf(':')\n const field = colonIndex === -1 ? line : line.slice(0, colonIndex)\n let value = colonIndex === -1 ? '' : line.slice(colonIndex + 1)\n // A single leading space after the colon is stripped, per the spec.\n if (value.startsWith(' ')) {\n value = value.slice(1)\n }\n\n switch (field) {\n case 'data':\n this.dataLines.push(value)\n break\n case 'event':\n this.eventName = value\n break\n case 'id':\n // Per spec, an id containing NUL is ignored; otherwise it updates the\n // last-event-id used on reconnect.\n if (!value.includes('\\u0000')) {\n this.lastEventId = value\n }\n break\n // `retry:` and unknown fields are intentionally ignored.\n default:\n break\n }\n }\n\n private dispatch(): void {\n if (this.dataLines.length === 0 && this.eventName === undefined) {\n // Nothing to dispatch (e.g. stray blank line).\n this.reset()\n return\n }\n\n this.onMessage({\n data: this.dataLines.join('\\n'),\n event: this.eventName,\n id: this.lastEventId,\n })\n this.reset()\n }\n\n private reset(): void {\n this.dataLines = []\n this.eventName = undefined\n }\n}\n\nfunction computeBackoff(\n attempt: number,\n baseDelayMs: number,\n maxDelayMs: number,\n): number {\n const exponential = Math.min(maxDelayMs, baseDelayMs * 2 ** attempt)\n // Full jitter: pick uniformly in [0, exponential].\n return Math.floor(Math.random() * exponential)\n}\n\nexport function connectSse(options: SseConnectionOptions): SseConnection {\n const heartbeatTimeoutMs =\n options.heartbeatTimeoutMs ?? DEFAULT_HEARTBEAT_TIMEOUT_MS\n const baseDelayMs = options.reconnect?.baseDelayMs ?? DEFAULT_BASE_DELAY_MS\n const maxDelayMs = options.reconnect?.maxDelayMs ?? DEFAULT_MAX_DELAY_MS\n const logger = options.config.logger\n\n let state: SseConnectionState = 'connecting'\n let closed = false\n let attempt = 0\n let lastEventId: string | undefined\n let controller: AbortController | undefined\n let heartbeatTimer: ReturnType<typeof setTimeout> | undefined\n let reconnectTimer: ReturnType<typeof setTimeout> | undefined\n // Once we've seen a 401/403 we allow exactly one immediate fresh-token retry\n // before falling back to normal backoff, so a transient auth blip self-heals\n // without hammering the token endpoint.\n let authRetryUsed = false\n\n const setState = (next: SseConnectionState): void => {\n if (state === next || closed) {\n return\n }\n state = next\n try {\n options.onStateChange?.(next)\n } catch (error) {\n logger?.debug('connectSse onStateChange callback threw', { error })\n }\n }\n\n const clearHeartbeat = (): void => {\n if (heartbeatTimer !== undefined) {\n clearTimeout(heartbeatTimer)\n heartbeatTimer = undefined\n }\n }\n\n const armHeartbeat = (): void => {\n clearHeartbeat()\n heartbeatTimer = setTimeout(() => {\n logger?.debug('connectSse heartbeat timeout, reconnecting', {\n url: options.url,\n })\n // Abort the current fetch; the read loop's catch schedules the reconnect.\n controller?.abort()\n }, heartbeatTimeoutMs)\n }\n\n const scheduleReconnect = (): void => {\n if (closed) {\n return\n }\n // Guard against stacking timers: multiple error paths (heartbeat abort +\n // stream end, etc.) can each call scheduleReconnect before the pending\n // timer fires. Without this, overlapping timers spawn concurrent run()\n // loops (and concurrent fetches).\n if (reconnectTimer !== undefined) {\n return\n }\n setState('reconnecting')\n const delay = computeBackoff(attempt, baseDelayMs, maxDelayMs)\n attempt += 1\n reconnectTimer = setTimeout(() => {\n reconnectTimer = undefined\n void run()\n }, delay)\n }\n\n const run = async (): Promise<void> => {\n if (closed) {\n return\n }\n\n controller = new AbortController()\n\n let token: string | undefined\n try {\n token = options.config.auth\n ? await options.config.auth.getToken()\n : undefined\n } catch (error) {\n logger?.debug('connectSse failed to get token', { error })\n scheduleReconnect()\n return\n }\n\n if (closed) {\n return\n }\n\n const headers: Record<string, string> = {\n accept: 'text/event-stream',\n }\n // Correlation headers (traceparent, x-cc-session-id) so the SSE connection\n // is traceable and grouped into the session, mirroring request(). A\n // throwing getDynamicHeaders is swallowed \u2014 the stream still connects.\n if (options.config.getDynamicHeaders) {\n try {\n Object.assign(headers, options.config.getDynamicHeaders())\n } catch (error) {\n logger?.debug('connectSse getDynamicHeaders threw', { error })\n }\n }\n if (token) {\n headers.authorization = `Bearer ${token}`\n }\n if (lastEventId !== undefined) {\n headers['last-event-id'] = lastEventId\n }\n\n const parser = new SseParser(options.onMessage, lastEventId)\n\n try {\n const response = await fetch(options.url, {\n method: 'GET',\n headers,\n signal: controller.signal,\n ...(options.config.proxyAgent ?? {}),\n })\n\n if (!response.ok) {\n // Auth blip: retry once immediately with a freshly minted token before\n // falling back to backoff.\n if (\n (response.status === 401 || response.status === 403) &&\n !authRetryUsed\n ) {\n authRetryUsed = true\n logger?.debug(\n 'connectSse auth error, retrying once with fresh token',\n {\n status: response.status,\n },\n )\n setState('reconnecting')\n void run()\n return\n }\n logger?.debug('connectSse non-ok response', { status: response.status })\n scheduleReconnect()\n return\n }\n\n if (!response.body) {\n logger?.debug('connectSse response has no body')\n scheduleReconnect()\n return\n }\n\n // Connected successfully: reset backoff + auth-retry budget.\n attempt = 0\n authRetryUsed = false\n setState('open')\n armHeartbeat()\n\n const reader = response.body.getReader()\n const decoder = new TextDecoder()\n\n while (!closed) {\n const { done, value } = await reader.read()\n if (done) {\n break\n }\n // Any bytes (incl. comments) count as liveness.\n armHeartbeat()\n parser.push(decoder.decode(value, { stream: true }))\n lastEventId = parser.getLastEventId()\n }\n\n // Stream ended cleanly (server closed): reconnect unless we're closing.\n if (!closed) {\n scheduleReconnect()\n }\n } catch (error) {\n if (closed) {\n return\n }\n // AbortError from close() is handled by the `closed` guard above; an\n // AbortError from the heartbeat means we intentionally dropped a silent\n // connection and should reconnect.\n logger?.debug('connectSse stream error, reconnecting', { error })\n scheduleReconnect()\n } finally {\n clearHeartbeat()\n }\n }\n\n const close = (): void => {\n if (closed) {\n return\n }\n closed = true\n clearHeartbeat()\n if (reconnectTimer !== undefined) {\n clearTimeout(reconnectTimer)\n reconnectTimer = undefined\n }\n controller?.abort()\n setStateClosed()\n }\n\n // `setState` short-circuits once `closed` is true, so set the terminal state\n // through a dedicated path.\n function setStateClosed(): void {\n state = 'closed'\n try {\n options.onStateChange?.('closed')\n } catch (error) {\n logger?.debug('connectSse onStateChange callback threw', { error })\n }\n }\n\n void run()\n\n return {\n close,\n get state() {\n return state\n },\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates a fresh W3C `traceparent` header value
|
|
3
|
+
* (`00-<32 hex trace id>-<16 hex span id>-01`).
|
|
4
|
+
*
|
|
5
|
+
* Isomorphic: uses the Web Crypto API available on both the browser and Node
|
|
6
|
+
* 18+ (`globalThis.crypto`). Intended for frontends to start a trace at the
|
|
7
|
+
* request boundary so backends adopt it (see SessionMiddleware) — each call
|
|
8
|
+
* mints a new trace, i.e. one trace per outgoing request.
|
|
9
|
+
*/
|
|
10
|
+
export declare function generateTraceparent(): string;
|
|
11
|
+
/** Convenience: the `{ traceparent }` header object for a config's getDynamicHeaders. */
|
|
12
|
+
export declare function traceparentHeaders(): Record<string, string>;
|
|
13
|
+
/**
|
|
14
|
+
* A stable per-tab session id, minted once and cached in `sessionStorage` so it
|
|
15
|
+
* survives reloads and in-tab navigation but is distinct per tab. Unlike a
|
|
16
|
+
* traceparent (one per request), this groups every request of one browser
|
|
17
|
+
* session under a single `sid` on the backend. Falls back to a per-process id
|
|
18
|
+
* when `sessionStorage` is unavailable (SSR, tests, sandboxed iframe, privacy
|
|
19
|
+
* mode) so it never throws.
|
|
20
|
+
*/
|
|
21
|
+
export declare function getSessionId(): string;
|
|
22
|
+
/** Convenience: the `{ 'x-cc-session-id' }` header object for the per-tab session. */
|
|
23
|
+
export declare function sessionHeaders(): Record<string, string>;
|
|
24
|
+
/**
|
|
25
|
+
* Correlation headers for a frontend request: a fresh `traceparent` (one trace
|
|
26
|
+
* per request, which the backend adopts) plus the stable per-tab
|
|
27
|
+
* `x-cc-session-id`. Wire as a config's `getDynamicHeaders` so the backend can
|
|
28
|
+
* both follow a single action (trace) and group a browser session (sid).
|
|
29
|
+
*/
|
|
30
|
+
export declare function correlationHeaders(): Record<string, string>;
|