@zintrust/trace 1.6.1 → 1.6.3

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.
Files changed (50) hide show
  1. package/dist/register.js +19 -15
  2. package/package.json +2 -3
  3. package/src/TraceConnection.ts +0 -182
  4. package/src/cli-register.ts +0 -63
  5. package/src/config.ts +0 -383
  6. package/src/context.ts +0 -101
  7. package/src/dashboard/handlers.ts +0 -353
  8. package/src/dashboard/routes.ts +0 -114
  9. package/src/dashboard/ui.ts +0 -1262
  10. package/src/dashboard/zintrust-debuger.svg +0 -30
  11. package/src/index.ts +0 -102
  12. package/src/ingest/TraceIngestGateway.ts +0 -414
  13. package/src/plugin.ts +0 -9
  14. package/src/register.ts +0 -659
  15. package/src/storage/ProxyTraceStorage.ts +0 -190
  16. package/src/storage/TraceContentBudget.ts +0 -491
  17. package/src/storage/TraceContentRedaction.ts +0 -44
  18. package/src/storage/TraceEntryFiltering.ts +0 -92
  19. package/src/storage/TraceServiceTag.ts +0 -56
  20. package/src/storage/TraceStorage.ts +0 -543
  21. package/src/storage/TraceWriteDiagnostics.ts +0 -289
  22. package/src/storage/index.ts +0 -4
  23. package/src/types.ts +0 -430
  24. package/src/ui.ts +0 -9
  25. package/src/utils/authTag.ts +0 -20
  26. package/src/utils/entryFilter.ts +0 -131
  27. package/src/utils/familyHash.ts +0 -8
  28. package/src/utils/redact.ts +0 -112
  29. package/src/utils/requestFilter.ts +0 -79
  30. package/src/utils/stackFrame.ts +0 -44
  31. package/src/watchers/AuthWatcher.ts +0 -53
  32. package/src/watchers/BatchWatcher.ts +0 -55
  33. package/src/watchers/CacheWatcher.ts +0 -72
  34. package/src/watchers/CommandWatcher.ts +0 -58
  35. package/src/watchers/DumpWatcher.ts +0 -45
  36. package/src/watchers/EventWatcher.ts +0 -46
  37. package/src/watchers/ExceptionWatcher.ts +0 -130
  38. package/src/watchers/GateWatcher.ts +0 -53
  39. package/src/watchers/HttpClientWatcher.ts +0 -219
  40. package/src/watchers/HttpWatcher.ts +0 -220
  41. package/src/watchers/JobWatcher.ts +0 -124
  42. package/src/watchers/LogWatcher.ts +0 -120
  43. package/src/watchers/MailWatcher.ts +0 -65
  44. package/src/watchers/MiddlewareWatcher.ts +0 -54
  45. package/src/watchers/ModelWatcher.ts +0 -60
  46. package/src/watchers/NotificationWatcher.ts +0 -60
  47. package/src/watchers/QueryWatcher.ts +0 -107
  48. package/src/watchers/RedisWatcher.ts +0 -42
  49. package/src/watchers/ScheduleWatcher.ts +0 -57
  50. package/src/watchers/ViewWatcher.ts +0 -40
@@ -1,190 +0,0 @@
1
- import { ErrorFactory, RemoteSignedJson } from '@zintrust/core';
2
- import type { ITraceEntry, ITraceStorage } from '../types';
3
-
4
- type ProxyTraceStorageSettings = {
5
- baseUrl: string;
6
- path: string;
7
- keyId: string;
8
- secret: string;
9
- timeoutMs: number;
10
- };
11
-
12
- type TraceProxyWriteRequest = {
13
- entry: ITraceEntry;
14
- };
15
-
16
- type TraceProxyUpdateRequest = {
17
- uuid: string;
18
- patch: Partial<Pick<ITraceEntry, 'content' | 'isLatest'>>;
19
- };
20
-
21
- type TraceProxyMarkFamilyStaleRequest = {
22
- familyHash: string;
23
- exceptUuid: string;
24
- };
25
-
26
- const ensureConfigured = (settings: ProxyTraceStorageSettings): void => {
27
- if (settings.baseUrl.trim() === '') {
28
- throw ErrorFactory.createConfigError('TRACE_PROXY_URL is required when TRACE_PROXY=true');
29
- }
30
-
31
- if (settings.keyId.trim() === '' || settings.secret.trim() === '') {
32
- throw ErrorFactory.createConfigError(
33
- 'TRACE_PROXY signing credentials are required when TRACE_PROXY=true'
34
- );
35
- }
36
- };
37
-
38
- const normalizePath = (value: string): string => {
39
- const trimmed = value.trim();
40
- if (trimmed === '') return '/zin/trace/write';
41
- return trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
42
- };
43
-
44
- const trimTrailingSlashes = (value: string): string => {
45
- let trimmed = value;
46
- while (trimmed.endsWith('/')) {
47
- trimmed = trimmed.slice(0, -1);
48
- }
49
- return trimmed;
50
- };
51
-
52
- const createUnsupportedReadError = (): Error =>
53
- ErrorFactory.createConfigError(
54
- 'Trace proxy sender storage does not expose dashboard/query operations. Use the trace server for reads.'
55
- );
56
-
57
- type ProxyRequestSettings = {
58
- baseUrl: string;
59
- keyId: string;
60
- secret: string;
61
- timeoutMs: number;
62
- signaturePathPrefixToStrip: string;
63
- missingUrlMessage: string;
64
- missingCredentialsMessage: string;
65
- messages: {
66
- unauthorized: string;
67
- forbidden: string;
68
- rateLimited: string;
69
- rejected: string;
70
- error: string;
71
- timedOut: string;
72
- };
73
- normalizedPath: string;
74
- };
75
-
76
- const buildSettings = (settings: ProxyTraceStorageSettings): ProxyRequestSettings => {
77
- ensureConfigured(settings);
78
- const normalizedPath = normalizePath(settings.path);
79
-
80
- return {
81
- baseUrl: settings.baseUrl,
82
- keyId: settings.keyId,
83
- secret: settings.secret,
84
- timeoutMs: settings.timeoutMs,
85
- signaturePathPrefixToStrip: new URL(settings.baseUrl).pathname,
86
- missingUrlMessage: 'TRACE_PROXY_URL is required when TRACE_PROXY=true',
87
- missingCredentialsMessage: 'TRACE_PROXY signing credentials are required when TRACE_PROXY=true',
88
- messages: {
89
- unauthorized: 'Trace proxy rejected the request credentials',
90
- forbidden: 'Trace proxy rejected the request signature',
91
- rateLimited: 'Trace proxy rate-limited the request',
92
- rejected: 'Trace proxy rejected the request payload',
93
- error: 'Trace proxy request failed',
94
- timedOut: 'Trace proxy request timed out',
95
- },
96
- normalizedPath,
97
- };
98
- };
99
-
100
- const appendSuffix = (path: string, suffix: string): string => {
101
- const base = trimTrailingSlashes(normalizePath(path));
102
- const tail = suffix.startsWith('/') ? suffix : `/${suffix}`;
103
- return `${base}${tail}`;
104
- };
105
-
106
- const unsupportedQueryEntries: ITraceStorage['queryEntries'] = async () => {
107
- throw createUnsupportedReadError();
108
- };
109
-
110
- const unsupportedGetEntry: ITraceStorage['getEntry'] = async () => {
111
- throw createUnsupportedReadError();
112
- };
113
-
114
- const unsupportedGetBatch: ITraceStorage['getBatch'] = async () => {
115
- throw createUnsupportedReadError();
116
- };
117
-
118
- const unsupportedQueryBatchEntries: ITraceStorage['queryBatchEntries'] = async () => {
119
- throw createUnsupportedReadError();
120
- };
121
-
122
- const unsupportedPrune: ITraceStorage['prune'] = async () => {
123
- throw createUnsupportedReadError();
124
- };
125
-
126
- const unsupportedClear: ITraceStorage['clear'] = async () => {
127
- throw createUnsupportedReadError();
128
- };
129
-
130
- const unsupportedGetMonitoring: ITraceStorage['getMonitoring'] = async () => {
131
- throw createUnsupportedReadError();
132
- };
133
-
134
- const unsupportedAddMonitoring: ITraceStorage['addMonitoring'] = async () => {
135
- throw createUnsupportedReadError();
136
- };
137
-
138
- const unsupportedRemoveMonitoring: ITraceStorage['removeMonitoring'] = async () => {
139
- throw createUnsupportedReadError();
140
- };
141
-
142
- const unsupportedStats: ITraceStorage['stats'] = async () => {
143
- throw createUnsupportedReadError();
144
- };
145
-
146
- export const ProxyTraceStorage = Object.freeze({
147
- create(settings: ProxyTraceStorageSettings): ITraceStorage {
148
- const normalized = buildSettings(settings);
149
-
150
- return Object.freeze({
151
- async writeEntry(entry: ITraceEntry): Promise<void> {
152
- await RemoteSignedJson.request<{ ok: true }>(normalized, normalized.normalizedPath, {
153
- entry,
154
- } satisfies TraceProxyWriteRequest);
155
- },
156
-
157
- async updateEntry(
158
- uuid: string,
159
- patch: Partial<Pick<ITraceEntry, 'content' | 'isLatest'>>
160
- ): Promise<void> {
161
- await RemoteSignedJson.request<{ ok: true }>(
162
- normalized,
163
- appendSuffix(normalized.normalizedPath, '/update'),
164
- { uuid, patch } satisfies TraceProxyUpdateRequest
165
- );
166
- },
167
-
168
- async markFamilyStale(familyHash: string, exceptUuid: string): Promise<void> {
169
- await RemoteSignedJson.request<{ ok: true }>(
170
- normalized,
171
- appendSuffix(normalized.normalizedPath, '/mark-family-stale'),
172
- { familyHash, exceptUuid } satisfies TraceProxyMarkFamilyStaleRequest
173
- );
174
- },
175
-
176
- queryEntries: unsupportedQueryEntries,
177
- getEntry: unsupportedGetEntry,
178
- getBatch: unsupportedGetBatch,
179
- queryBatchEntries: unsupportedQueryBatchEntries,
180
- prune: unsupportedPrune,
181
- clear: unsupportedClear,
182
- getMonitoring: unsupportedGetMonitoring,
183
- addMonitoring: unsupportedAddMonitoring,
184
- removeMonitoring: unsupportedRemoveMonitoring,
185
- stats: unsupportedStats,
186
- });
187
- },
188
- });
189
-
190
- export default ProxyTraceStorage;
@@ -1,491 +0,0 @@
1
- import type { ITraceConfig, ITraceEntry, ITraceStorage } from '../types';
2
-
3
- const DEFAULT_MAX_ENTRY_BYTES = 64 * 1024;
4
- const DEFAULT_MAX_STRING_BYTES = 16 * 1024;
5
- const DEFAULT_MAX_ARRAY_ITEMS = 25;
6
- const DEFAULT_MAX_OBJECT_ENTRIES = 40;
7
- const DEFAULT_MAX_DEPTH = 6;
8
-
9
- const DROPPED_FIELD_MESSAGE =
10
- '[trace] Value dropped because the field exceeded the trace storage size limit.';
11
- const COMPACTED_CONTENT_MESSAGE =
12
- '[trace] Trace content was compacted because it exceeded the trace storage size limit.';
13
- const REPLACED_CONTENT_MESSAGE = 'Trace content exceeded budget and was replaced.';
14
-
15
- const encoder = new TextEncoder();
16
-
17
- const serializedSize = (value: unknown): number => {
18
- try {
19
- return encoder.encode(JSON.stringify(value)).length;
20
- } catch {
21
- return Number.MAX_SAFE_INTEGER;
22
- }
23
- };
24
-
25
- const describeValueType = (value: unknown): string => {
26
- if (Array.isArray(value)) return 'array';
27
- if (value === null) return 'null';
28
- return typeof value;
29
- };
30
-
31
- const compactValue = (value: unknown, depth: number): unknown => {
32
- if (depth >= DEFAULT_MAX_DEPTH) {
33
- return DROPPED_FIELD_MESSAGE;
34
- }
35
-
36
- if (typeof value === 'string') {
37
- return serializedSize(value) > DEFAULT_MAX_STRING_BYTES ? DROPPED_FIELD_MESSAGE : value;
38
- }
39
-
40
- if (Array.isArray(value)) {
41
- const next = value
42
- .slice(0, DEFAULT_MAX_ARRAY_ITEMS)
43
- .map((item) => compactValue(item, depth + 1));
44
-
45
- if (value.length > DEFAULT_MAX_ARRAY_ITEMS) {
46
- next.push(
47
- `[trace] ${String(value.length - DEFAULT_MAX_ARRAY_ITEMS)} additional items were dropped.`
48
- );
49
- }
50
-
51
- return next;
52
- }
53
-
54
- if (typeof value !== 'object' || value === null) {
55
- return value;
56
- }
57
-
58
- const entries = Object.entries(value);
59
- const compactedEntries = entries
60
- .slice(0, DEFAULT_MAX_OBJECT_ENTRIES)
61
- .map(([key, entryValue]) => [key, compactValue(entryValue, depth + 1)]);
62
-
63
- if (entries.length > DEFAULT_MAX_OBJECT_ENTRIES) {
64
- compactedEntries.push([
65
- '__traceNotice',
66
- `[trace] ${String(entries.length - DEFAULT_MAX_OBJECT_ENTRIES)} additional fields were dropped.`,
67
- ]);
68
- }
69
-
70
- return Object.fromEntries(compactedEntries);
71
- };
72
-
73
- const compactStructuredValueToBudget = (value: unknown): unknown => {
74
- if (typeof value !== 'object' || value === null || Array.isArray(value)) {
75
- return value;
76
- }
77
-
78
- const compacted: Record<string, unknown> = {
79
- ...(value as Record<string, unknown>),
80
- __traceNotice: COMPACTED_CONTENT_MESSAGE,
81
- };
82
-
83
- const topLevelCandidates = Object.entries(compacted)
84
- .filter(([key]) => key !== '__traceNotice')
85
- .map(([key, entryValue]) => ({ key, size: serializedSize(entryValue) }))
86
- .sort((left, right) => right.size - left.size);
87
-
88
- let droppedCount = 0;
89
-
90
- for (const candidate of topLevelCandidates) {
91
- if (serializedSize(compacted) <= DEFAULT_MAX_ENTRY_BYTES) {
92
- break;
93
- }
94
-
95
- compacted[candidate.key] = DROPPED_FIELD_MESSAGE;
96
- droppedCount += 1;
97
- }
98
-
99
- if (droppedCount > 0) {
100
- compacted['__traceNotice'] =
101
- `${COMPACTED_CONTENT_MESSAGE} ${String(droppedCount)} top-level field(s) were dropped.`;
102
- }
103
-
104
- return compacted;
105
- };
106
-
107
- const fitContentToBudget = (content: unknown): unknown => {
108
- if (serializedSize(content) <= DEFAULT_MAX_ENTRY_BYTES) {
109
- return content;
110
- }
111
-
112
- const compacted = compactValue(content, 0);
113
- if (serializedSize(compacted) <= DEFAULT_MAX_ENTRY_BYTES) {
114
- return compacted;
115
- }
116
-
117
- if (typeof compacted === 'object' && compacted !== null) {
118
- const budgetCompacted = compactStructuredValueToBudget(compacted);
119
- if (serializedSize(budgetCompacted) <= DEFAULT_MAX_ENTRY_BYTES) {
120
- return budgetCompacted;
121
- }
122
- }
123
-
124
- return {
125
- __traceNotice: COMPACTED_CONTENT_MESSAGE,
126
- dropped: true,
127
- valueType: describeValueType(content),
128
- };
129
- };
130
-
131
- const fitEntryToBudget = (entry: ITraceEntry): ITraceEntry => ({
132
- ...entry,
133
- content: fitContentToBudget(entry.content),
134
- });
135
-
136
- const fitPatchToBudget = (
137
- patch: Partial<Pick<ITraceEntry, 'content' | 'isLatest'>>
138
- ): Partial<Pick<ITraceEntry, 'content' | 'isLatest'>> => {
139
- if (patch.content === undefined) return patch;
140
-
141
- return {
142
- ...patch,
143
- content: fitContentToBudget(patch.content),
144
- };
145
- };
146
-
147
- type TraceDispatchMessage =
148
- | { operation: 'write'; entry: ITraceEntry }
149
- | {
150
- operation: 'update';
151
- uuid: string;
152
- patch: Partial<Pick<ITraceEntry, 'content' | 'isLatest'>>;
153
- };
154
-
155
- type QueueApi = {
156
- get(name?: string): {
157
- enqueue<T = unknown>(queue: string, payload: T): Promise<string>;
158
- };
159
- };
160
-
161
- type TimeoutManagerApi = {
162
- withTimeout<T>(
163
- operation: () => Promise<T>,
164
- timeoutMs: number,
165
- operationName: string,
166
- timeoutHandler?: () => Promise<T>
167
- ): Promise<T>;
168
- };
169
-
170
- type QueueWorkerApi = {
171
- createQueueWorker<TPayload>(options: {
172
- kindLabel: string;
173
- defaultQueueName: string;
174
- maxAttempts: number;
175
- getLogFields?: (payload: {
176
- id: string;
177
- payload: TPayload;
178
- attempts: number;
179
- }) => Record<string, unknown>;
180
- handle(payload: TPayload): Promise<void>;
181
- }): {
182
- runOnce(options?: {
183
- queueName?: string;
184
- driverName?: string;
185
- maxItems?: number;
186
- maxDurationMs?: number;
187
- concurrency?: number;
188
- }): Promise<number>;
189
- };
190
- };
191
-
192
- type UnrefableTimer = ReturnType<typeof setInterval> & { unref?: () => void };
193
-
194
- type TraceContentBudgetRuntime = {
195
- queue?: QueueApi | null;
196
- timeoutManager?: TimeoutManagerApi | null;
197
- queueWorkerApi?: QueueWorkerApi | null;
198
- };
199
-
200
- const startedWorkerKeys = new Set<string>();
201
-
202
- const closePort = (port: MessagePort): void => {
203
- if (typeof port.close === 'function') {
204
- port.close();
205
- }
206
- };
207
-
208
- const scheduleTask = async (task: () => Promise<void>): Promise<void> => {
209
- return await new Promise<void>((resolve, reject) => {
210
- const runTask = (): void => {
211
- void task().then(resolve).catch(reject);
212
- };
213
-
214
- if (typeof MessageChannel === 'function') {
215
- const channel = new MessageChannel();
216
-
217
- channel.port1.onmessage = (): void => {
218
- channel.port1.onmessage = null;
219
- closePort(channel.port1);
220
- closePort(channel.port2);
221
- runTask();
222
- };
223
-
224
- channel.port2.postMessage(undefined);
225
- return;
226
- }
227
-
228
- Promise.resolve().then(runTask).catch(reject);
229
- });
230
- };
231
-
232
- const getReplacementContent = (content: unknown): Record<string, unknown> => {
233
- return {
234
- __traceNotice: REPLACED_CONTENT_MESSAGE,
235
- dropped: true,
236
- valueType: describeValueType(content),
237
- };
238
- };
239
-
240
- const replaceEntryContent = (entry: ITraceEntry): ITraceEntry => ({
241
- ...entry,
242
- content: getReplacementContent(entry.content),
243
- });
244
-
245
- const replacePatchContent = (
246
- patch: Partial<Pick<ITraceEntry, 'content' | 'isLatest'>>
247
- ): Partial<Pick<ITraceEntry, 'content' | 'isLatest'>> => {
248
- if (patch.content === undefined) return patch;
249
-
250
- return {
251
- ...patch,
252
- content: getReplacementContent(patch.content),
253
- };
254
- };
255
-
256
- const shouldReplaceContent = (content: unknown): boolean => {
257
- return serializedSize(content) > DEFAULT_MAX_ENTRY_BYTES;
258
- };
259
-
260
- const hasQueueDispatch = (config: ITraceConfig): boolean => {
261
- const driver = config.contentDispatch.driver?.trim();
262
- return typeof driver === 'string' && driver !== '';
263
- };
264
-
265
- const getCoreRuntime = async (): Promise<{
266
- Queue: QueueApi | null;
267
- TimeoutManager: TimeoutManagerApi | null;
268
- }> => {
269
- try {
270
- const mod = (await import('@zintrust/core')) as unknown as {
271
- Queue?: QueueApi;
272
- TimeoutManager?: TimeoutManagerApi;
273
- };
274
- return {
275
- Queue: mod.Queue ?? null,
276
- TimeoutManager: mod.TimeoutManager ?? null,
277
- };
278
- } catch {
279
- return {
280
- Queue: null,
281
- TimeoutManager: null,
282
- };
283
- }
284
- };
285
-
286
- const getQueueWorkerApi = async (): Promise<QueueWorkerApi | null> => {
287
- try {
288
- // @ts-ignore
289
- const mod = (await import('@zintrust/workers')) as unknown as QueueWorkerApi;
290
- return typeof mod.createQueueWorker === 'function' ? mod : null;
291
- } catch {
292
- return null;
293
- }
294
- };
295
-
296
- const enqueueTraceDispatch = async (
297
- config: ITraceConfig,
298
- payload: TraceDispatchMessage,
299
- runtime?: TraceContentBudgetRuntime
300
- ): Promise<boolean> => {
301
- const driverName = config.contentDispatch.driver?.trim();
302
- if (driverName === undefined || driverName === '') return false;
303
-
304
- const coreRuntime =
305
- runtime?.queue !== undefined || runtime?.timeoutManager !== undefined
306
- ? {
307
- Queue: runtime?.queue ?? null,
308
- TimeoutManager: runtime?.timeoutManager ?? null,
309
- }
310
- : await getCoreRuntime();
311
- const queueApi = coreRuntime.Queue;
312
- if (queueApi === null) return false;
313
-
314
- try {
315
- const driver = queueApi.get(driverName);
316
- const timeoutMs = Math.max(1, config.contentDispatch.enqueueTimeoutMs);
317
- if (coreRuntime.TimeoutManager === null) {
318
- await driver.enqueue(config.contentDispatch.queueName, payload);
319
- } else {
320
- await coreRuntime.TimeoutManager.withTimeout(
321
- () => driver.enqueue(config.contentDispatch.queueName, payload),
322
- timeoutMs,
323
- 'trace-content-dispatch-enqueue'
324
- );
325
- }
326
-
327
- return true;
328
- } catch {
329
- return false;
330
- }
331
- };
332
-
333
- const persistWriteFallback = async (storage: ITraceStorage, entry: ITraceEntry): Promise<void> => {
334
- await storage.writeEntry(
335
- shouldReplaceContent(entry.content) ? replaceEntryContent(entry) : entry
336
- );
337
- };
338
-
339
- const persistUpdateFallback = async (
340
- storage: ITraceStorage,
341
- uuid: string,
342
- patch: Partial<Pick<ITraceEntry, 'content' | 'isLatest'>>
343
- ): Promise<void> => {
344
- await storage.updateEntry(
345
- uuid,
346
- patch.content !== undefined && shouldReplaceContent(patch.content)
347
- ? replacePatchContent(patch)
348
- : patch
349
- );
350
- };
351
-
352
- const processQueuedMessage = async (
353
- storage: ITraceStorage,
354
- message: TraceDispatchMessage
355
- ): Promise<void> => {
356
- if (message.operation === 'write') {
357
- await storage.writeEntry(fitEntryToBudget(message.entry));
358
- return;
359
- }
360
-
361
- await storage.updateEntry(message.uuid, fitPatchToBudget(message.patch));
362
- };
363
-
364
- const ensureWorkerTimer = (_key: string, timer: ReturnType<typeof setInterval>): void => {
365
- const unrefable = timer as UnrefableTimer;
366
- if (typeof unrefable.unref === 'function') {
367
- unrefable.unref();
368
- }
369
- };
370
-
371
- const startInternalDispatchWorker = (
372
- storage: ITraceStorage,
373
- config: ITraceConfig,
374
- runtime?: TraceContentBudgetRuntime
375
- ): void => {
376
- if (!hasQueueDispatch(config) || config.contentDispatch.worker.enabled !== true) return;
377
-
378
- const driverName = config.contentDispatch.driver?.trim() ?? '';
379
- const key = `${driverName}:${config.contentDispatch.queueName}`;
380
- if (startedWorkerKeys.has(key)) return;
381
- startedWorkerKeys.add(key);
382
-
383
- void scheduleTask(async () => {
384
- const workersApi = runtime?.queueWorkerApi ?? (await getQueueWorkerApi());
385
- if (workersApi === null) {
386
- startedWorkerKeys.delete(key);
387
- return;
388
- }
389
-
390
- let running = false;
391
- const runWorker = async (): Promise<void> => {
392
- if (running) return;
393
- running = true;
394
- try {
395
- const worker = workersApi.createQueueWorker<TraceDispatchMessage>({
396
- kindLabel: 'trace-content-dispatch',
397
- defaultQueueName: config.contentDispatch.queueName,
398
- maxAttempts: 1,
399
- getLogFields: () => ({
400
- queueName: config.contentDispatch.queueName,
401
- driverName,
402
- }),
403
- handle: async (payload) => {
404
- await processQueuedMessage(storage, payload);
405
- },
406
- });
407
-
408
- await worker.runOnce({
409
- queueName: config.contentDispatch.queueName,
410
- driverName,
411
- maxDurationMs: Math.max(1, config.contentDispatch.worker.maxDurationMs),
412
- concurrency: Math.max(1, config.contentDispatch.worker.concurrency),
413
- });
414
- } finally {
415
- running = false;
416
- }
417
- };
418
-
419
- await runWorker();
420
-
421
- const intervalMs = Math.max(100, config.contentDispatch.worker.intervalMs);
422
- ensureWorkerTimer(
423
- key,
424
- setInterval(() => {
425
- void runWorker();
426
- }, intervalMs)
427
- );
428
- }).catch(() => {
429
- startedWorkerKeys.delete(key);
430
- });
431
- };
432
-
433
- const dispatchWrite = async (
434
- storage: ITraceStorage,
435
- config: ITraceConfig,
436
- entry: ITraceEntry,
437
- runtime?: TraceContentBudgetRuntime
438
- ): Promise<void> => {
439
- await scheduleTask(async () => {
440
- if (hasQueueDispatch(config)) {
441
- const enqueued = await enqueueTraceDispatch(config, { operation: 'write', entry }, runtime);
442
- if (enqueued) return;
443
- }
444
-
445
- await persistWriteFallback(storage, entry);
446
- });
447
- };
448
-
449
- const dispatchUpdate = async (
450
- storage: ITraceStorage,
451
- config: ITraceConfig,
452
- uuid: string,
453
- patch: Partial<Pick<ITraceEntry, 'content' | 'isLatest'>>,
454
- runtime?: TraceContentBudgetRuntime
455
- ): Promise<void> => {
456
- await scheduleTask(async () => {
457
- if (hasQueueDispatch(config)) {
458
- const enqueued = await enqueueTraceDispatch(
459
- config,
460
- { operation: 'update', uuid, patch },
461
- runtime
462
- );
463
- if (enqueued) return;
464
- }
465
-
466
- await persistUpdateFallback(storage, uuid, patch);
467
- });
468
- };
469
-
470
- export const TraceContentBudget = Object.freeze({
471
- wrapStorage(
472
- storage: ITraceStorage,
473
- config: ITraceConfig,
474
- runtime?: TraceContentBudgetRuntime
475
- ): ITraceStorage {
476
- startInternalDispatchWorker(storage, config, runtime);
477
-
478
- return Object.freeze({
479
- ...storage,
480
- writeEntry: async (entry: ITraceEntry): Promise<void> => {
481
- await dispatchWrite(storage, config, entry, runtime);
482
- },
483
- updateEntry: async (
484
- uuid: string,
485
- patch: Partial<Pick<ITraceEntry, 'content' | 'isLatest'>>
486
- ): Promise<void> => {
487
- await dispatchUpdate(storage, config, uuid, patch, runtime);
488
- },
489
- });
490
- },
491
- });