@pdpp/local-collector 0.0.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.
Files changed (49) hide show
  1. package/README.md +48 -0
  2. package/dist/local-collector/bin/pdpp-local-collector.js +347 -0
  3. package/dist/local-collector/src/errors.d.ts +12 -0
  4. package/dist/local-collector/src/errors.js +20 -0
  5. package/dist/local-collector/src/runner.d.ts +16 -0
  6. package/dist/local-collector/src/runner.js +59 -0
  7. package/dist/polyfill-connectors/connectors/claude_code/index.js +806 -0
  8. package/dist/polyfill-connectors/connectors/claude_code/parsers.js +224 -0
  9. package/dist/polyfill-connectors/connectors/claude_code/schemas.js +120 -0
  10. package/dist/polyfill-connectors/connectors/claude_code/types.js +1 -0
  11. package/dist/polyfill-connectors/connectors/codex/index.js +880 -0
  12. package/dist/polyfill-connectors/connectors/codex/parsers.js +159 -0
  13. package/dist/polyfill-connectors/connectors/codex/schemas.js +118 -0
  14. package/dist/polyfill-connectors/connectors/codex/types.js +1 -0
  15. package/dist/polyfill-connectors/src/auth.js +76 -0
  16. package/dist/polyfill-connectors/src/browser-handoff.js +197 -0
  17. package/dist/polyfill-connectors/src/collector-protocol.d.ts +2 -0
  18. package/dist/polyfill-connectors/src/collector-protocol.js +2 -0
  19. package/dist/polyfill-connectors/src/collector-runner.d.ts +139 -0
  20. package/dist/polyfill-connectors/src/collector-runner.js +1084 -0
  21. package/dist/polyfill-connectors/src/connector-runtime-protocol.d.ts +191 -0
  22. package/dist/polyfill-connectors/src/connector-runtime-protocol.js +1 -0
  23. package/dist/polyfill-connectors/src/connector-runtime.js +879 -0
  24. package/dist/polyfill-connectors/src/fixture-capture.js +237 -0
  25. package/dist/polyfill-connectors/src/is-main-module.d.ts +1 -0
  26. package/dist/polyfill-connectors/src/is-main-module.js +17 -0
  27. package/dist/polyfill-connectors/src/local-device-client.d.ts +126 -0
  28. package/dist/polyfill-connectors/src/local-device-client.js +132 -0
  29. package/dist/polyfill-connectors/src/local-device-envelope.d.ts +26 -0
  30. package/dist/polyfill-connectors/src/local-device-envelope.js +43 -0
  31. package/dist/polyfill-connectors/src/local-device-outbox.d.ts +115 -0
  32. package/dist/polyfill-connectors/src/local-device-outbox.js +509 -0
  33. package/dist/polyfill-connectors/src/local-device-queue.d.ts +34 -0
  34. package/dist/polyfill-connectors/src/local-device-queue.js +133 -0
  35. package/dist/polyfill-connectors/src/local-source-inventory.js +119 -0
  36. package/dist/polyfill-connectors/src/pdpp-safe-text.js +13 -0
  37. package/dist/polyfill-connectors/src/runner/index.d.ts +11 -0
  38. package/dist/polyfill-connectors/src/runner/index.js +10 -0
  39. package/dist/polyfill-connectors/src/runtime-capabilities.d.ts +40 -0
  40. package/dist/polyfill-connectors/src/runtime-capabilities.js +59 -0
  41. package/dist/polyfill-connectors/src/safe-emit.d.ts +3 -0
  42. package/dist/polyfill-connectors/src/safe-emit.js +30 -0
  43. package/dist/polyfill-connectors/src/safe-text-preview.js +156 -0
  44. package/dist/polyfill-connectors/src/schema-registry.js +17 -0
  45. package/dist/polyfill-connectors/src/scope-filters.d.ts +38 -0
  46. package/dist/polyfill-connectors/src/scope-filters.js +80 -0
  47. package/dist/polyfill-connectors/src/shutdown-hook.js +51 -0
  48. package/dist/polyfill-connectors/src/streaming-target-registration.js +161 -0
  49. package/package.json +63 -0
@@ -0,0 +1,1084 @@
1
+ import { spawn } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { mkdirSync, writeFileSync } from "node:fs";
4
+ import { delimiter, join } from "node:path";
5
+ import { createInterface } from "node:readline";
6
+ import { fileURLToPath } from "node:url";
7
+ import { LocalDeviceClient, } from "./local-device-client.js";
8
+ import { buildLocalDeviceRecordEnvelope, hashCanonicalJson, } from "./local-device-envelope.js";
9
+ import { buildLocalDeviceOutboxId, LocalDeviceOutbox, } from "./local-device-outbox.js";
10
+ import { assertPlacementOrThrow, COLLECTOR_RUNTIME_CAPABILITIES, } from "./runtime-capabilities.js";
11
+ export const COLLECTOR_STDERR_MAX_BYTES = 256 * 1024;
12
+ const COLLECTOR_GAP_DETAILS_MAX_CHARS = 300;
13
+ const KEYED_SECRET_RE = /\b(authorization|bearer|token|password|passwd|cookie|secret|otp|api[_-]?key)\b\s*[:=]\s*["']?[^"',\s}]+/gi;
14
+ const OTP_RE = /\b\d{6}\b/g;
15
+ const LONG_OPAQUE_RE = /\b[A-Za-z0-9_-]{24,}\b/g;
16
+ const SCAN_BATCH_LIMIT_DETAIL_RE = /enqueued\s+\d+\s+batches\s+>=\s+(?:run batch limit|(?:maxEnqueuedBatchesPerRun|\[REDACTED\]))\s+(\d+)/;
17
+ const CONNECTOR_PROTOCOL_DEBUG_DIR_ENV = "PDPP_DEBUG_CONNECTOR_PROTOCOL_DIR";
18
+ export const DEFAULT_COLLECTOR_OUTBOX_POLICY = Object.freeze({
19
+ drainBatchSize: 4,
20
+ leaseMs: 60_000,
21
+ maxAttempts: 5,
22
+ maxDrainDurationMs: 120_000,
23
+ maxDrainIterations: 256,
24
+ maxEnqueuedBatchesPerRun: 10_000,
25
+ maxQueueDepth: 10_000,
26
+ retryBackoffMs: 30_000,
27
+ });
28
+ const PACKAGE_ROOT = fileURLToPath(new URL("..", import.meta.url));
29
+ const REPO_ROOT = join(PACKAGE_ROOT, "..", "..");
30
+ export async function enrollCollector(config) {
31
+ const client = new LocalDeviceClient({ baseUrl: config.baseUrl });
32
+ return await client.exchangeEnrollment({
33
+ enrollment_code: config.code,
34
+ ...(config.deviceLabel ? { deviceLabel: config.deviceLabel } : {}),
35
+ });
36
+ }
37
+ export class CollectorStateReadError extends Error {
38
+ constructor(message, cause) {
39
+ super(message, { cause });
40
+ this.name = "CollectorStateReadError";
41
+ }
42
+ }
43
+ export async function runCollectorConnector(config) {
44
+ throwIfAborted(config.abortSignal);
45
+ const satisfiedBindings = assertPlacementOrThrow(config.connector, COLLECTOR_RUNTIME_CAPABILITIES);
46
+ const policy = { ...DEFAULT_COLLECTOR_OUTBOX_POLICY, ...(config.outboxPolicy ?? {}) };
47
+ const holderId = config.collectorHolderId ?? randomUUID();
48
+ const outboxPath = config.outboxPath ?? config.queuePath;
49
+ const outbox = new LocalDeviceOutbox({ path: outboxPath });
50
+ const client = new LocalDeviceClient({
51
+ baseUrl: config.baseUrl,
52
+ deviceId: config.deviceId,
53
+ deviceToken: config.deviceToken,
54
+ });
55
+ try {
56
+ const recoveredLeases = outbox.recoverExpiredLeases({ sourceInstanceId: config.sourceInstanceId });
57
+ const preScanDrain = await drainCollectorOutbox({
58
+ ...(config.abortSignal ? { abortSignal: config.abortSignal } : {}),
59
+ client,
60
+ connectorId: config.connector.connector_id,
61
+ holderId,
62
+ outbox,
63
+ policy,
64
+ sourceInstanceId: config.sourceInstanceId,
65
+ });
66
+ const postDrainSummary = outbox.summary({ sourceInstanceId: config.sourceInstanceId });
67
+ const skipResult = await maybeSkipScanForBacklog({
68
+ client,
69
+ config,
70
+ outbox,
71
+ policy,
72
+ postDrainSummary,
73
+ preScanDrain,
74
+ recoveredLeases,
75
+ satisfiedBindings,
76
+ });
77
+ if (skipResult) {
78
+ return skipResult;
79
+ }
80
+ const priorState = await readPriorStateOrBlock({
81
+ client,
82
+ config,
83
+ recordsPending: pendingOutboxWorkCount(postDrainSummary),
84
+ });
85
+ await client.heartbeat({
86
+ connector_id: config.connector.connector_id,
87
+ outbox: buildHeartbeatOutboxDiagnostics(postDrainSummary, {
88
+ backlogOpen: countOpenBacklogGaps(outbox, config.sourceInstanceId),
89
+ }),
90
+ records_pending: pendingOutboxWorkCount(postDrainSummary),
91
+ source_instance_id: config.sourceInstanceId,
92
+ status: "starting",
93
+ });
94
+ const streamResult = await streamConnectorIntoOutbox({
95
+ ...(config.abortSignal ? { abortSignal: config.abortSignal } : {}),
96
+ batchSize: config.batchSize ?? 100,
97
+ config,
98
+ outbox,
99
+ policy,
100
+ priorState,
101
+ });
102
+ const done = streamResult.done;
103
+ const bufferedState = streamResult.bufferedState;
104
+ const enqueueResult = {
105
+ enqueuedBatches: streamResult.enqueuedBatches,
106
+ recordsQueued: streamResult.recordsQueued,
107
+ };
108
+ const recordDrain = await drainCollectorOutbox({
109
+ ...(config.abortSignal ? { abortSignal: config.abortSignal } : {}),
110
+ client,
111
+ connectorId: config.connector.connector_id,
112
+ holderId,
113
+ outbox,
114
+ policy,
115
+ sourceInstanceId: config.sourceInstanceId,
116
+ });
117
+ const afterRecordsSummary = outbox.summary({ sourceInstanceId: config.sourceInstanceId });
118
+ const checkpointResult = await maybeCommitCheckpoint({
119
+ afterRecordsSummary,
120
+ bufferedState,
121
+ client,
122
+ config,
123
+ holderId,
124
+ outbox,
125
+ policy,
126
+ });
127
+ await recoverResolvedLocalCollectorGaps({
128
+ client,
129
+ config,
130
+ deferRecoveredGapCleanup: streamResult.scanBudgetExceeded,
131
+ outbox,
132
+ });
133
+ const finalSummary = outbox.summary({ sourceInstanceId: config.sourceInstanceId });
134
+ const recordsPending = pendingOutboxWorkCount(finalSummary);
135
+ if (!checkpointResult.statePutFailed) {
136
+ await client.heartbeat({
137
+ connector_id: config.connector.connector_id,
138
+ outbox: buildHeartbeatOutboxDiagnostics(finalSummary, {
139
+ backlogOpen: countOpenBacklogGaps(outbox, config.sourceInstanceId),
140
+ }),
141
+ records_pending: recordsPending,
142
+ source_instance_id: config.sourceInstanceId,
143
+ status: streamResult.scanBudgetExceeded ? "retrying" : heartbeatStatusForSummary(finalSummary, policy),
144
+ });
145
+ }
146
+ return {
147
+ done,
148
+ enqueuedBatches: enqueueResult.enqueuedBatches,
149
+ flushedState: checkpointResult.flushedState,
150
+ outboxSummary: finalSummary,
151
+ priorState,
152
+ recordsQueued: enqueueResult.recordsQueued,
153
+ recoveredLeases,
154
+ satisfiedBindings,
155
+ sentBatches: (preScanDrain.sentByKind.record_batch ?? 0) + (recordDrain.sentByKind.record_batch ?? 0),
156
+ skippedScanForBacklog: false,
157
+ scanBudgetExceeded: streamResult.scanBudgetExceeded,
158
+ statePutFailed: checkpointResult.statePutFailed,
159
+ streamingBufferHighWaterMark: streamResult.bufferHighWaterMark,
160
+ };
161
+ }
162
+ finally {
163
+ outbox.close();
164
+ }
165
+ }
166
+ async function maybeSkipScanForBacklog(input) {
167
+ if (!hasScanBlockingOutboxWork(input.outbox, input.config.sourceInstanceId, input.policy)) {
168
+ return null;
169
+ }
170
+ const recordsPending = pendingOutboxWorkCount(input.postDrainSummary);
171
+ if (recordsPending >= input.policy.maxQueueDepth) {
172
+ ensureCollectorGapRow({
173
+ clock: () => new Date(),
174
+ connectorId: input.config.connector.connector_id,
175
+ details: `pending ${recordsPending} >= maxQueueDepth ${input.policy.maxQueueDepth}`,
176
+ outbox: input.outbox,
177
+ reason: "policy_budget",
178
+ retryable: true,
179
+ ...(input.config.runId ? { runId: input.config.runId } : {}),
180
+ sourceInstanceId: input.config.sourceInstanceId,
181
+ });
182
+ }
183
+ const summaryAfterGap = input.outbox.summary({ sourceInstanceId: input.config.sourceInstanceId });
184
+ const recordsPendingAfterGap = pendingOutboxWorkCount(summaryAfterGap);
185
+ await safeHeartbeat(input.client, {
186
+ connector_id: input.config.connector.connector_id,
187
+ outbox: buildHeartbeatOutboxDiagnostics(summaryAfterGap, {
188
+ backlogOpen: countOpenBacklogGaps(input.outbox, input.config.sourceInstanceId),
189
+ }),
190
+ records_pending: recordsPendingAfterGap,
191
+ source_instance_id: input.config.sourceInstanceId,
192
+ status: heartbeatStatusForSummary(summaryAfterGap, input.policy),
193
+ });
194
+ return {
195
+ done: null,
196
+ enqueuedBatches: 0,
197
+ flushedState: null,
198
+ outboxSummary: summaryAfterGap,
199
+ priorState: Object.freeze({}),
200
+ recordsQueued: 0,
201
+ recoveredLeases: input.recoveredLeases,
202
+ satisfiedBindings: input.satisfiedBindings,
203
+ sentBatches: input.preScanDrain.sentByKind.record_batch ?? 0,
204
+ skippedScanForBacklog: true,
205
+ scanBudgetExceeded: false,
206
+ statePutFailed: false,
207
+ streamingBufferHighWaterMark: 0,
208
+ };
209
+ }
210
+ async function readPriorStateOrBlock(input) {
211
+ try {
212
+ throwIfAborted(input.config.abortSignal);
213
+ const projection = await input.client.getSourceInstanceState({ sourceInstanceId: input.config.sourceInstanceId });
214
+ return projection.state && typeof projection.state === "object"
215
+ ? Object.freeze({ ...projection.state })
216
+ : Object.freeze({});
217
+ }
218
+ catch (error) {
219
+ await safeHeartbeat(input.client, {
220
+ connector_id: input.config.connector.connector_id,
221
+ records_pending: input.recordsPending,
222
+ source_instance_id: input.config.sourceInstanceId,
223
+ status: "blocked",
224
+ });
225
+ throw new CollectorStateReadError(`failed to read prior state for ${input.config.sourceInstanceId}: ${error instanceof Error ? error.message : String(error)}`, error);
226
+ }
227
+ }
228
+ async function streamConnectorIntoOutbox(input) {
229
+ throwIfAborted(input.abortSignal);
230
+ const child = spawnConnector(input.config.connector, {
231
+ baseUrl: input.config.baseUrl,
232
+ deviceToken: input.config.deviceToken,
233
+ ...(input.config.runId ? { runId: input.config.runId } : {}),
234
+ });
235
+ const stderr = new BoundedStderrBuffer(COLLECTOR_STDERR_MAX_BYTES);
236
+ const inScopeStreams = new Set(input.config.connector.streams);
237
+ const bufferedState = {};
238
+ let batchSeq = nextOutboxBatchSeq(input.outbox, input.config.sourceInstanceId);
239
+ let pendingRecords = [];
240
+ let bufferHighWaterMark = 0;
241
+ let recordsQueued = 0;
242
+ let enqueuedBatches = 0;
243
+ let done = null;
244
+ let scanBudgetExceeded = false;
245
+ const flushPendingBatch = () => {
246
+ if (pendingRecords.length === 0) {
247
+ return;
248
+ }
249
+ const chunk = pendingRecords;
250
+ pendingRecords = [];
251
+ const batchId = buildOutboxBatchId({
252
+ batchSeq,
253
+ connectorId: input.config.connector.connector_id,
254
+ records: chunk,
255
+ sourceInstanceId: input.config.sourceInstanceId,
256
+ });
257
+ const envelopes = chunk.map((record) => buildLocalDeviceRecordEnvelope({
258
+ batchId,
259
+ batchSeq,
260
+ connectorId: input.config.connector.connector_id,
261
+ deviceId: input.config.deviceId,
262
+ record,
263
+ sourceInstanceId: input.config.sourceInstanceId,
264
+ }));
265
+ input.outbox.enqueue({
266
+ id: buildLocalDeviceOutboxId({
267
+ kind: "record_batch",
268
+ parts: [input.config.connector.connector_id, batchSeq, batchId],
269
+ sourceInstanceId: input.config.sourceInstanceId,
270
+ }),
271
+ kind: "record_batch",
272
+ payload: {
273
+ batchId,
274
+ batchSeq,
275
+ connectorId: input.config.connector.connector_id,
276
+ deviceId: input.config.deviceId,
277
+ records: envelopes,
278
+ sourceInstanceId: input.config.sourceInstanceId,
279
+ },
280
+ sourceInstanceId: input.config.sourceInstanceId,
281
+ });
282
+ recordsQueued += envelopes.length;
283
+ enqueuedBatches++;
284
+ batchSeq++;
285
+ scanBudgetExceeded = maybeRecordScanBudgetGap({
286
+ enqueuedBatches,
287
+ input,
288
+ scanBudgetExceeded,
289
+ });
290
+ };
291
+ const handleMessage = (message) => {
292
+ if (message.type === "RECORD") {
293
+ pendingRecords.push(message);
294
+ if (pendingRecords.length > bufferHighWaterMark) {
295
+ bufferHighWaterMark = pendingRecords.length;
296
+ }
297
+ if (pendingRecords.length >= input.batchSize) {
298
+ flushPendingBatch();
299
+ }
300
+ return;
301
+ }
302
+ if (message.type === "STATE") {
303
+ if (!inScopeStreams.has(message.stream)) {
304
+ process.stderr.write(`${input.config.connector.connector_id} dropped out-of-scope STATE for stream '${message.stream}'\n`);
305
+ return;
306
+ }
307
+ bufferedState[message.stream] = message.cursor;
308
+ return;
309
+ }
310
+ if (message.type === "DONE") {
311
+ done = message;
312
+ }
313
+ };
314
+ const abortListener = input.abortSignal
315
+ ? () => {
316
+ try {
317
+ child.kill("SIGTERM");
318
+ }
319
+ catch {
320
+ }
321
+ setTimeout(() => {
322
+ try {
323
+ if (!child.killed) {
324
+ child.kill("SIGKILL");
325
+ }
326
+ }
327
+ catch {
328
+ }
329
+ }, 1000).unref?.();
330
+ }
331
+ : null;
332
+ if (input.abortSignal && abortListener) {
333
+ input.abortSignal.addEventListener("abort", abortListener, { once: true });
334
+ }
335
+ const exitPromise = new Promise((resolve, reject) => {
336
+ child.once("error", reject);
337
+ child.once("close", resolve);
338
+ });
339
+ const outputPromise = (async () => {
340
+ const lines = createInterface({ input: child.stdout, terminal: false });
341
+ let lineNumber = 0;
342
+ for await (const line of lines) {
343
+ lineNumber++;
344
+ if (!line.trim()) {
345
+ continue;
346
+ }
347
+ handleMessage(parseConnectorProtocolLine(line, lineNumber, input.config.connector.connector_id));
348
+ if (scanBudgetExceeded) {
349
+ try {
350
+ child.kill("SIGTERM");
351
+ }
352
+ catch {
353
+ }
354
+ break;
355
+ }
356
+ }
357
+ })();
358
+ child.stdin.on("error", () => {
359
+ });
360
+ child.stderr.on("data", (chunk) => stderr.push(chunk));
361
+ child.stdin.end(`${JSON.stringify(buildCollectorStartMessage(input.config.connector.streams, input.config.connector.streamsToBackfill, input.priorState))}\n`);
362
+ let exitCode;
363
+ try {
364
+ [exitCode] = await Promise.all([exitPromise, outputPromise]);
365
+ }
366
+ catch (error) {
367
+ if (input.abortSignal && abortListener) {
368
+ input.abortSignal.removeEventListener("abort", abortListener);
369
+ }
370
+ flushPendingBatch();
371
+ const details = sanitizeCollectorGapDetails(error instanceof Error ? error.message : String(error));
372
+ recordConnectorChildFailureGap({
373
+ details,
374
+ enqueuedBatches,
375
+ input,
376
+ });
377
+ throw new Error(`${input.config.connector.connector_id} connector failed to start or stream output: ${details || "unknown error"}`);
378
+ }
379
+ if (input.abortSignal && abortListener) {
380
+ input.abortSignal.removeEventListener("abort", abortListener);
381
+ }
382
+ if (input.abortSignal?.aborted) {
383
+ flushPendingBatch();
384
+ throw input.abortSignal.reason instanceof Error
385
+ ? input.abortSignal.reason
386
+ : new DOMException("Aborted", "AbortError");
387
+ }
388
+ throwIfConnectorExitedUncleanly({
389
+ enqueuedBatches,
390
+ exitCode,
391
+ flushPendingBatch,
392
+ input,
393
+ scanBudgetExceeded,
394
+ stderr,
395
+ });
396
+ flushPendingBatch();
397
+ return {
398
+ bufferedState: Object.freeze(scanBudgetExceeded ? {} : { ...bufferedState }),
399
+ bufferHighWaterMark,
400
+ done: scanBudgetExceeded ? null : done,
401
+ enqueuedBatches,
402
+ recordsQueued,
403
+ scanBudgetExceeded,
404
+ };
405
+ }
406
+ function parseConnectorProtocolLine(line, lineNumber, connectorId) {
407
+ try {
408
+ return JSON.parse(line);
409
+ }
410
+ catch (error) {
411
+ const debugPath = writeConnectorProtocolDebugLine({ connectorId, error, line, lineNumber });
412
+ const suffix = debugPath ? `; raw line saved to ${debugPath}` : "";
413
+ throw new Error(`${error instanceof Error ? error.message : String(error)} at connector protocol line ${lineNumber} (${line.length} chars)${suffix}`);
414
+ }
415
+ }
416
+ function writeConnectorProtocolDebugLine(input) {
417
+ const dir = process.env[CONNECTOR_PROTOCOL_DEBUG_DIR_ENV]?.trim();
418
+ if (!dir) {
419
+ return null;
420
+ }
421
+ try {
422
+ mkdirSync(dir, { mode: 0o700, recursive: true });
423
+ const path = join(dir, `${input.connectorId}-${Date.now()}-${randomUUID()}.json`);
424
+ writeFileSync(path, `${JSON.stringify({
425
+ connector_id: input.connectorId,
426
+ error: input.error instanceof Error ? input.error.message : String(input.error),
427
+ line: input.line,
428
+ line_length: input.line.length,
429
+ line_number: input.lineNumber,
430
+ }, null, 2)}\n`, { mode: 0o600 });
431
+ return path;
432
+ }
433
+ catch {
434
+ return null;
435
+ }
436
+ }
437
+ function throwIfConnectorExitedUncleanly(input) {
438
+ if (input.exitCode === 0 || input.scanBudgetExceeded) {
439
+ return;
440
+ }
441
+ input.flushPendingBatch();
442
+ const details = sanitizeCollectorGapDetails(`exit ${input.exitCode}: ${input.stderr.toString().trim()}`);
443
+ recordConnectorChildFailureGap({
444
+ details,
445
+ enqueuedBatches: input.enqueuedBatches,
446
+ input: input.input,
447
+ });
448
+ throw new Error(`${input.input.config.connector.connector_id} connector exited ${input.exitCode}: ${details || "unknown error"}`);
449
+ }
450
+ function maybeRecordScanBudgetGap(input) {
451
+ if (input.scanBudgetExceeded) {
452
+ return true;
453
+ }
454
+ if (input.enqueuedBatches < input.input.policy.maxEnqueuedBatchesPerRun) {
455
+ return false;
456
+ }
457
+ ensureCollectorGapRow({
458
+ clock: () => new Date(),
459
+ connectorId: input.input.config.connector.connector_id,
460
+ details: `enqueued ${input.enqueuedBatches} batches >= run batch limit ${input.input.policy.maxEnqueuedBatchesPerRun}`,
461
+ outbox: input.input.outbox,
462
+ reason: "policy_budget",
463
+ retryable: true,
464
+ ...(input.input.config.runId ? { runId: input.input.config.runId } : {}),
465
+ sourceInstanceId: input.input.config.sourceInstanceId,
466
+ });
467
+ return true;
468
+ }
469
+ function recordConnectorChildFailureGap(input) {
470
+ if (input.enqueuedBatches === 0) {
471
+ return;
472
+ }
473
+ ensureCollectorGapRow({
474
+ clock: () => new Date(),
475
+ connectorId: input.input.config.connector.connector_id,
476
+ details: input.details,
477
+ outbox: input.input.outbox,
478
+ reason: "connector_child_failure",
479
+ retryable: true,
480
+ ...(input.input.config.runId ? { runId: input.input.config.runId } : {}),
481
+ sourceInstanceId: input.input.config.sourceInstanceId,
482
+ });
483
+ }
484
+ async function maybeCommitCheckpoint(input) {
485
+ if (Object.keys(input.bufferedState).length === 0) {
486
+ return { flushedState: null, statePutFailed: false };
487
+ }
488
+ const checkpointId = buildLocalDeviceOutboxId({
489
+ kind: "checkpoint",
490
+ parts: [input.config.connector.connector_id, input.bufferedState],
491
+ sourceInstanceId: input.config.sourceInstanceId,
492
+ });
493
+ input.outbox.enqueue({
494
+ id: checkpointId,
495
+ kind: "checkpoint",
496
+ payload: {
497
+ connectorId: input.config.connector.connector_id,
498
+ sourceInstanceId: input.config.sourceInstanceId,
499
+ state: input.bufferedState,
500
+ },
501
+ sourceInstanceId: input.config.sourceInstanceId,
502
+ });
503
+ const checkpointDrain = await drainCollectorOutbox({
504
+ ...(input.config.abortSignal ? { abortSignal: input.config.abortSignal } : {}),
505
+ client: input.client,
506
+ connectorId: input.config.connector.connector_id,
507
+ holderId: input.holderId,
508
+ outbox: input.outbox,
509
+ policy: input.policy,
510
+ sourceInstanceId: input.config.sourceInstanceId,
511
+ });
512
+ const checkpointAfter = input.outbox.get(checkpointId);
513
+ if (checkpointAfter?.status === "succeeded") {
514
+ return { flushedState: Object.freeze({ ...input.bufferedState }), statePutFailed: false };
515
+ }
516
+ if (checkpointAfter && hasCheckpointPredecessorBlockingWork(input.outbox, checkpointAfter)) {
517
+ return { flushedState: null, statePutFailed: false };
518
+ }
519
+ await safeHeartbeat(input.client, {
520
+ connector_id: input.config.connector.connector_id,
521
+ outbox: buildHeartbeatOutboxDiagnostics(input.afterRecordsSummary, {
522
+ backlogOpen: countOpenBacklogGaps(input.outbox, input.config.sourceInstanceId),
523
+ }),
524
+ records_pending: pendingOutboxWorkCount(input.afterRecordsSummary),
525
+ source_instance_id: input.config.sourceInstanceId,
526
+ status: "retrying",
527
+ });
528
+ process.stderr.write(`${input.config.connector.connector_id} checkpoint not yet committed (drained ${checkpointDrain.sent} this pass; ${checkpointAfter?.last_error ?? "no error"})\n`);
529
+ return { flushedState: null, statePutFailed: true };
530
+ }
531
+ async function recoverResolvedLocalCollectorGaps(input) {
532
+ if (input.deferRecoveredGapCleanup) {
533
+ return;
534
+ }
535
+ if (hasCheckpointBlockingOutboxWork(input.outbox, input.config.sourceInstanceId)) {
536
+ return;
537
+ }
538
+ const succeededGaps = input.outbox.listByKind({
539
+ kind: "gap",
540
+ sourceInstanceId: input.config.sourceInstanceId,
541
+ statuses: ["succeeded"],
542
+ });
543
+ for (const item of succeededGaps) {
544
+ let payload;
545
+ try {
546
+ payload = assertGapPayload(item.payload, item.id);
547
+ }
548
+ catch (error) {
549
+ process.stderr.write(`${input.config.connector.connector_id} skipped malformed succeeded gap recovery ${item.id}: ${error instanceof Error ? error.message : String(error)}\n`);
550
+ continue;
551
+ }
552
+ try {
553
+ await input.client.recoverLocalCollectorGap({
554
+ connector_id: payload.connectorId,
555
+ reason: payload.reason,
556
+ source_instance_id: payload.sourceInstanceId,
557
+ ...(input.config.runId ? { recovered_run_id: input.config.runId } : {}),
558
+ ...(payload.stream ? { stream: payload.stream } : {}),
559
+ ...(payload.streamBoundary ? { stream_boundary: payload.streamBoundary } : {}),
560
+ });
561
+ input.outbox.deleteSucceeded(item.id);
562
+ }
563
+ catch (error) {
564
+ process.stderr.write(`${input.config.connector.connector_id} local gap recovery deferred for ${item.id}: ${error instanceof Error ? error.message : String(error)}\n`);
565
+ }
566
+ }
567
+ }
568
+ async function safeHeartbeat(client, request) {
569
+ try {
570
+ await client.heartbeat(request);
571
+ }
572
+ catch {
573
+ }
574
+ }
575
+ export function buildCollectorStartMessage(streams, streamsToBackfill = [], priorState) {
576
+ const start = {
577
+ scope: { streams: streams.map((name) => ({ name })) },
578
+ type: "START",
579
+ };
580
+ if (streamsToBackfill.length > 0) {
581
+ start.streamsToBackfill = [...streamsToBackfill];
582
+ }
583
+ if (priorState && Object.keys(priorState).length > 0) {
584
+ start.state = { ...priorState };
585
+ }
586
+ return start;
587
+ }
588
+ export function transformRecordsToCollectorEnvelopes(input) {
589
+ return input.messages
590
+ .filter((msg) => msg.type === "RECORD")
591
+ .map((record) => buildLocalDeviceRecordEnvelope({
592
+ batchId: input.batchId,
593
+ batchSeq: input.batchSeq,
594
+ connectorId: input.connectorId,
595
+ deviceId: input.deviceId,
596
+ record,
597
+ sourceInstanceId: input.sourceInstanceId,
598
+ }));
599
+ }
600
+ function ensureCollectorGapRow(input) {
601
+ const firstSeenAt = input.clock().toISOString();
602
+ const idParts = [
603
+ input.connectorId,
604
+ input.sourceInstanceId,
605
+ input.reason,
606
+ input.stream ?? null,
607
+ input.streamBoundary ?? null,
608
+ ];
609
+ const id = buildLocalDeviceOutboxId({
610
+ kind: "gap",
611
+ parts: idParts,
612
+ sourceInstanceId: input.sourceInstanceId,
613
+ });
614
+ const existing = input.outbox.get(id);
615
+ if (existing) {
616
+ return existing;
617
+ }
618
+ const payload = {
619
+ connectorId: input.connectorId,
620
+ firstSeenAt,
621
+ nextAttemptBackoffMs: DEFAULT_GAP_RETRY_BACKOFF_MS,
622
+ reason: input.reason,
623
+ retryable: input.retryable,
624
+ sourceInstanceId: input.sourceInstanceId,
625
+ };
626
+ if (input.stream) {
627
+ payload.stream = input.stream;
628
+ }
629
+ if (input.streamBoundary) {
630
+ payload.streamBoundary = input.streamBoundary;
631
+ }
632
+ if (input.runId) {
633
+ payload.firstSeenRunId = input.runId;
634
+ }
635
+ const details = input.details ? sanitizeCollectorGapDetails(input.details) : null;
636
+ if (details) {
637
+ payload.details = details;
638
+ }
639
+ return input.outbox.enqueue({
640
+ id,
641
+ kind: "gap",
642
+ payload,
643
+ sourceInstanceId: input.sourceInstanceId,
644
+ });
645
+ }
646
+ function sanitizeCollectorGapDetails(value) {
647
+ let next = String(value)
648
+ .replace(KEYED_SECRET_RE, (_match, marker) => `${marker}=[REDACTED]`)
649
+ .replace(OTP_RE, "[REDACTED_OTP]")
650
+ .replace(LONG_OPAQUE_RE, "[REDACTED]")
651
+ .replace(/\s+/g, " ")
652
+ .trim();
653
+ if (next.length > COLLECTOR_GAP_DETAILS_MAX_CHARS) {
654
+ next = `${next.slice(0, COLLECTOR_GAP_DETAILS_MAX_CHARS - 1)}…`;
655
+ }
656
+ return next;
657
+ }
658
+ export async function drainCollectorOutbox(input) {
659
+ const sentByKind = {};
660
+ const result = {
661
+ deadLettered: 0,
662
+ durationBudgetExceeded: false,
663
+ failed: 0,
664
+ iterations: 0,
665
+ sent: 0,
666
+ sentByKind,
667
+ };
668
+ const startedAt = Date.now();
669
+ for (let i = 0; i < input.policy.maxDrainIterations; i++) {
670
+ throwIfAborted(input.abortSignal);
671
+ if (Date.now() - startedAt >= input.policy.maxDrainDurationMs) {
672
+ result.durationBudgetExceeded = true;
673
+ return result;
674
+ }
675
+ const claimed = claimReadyOutboxItems(input);
676
+ if (claimed.length === 0) {
677
+ return result;
678
+ }
679
+ result.iterations++;
680
+ for (const item of claimed) {
681
+ await drainClaimedOutboxItem(input, item, result, sentByKind);
682
+ }
683
+ }
684
+ return result;
685
+ }
686
+ function claimReadyOutboxItems(input) {
687
+ const nextReady = nextReadyOutboxItem(input);
688
+ if (!nextReady) {
689
+ return [];
690
+ }
691
+ if (nextReady.kind === "checkpoint" && hasCheckpointPredecessorBlockingWork(input.outbox, nextReady)) {
692
+ return [];
693
+ }
694
+ const claimInput = {
695
+ excludeKinds: nextReady.kind === "checkpoint" ? [] : ["checkpoint"],
696
+ holder: input.holderId,
697
+ leaseMs: input.policy.leaseMs,
698
+ limit: nextReady.kind === "checkpoint" ? 1 : input.policy.drainBatchSize,
699
+ };
700
+ if (input.sourceInstanceId) {
701
+ claimInput.sourceInstanceId = input.sourceInstanceId;
702
+ }
703
+ return input.outbox.claimReady(claimInput);
704
+ }
705
+ function nextReadyOutboxItem(input) {
706
+ return input.outbox.peekReady(input.sourceInstanceId ? { sourceInstanceId: input.sourceInstanceId } : {});
707
+ }
708
+ function hasCheckpointPredecessorBlockingWork(outbox, checkpoint) {
709
+ return outbox.hasNonSucceededPredecessor({
710
+ beforeInsertOrder: checkpoint.insert_order,
711
+ kinds: ["record_batch", "gap"],
712
+ sourceInstanceId: checkpoint.source_instance_id,
713
+ });
714
+ }
715
+ async function drainClaimedOutboxItem(input, item, result, sentByKind) {
716
+ throwIfAborted(input.abortSignal);
717
+ try {
718
+ await sendOutboxItem(input.client, item);
719
+ input.outbox.acknowledge({ holder: input.holderId, id: item.id, leaseEpoch: item.lease_epoch });
720
+ result.sent++;
721
+ sentByKind[item.kind] = (sentByKind[item.kind] ?? 0) + 1;
722
+ }
723
+ catch (error) {
724
+ failOutboxItem(input, item, error, result);
725
+ }
726
+ }
727
+ function failOutboxItem(input, item, error, result) {
728
+ const message = error instanceof Error ? error.message : String(error);
729
+ if (error instanceof OutboxPayloadShapeError || item.attempt_count + 1 >= input.policy.maxAttempts) {
730
+ input.outbox.deadLetter({
731
+ error: message,
732
+ holder: input.holderId,
733
+ id: item.id,
734
+ leaseEpoch: item.lease_epoch,
735
+ });
736
+ result.deadLettered++;
737
+ return;
738
+ }
739
+ input.outbox.failRetryable({
740
+ error: message,
741
+ holder: input.holderId,
742
+ id: item.id,
743
+ leaseEpoch: item.lease_epoch,
744
+ retryBackoffMs: input.policy.retryBackoffMs * (item.attempt_count + 1),
745
+ });
746
+ result.failed++;
747
+ }
748
+ class OutboxPayloadShapeError extends Error {
749
+ constructor(message) {
750
+ super(message);
751
+ this.name = "OutboxPayloadShapeError";
752
+ }
753
+ }
754
+ const DEFAULT_GAP_RETRY_BACKOFF_MS = 15 * 60_000;
755
+ async function sendOutboxItem(client, item) {
756
+ if (item.kind === "record_batch") {
757
+ const payload = assertRecordBatchPayload(item.payload, item.id);
758
+ if (payload.records.length === 0) {
759
+ throw new OutboxPayloadShapeError(`record_batch payload has no records: ${item.id}`);
760
+ }
761
+ await client.ingestBatch({
762
+ batch_id: payload.batchId,
763
+ batch_seq: payload.batchSeq,
764
+ body_hash: hashCanonicalJson(payload.records),
765
+ connector_id: payload.connectorId,
766
+ device_id: payload.deviceId,
767
+ records: payload.records.map((record) => ({
768
+ data: record.data,
769
+ emitted_at: record.emitted_at,
770
+ record_key: record.record_key,
771
+ stream: record.stream,
772
+ })),
773
+ source_instance_id: payload.sourceInstanceId,
774
+ });
775
+ return;
776
+ }
777
+ if (item.kind === "checkpoint") {
778
+ const payload = assertCheckpointPayload(item.payload, item.id);
779
+ await client.putSourceInstanceState({
780
+ sourceInstanceId: payload.sourceInstanceId,
781
+ state: payload.state,
782
+ });
783
+ return;
784
+ }
785
+ if (item.kind === "gap") {
786
+ const payload = assertGapPayload(item.payload, item.id);
787
+ await client.ackLocalCollectorGap({
788
+ connector_id: payload.connectorId,
789
+ first_seen_at: payload.firstSeenAt,
790
+ next_attempt_backoff_ms: payload.nextAttemptBackoffMs,
791
+ reason: payload.reason,
792
+ retryable: payload.retryable,
793
+ source_instance_id: payload.sourceInstanceId,
794
+ ...(payload.stream ? { stream: payload.stream } : {}),
795
+ ...(payload.streamBoundary ? { stream_boundary: payload.streamBoundary } : {}),
796
+ ...(payload.firstSeenRunId ? { first_seen_run_id: payload.firstSeenRunId } : {}),
797
+ ...(payload.firstSeenRunId ? { last_run_id: payload.firstSeenRunId } : {}),
798
+ ...(payload.details ? { details: payload.details } : {}),
799
+ });
800
+ return;
801
+ }
802
+ throw new OutboxPayloadShapeError(`unsupported outbox kind ${item.kind} for id ${item.id}`);
803
+ }
804
+ function assertRecordBatchPayload(payload, id) {
805
+ if (!isRecord(payload)) {
806
+ throw new OutboxPayloadShapeError(`record_batch payload is not an object: ${id}`);
807
+ }
808
+ if (typeof payload.batchId !== "string" ||
809
+ typeof payload.batchSeq !== "number" ||
810
+ typeof payload.connectorId !== "string" ||
811
+ typeof payload.deviceId !== "string" ||
812
+ typeof payload.sourceInstanceId !== "string" ||
813
+ !Array.isArray(payload.records) ||
814
+ !payload.records.every(isLocalDeviceRecordEnvelope)) {
815
+ throw new OutboxPayloadShapeError(`record_batch payload missing required fields: ${id}`);
816
+ }
817
+ return {
818
+ batchId: payload.batchId,
819
+ batchSeq: payload.batchSeq,
820
+ connectorId: payload.connectorId,
821
+ deviceId: payload.deviceId,
822
+ records: payload.records,
823
+ sourceInstanceId: payload.sourceInstanceId,
824
+ };
825
+ }
826
+ function assertGapPayload(payload, id) {
827
+ if (!isRecord(payload)) {
828
+ throw new OutboxPayloadShapeError(`gap payload is not an object: ${id}`);
829
+ }
830
+ if (typeof payload.connectorId !== "string" ||
831
+ typeof payload.sourceInstanceId !== "string" ||
832
+ typeof payload.firstSeenAt !== "string" ||
833
+ typeof payload.nextAttemptBackoffMs !== "number" ||
834
+ typeof payload.retryable !== "boolean" ||
835
+ (payload.reason !== "policy_budget" && payload.reason !== "connector_child_failure") ||
836
+ (payload.stream !== undefined && typeof payload.stream !== "string") ||
837
+ (payload.streamBoundary !== undefined && typeof payload.streamBoundary !== "string") ||
838
+ (payload.firstSeenRunId !== undefined && typeof payload.firstSeenRunId !== "string") ||
839
+ (payload.details !== undefined && typeof payload.details !== "string")) {
840
+ throw new OutboxPayloadShapeError(`gap payload missing or invalid fields: ${id}`);
841
+ }
842
+ const gap = {
843
+ connectorId: payload.connectorId,
844
+ firstSeenAt: payload.firstSeenAt,
845
+ nextAttemptBackoffMs: payload.nextAttemptBackoffMs,
846
+ reason: payload.reason,
847
+ retryable: payload.retryable,
848
+ sourceInstanceId: payload.sourceInstanceId,
849
+ };
850
+ if (typeof payload.stream === "string") {
851
+ gap.stream = payload.stream;
852
+ }
853
+ if (typeof payload.streamBoundary === "string") {
854
+ gap.streamBoundary = payload.streamBoundary;
855
+ }
856
+ if (typeof payload.firstSeenRunId === "string") {
857
+ gap.firstSeenRunId = payload.firstSeenRunId;
858
+ }
859
+ if (typeof payload.details === "string") {
860
+ gap.details = payload.details;
861
+ }
862
+ return gap;
863
+ }
864
+ function assertCheckpointPayload(payload, id) {
865
+ if (!isRecord(payload)) {
866
+ throw new OutboxPayloadShapeError(`checkpoint payload is not an object: ${id}`);
867
+ }
868
+ if (typeof payload.connectorId !== "string" ||
869
+ typeof payload.sourceInstanceId !== "string" ||
870
+ !isRecord(payload.state)) {
871
+ throw new OutboxPayloadShapeError(`checkpoint payload missing required fields: ${id}`);
872
+ }
873
+ return {
874
+ connectorId: payload.connectorId,
875
+ sourceInstanceId: payload.sourceInstanceId,
876
+ state: payload.state,
877
+ };
878
+ }
879
+ function isLocalDeviceRecordEnvelope(value) {
880
+ return (isRecord(value) &&
881
+ typeof value.batch_id === "string" &&
882
+ typeof value.batch_seq === "number" &&
883
+ typeof value.body_hash === "string" &&
884
+ typeof value.connector_id === "string" &&
885
+ isRecord(value.data) &&
886
+ typeof value.device_id === "string" &&
887
+ typeof value.emitted_at === "string" &&
888
+ typeof value.record_key === "string" &&
889
+ typeof value.source_instance_id === "string" &&
890
+ typeof value.stream === "string");
891
+ }
892
+ function isRecord(value) {
893
+ return typeof value === "object" && value !== null && !Array.isArray(value);
894
+ }
895
+ function buildOutboxBatchId(input) {
896
+ return `local-batch:${hashCanonicalJson({
897
+ batch_seq: input.batchSeq,
898
+ connector_id: input.connectorId,
899
+ records: input.records.map((record) => ({
900
+ data: record.data,
901
+ emitted_at: record.emitted_at,
902
+ key: String(record.key),
903
+ stream: record.stream,
904
+ })),
905
+ source_instance_id: input.sourceInstanceId,
906
+ })}`;
907
+ }
908
+ function pendingOutboxWorkCount(summary) {
909
+ return summary.ready + summary.leased;
910
+ }
911
+ function hasScanBlockingOutboxWork(outbox, sourceInstanceId, policy) {
912
+ if (outbox.hasNonSucceededWork({ excludeKinds: ["gap"], sourceInstanceId })) {
913
+ return true;
914
+ }
915
+ return outbox.listByKind({ kind: "gap", sourceInstanceId }).some((item) => isUnresolvedScanBudgetGap(item, policy));
916
+ }
917
+ function hasCheckpointBlockingOutboxWork(outbox, sourceInstanceId) {
918
+ return outbox.hasNonSucceededWork({ sourceInstanceId });
919
+ }
920
+ function isUnresolvedScanBudgetGap(item, policy) {
921
+ if (item.status === "dead_letter") {
922
+ return false;
923
+ }
924
+ let payload;
925
+ try {
926
+ payload = assertGapPayload(item.payload, item.id);
927
+ }
928
+ catch {
929
+ return item.status !== "succeeded";
930
+ }
931
+ if (payload.reason !== "policy_budget" || !payload.retryable || !payload.details) {
932
+ return false;
933
+ }
934
+ const match = payload.details.match(SCAN_BATCH_LIMIT_DETAIL_RE);
935
+ if (!match?.[1]) {
936
+ return false;
937
+ }
938
+ return policy.maxEnqueuedBatchesPerRun <= Number(match[1]);
939
+ }
940
+ function heartbeatStatusForSummary(summary, policy) {
941
+ if (summary.deadLetter > 0) {
942
+ return "blocked";
943
+ }
944
+ const pending = pendingOutboxWorkCount(summary);
945
+ if (policy && pending >= policy.maxQueueDepth) {
946
+ return "blocked";
947
+ }
948
+ if (pending > 0) {
949
+ return "retrying";
950
+ }
951
+ return "healthy";
952
+ }
953
+ export function buildHeartbeatOutboxDiagnostics(summary, options = {}) {
954
+ return {
955
+ backlog_open: Math.max(0, options.backlogOpen ?? 0),
956
+ dead_letter: summary.deadLetter,
957
+ leased: summary.leased,
958
+ oldest_pending_at: summary.oldestReadyAt,
959
+ pending: summary.ready,
960
+ retrying: summary.retrying,
961
+ stale_leases: summary.staleLeases,
962
+ succeeded: summary.succeeded,
963
+ total: summary.total,
964
+ };
965
+ }
966
+ function countOpenBacklogGaps(outbox, sourceInstanceId) {
967
+ return outbox.countOpenGaps({ sourceInstanceId });
968
+ }
969
+ function nextOutboxBatchSeq(outbox, sourceInstanceId) {
970
+ return outbox.maxRecordBatchSeq({ sourceInstanceId }) + 1;
971
+ }
972
+ export async function drainCollectorQueue(input) {
973
+ let sent = 0;
974
+ for (;;) {
975
+ throwIfAborted(input.abortSignal);
976
+ const item = await input.queue.dequeueReady();
977
+ if (!item) {
978
+ return sent;
979
+ }
980
+ try {
981
+ await sendQueueItem(input.client, item);
982
+ await input.queue.markSent(item.batch_id);
983
+ sent++;
984
+ }
985
+ catch (error) {
986
+ await input.queue.markRetry(item.batch_id, error instanceof Error ? error.message : String(error));
987
+ return sent;
988
+ }
989
+ }
990
+ }
991
+ export function recoverAndSummarizeOutbox(outbox, input = {}) {
992
+ const recovered = input.sourceInstanceId
993
+ ? outbox.recoverExpiredLeases({ sourceInstanceId: input.sourceInstanceId })
994
+ : outbox.recoverExpiredLeases();
995
+ const summary = input.sourceInstanceId
996
+ ? outbox.summary({ sourceInstanceId: input.sourceInstanceId })
997
+ : outbox.summary();
998
+ return { recovered, summary };
999
+ }
1000
+ function throwIfAborted(signal) {
1001
+ if (signal?.aborted) {
1002
+ throw signal.reason instanceof Error ? signal.reason : new DOMException("Aborted", "AbortError");
1003
+ }
1004
+ }
1005
+ async function sendQueueItem(client, item) {
1006
+ const firstRecord = item.records[0];
1007
+ if (!firstRecord) {
1008
+ throw new Error(`collector batch has no records: ${item.batch_id}`);
1009
+ }
1010
+ await client.ingestBatch({
1011
+ batch_id: item.batch_id,
1012
+ batch_seq: item.batch_seq,
1013
+ body_hash: hashCanonicalJson(item.records),
1014
+ connector_id: firstRecord.connector_id,
1015
+ device_id: firstRecord.device_id,
1016
+ records: item.records.map((record) => ({
1017
+ data: record.data,
1018
+ emitted_at: record.emitted_at,
1019
+ record_key: record.record_key,
1020
+ stream: record.stream,
1021
+ })),
1022
+ source_instance_id: item.source_instance_id,
1023
+ });
1024
+ }
1025
+ function buildCollectorChildEnv(context) {
1026
+ const env = {
1027
+ PDPP_REFERENCE_BASE_URL: context.baseUrl,
1028
+ PDPP_LOCAL_DEVICE_TOKEN: context.deviceToken,
1029
+ };
1030
+ if (context.runId) {
1031
+ env.PDPP_RUN_ID = context.runId;
1032
+ }
1033
+ return env;
1034
+ }
1035
+ class BoundedStderrBuffer {
1036
+ #limit;
1037
+ #chunks = [];
1038
+ #size = 0;
1039
+ #dropped = 0;
1040
+ constructor(limit) {
1041
+ this.#limit = Math.max(1024, limit);
1042
+ }
1043
+ push(chunk) {
1044
+ this.#chunks.push(chunk);
1045
+ this.#size += chunk.length;
1046
+ while (this.#size > this.#limit && this.#chunks.length > 0) {
1047
+ const head = this.#chunks[0];
1048
+ if (!head) {
1049
+ break;
1050
+ }
1051
+ const overflow = this.#size - this.#limit;
1052
+ if (head.length <= overflow) {
1053
+ this.#chunks.shift();
1054
+ this.#size -= head.length;
1055
+ this.#dropped += head.length;
1056
+ continue;
1057
+ }
1058
+ this.#chunks[0] = head.subarray(overflow);
1059
+ this.#size -= overflow;
1060
+ this.#dropped += overflow;
1061
+ break;
1062
+ }
1063
+ }
1064
+ toString() {
1065
+ const body = Buffer.concat(this.#chunks).toString("utf8");
1066
+ if (this.#dropped === 0) {
1067
+ return body;
1068
+ }
1069
+ return `[truncated ${this.#dropped} stderr bytes]\n${body}`;
1070
+ }
1071
+ }
1072
+ function spawnConnector(connector, childContext) {
1073
+ const env = { ...process.env, ...buildCollectorChildEnv(childContext), ...connector.env };
1074
+ env.PATH = buildCollectorChildPath(env.PATH);
1075
+ return spawn(connector.command, [...connector.args], {
1076
+ cwd: PACKAGE_ROOT,
1077
+ env,
1078
+ });
1079
+ }
1080
+ function buildCollectorChildPath(pathValue) {
1081
+ return [join(PACKAGE_ROOT, "node_modules", ".bin"), join(REPO_ROOT, "node_modules", ".bin"), pathValue]
1082
+ .filter((part) => Boolean(part))
1083
+ .join(delimiter);
1084
+ }