@shipfox/api-logs 5.0.0 → 7.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/.turbo/turbo-build.log +14 -14
  2. package/CHANGELOG.md +46 -0
  3. package/dist/api/object-storage.d.ts.map +1 -1
  4. package/dist/api/object-storage.js +28 -2
  5. package/dist/api/object-storage.js.map +1 -1
  6. package/dist/config.d.ts +1 -0
  7. package/dist/config.d.ts.map +1 -1
  8. package/dist/config.js +5 -10
  9. package/dist/config.js.map +1 -1
  10. package/dist/core/append-logs.d.ts +2 -6
  11. package/dist/core/append-logs.d.ts.map +1 -1
  12. package/dist/core/append-logs.js +7 -16
  13. package/dist/core/append-logs.js.map +1 -1
  14. package/dist/core/reap-stale-open-streams.d.ts.map +1 -1
  15. package/dist/core/reap-stale-open-streams.js +7 -0
  16. package/dist/core/reap-stale-open-streams.js.map +1 -1
  17. package/dist/core/retention.d.ts.map +1 -1
  18. package/dist/core/retention.js +25 -0
  19. package/dist/core/retention.js.map +1 -1
  20. package/dist/index.d.ts +5 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +63 -59
  23. package/dist/index.js.map +1 -1
  24. package/dist/presentation/routes/append-logs.d.ts +2 -1
  25. package/dist/presentation/routes/append-logs.d.ts.map +1 -1
  26. package/dist/presentation/routes/append-logs.js +61 -61
  27. package/dist/presentation/routes/append-logs.js.map +1 -1
  28. package/dist/presentation/routes/index.d.ts +2 -1
  29. package/dist/presentation/routes/index.d.ts.map +1 -1
  30. package/dist/presentation/routes/index.js +25 -23
  31. package/dist/presentation/routes/index.js.map +1 -1
  32. package/dist/temporal/activities/compact-stream.d.ts.map +1 -1
  33. package/dist/temporal/activities/compact-stream.js +17 -1
  34. package/dist/temporal/activities/compact-stream.js.map +1 -1
  35. package/dist/temporal/activities/compaction-reconcile.d.ts.map +1 -1
  36. package/dist/temporal/activities/compaction-reconcile.js +7 -0
  37. package/dist/temporal/activities/compaction-reconcile.js.map +1 -1
  38. package/dist/temporal/workflows/index.bundle.js +17164 -3306
  39. package/dist/tsconfig.test.tsbuildinfo +1 -1
  40. package/package.json +12 -30
  41. package/src/api/object-storage.ts +21 -2
  42. package/src/config.test.ts +8 -32
  43. package/src/config.ts +10 -15
  44. package/src/core/append-logs.test.ts +13 -19
  45. package/src/core/append-logs.ts +12 -20
  46. package/src/core/reap-stale-open-streams.ts +2 -0
  47. package/src/core/retention.ts +5 -0
  48. package/src/index.ts +63 -51
  49. package/src/presentation/routes/append-logs.test.ts +3 -2
  50. package/src/presentation/routes/append-logs.ts +54 -50
  51. package/src/presentation/routes/index.ts +22 -19
  52. package/src/presentation/routes/read-logs.test.ts +3 -2
  53. package/src/temporal/activities/compact-stream.ts +13 -1
  54. package/src/temporal/activities/compaction-reconcile.ts +2 -0
  55. package/test/env.ts +1 -3
  56. package/test/fixtures/lease-token.ts +2 -1
  57. package/test/fixtures/workflows-client.ts +17 -0
  58. package/tsconfig.build.tsbuildinfo +1 -1
@@ -1,3 +1,4 @@
1
+ import { reportError } from '@shipfox/node-error-monitoring';
1
2
  import { logger } from '@shipfox/node-opentelemetry';
2
3
  import { deleteObjectsByPrefix } from '#api/object-storage.js';
3
4
  import { config } from '#config.js';
@@ -81,6 +82,12 @@ function deleteExpiredStreamRow(params) {
81
82
  err: error,
82
83
  streamId: stream.id
83
84
  }, 'Failed to delete expired log stream objects');
85
+ reportError(error, {
86
+ boundary: 'logs.maintenance',
87
+ extra: {
88
+ streamId: stream.id
89
+ }
90
+ });
84
91
  continue;
85
92
  }
86
93
  let outcome;
@@ -96,6 +103,12 @@ function deleteExpiredStreamRow(params) {
96
103
  err: error,
97
104
  streamId: stream.id
98
105
  }, 'Failed to delete expired log stream row');
106
+ reportError(error, {
107
+ boundary: 'logs.maintenance',
108
+ extra: {
109
+ streamId: stream.id
110
+ }
111
+ });
99
112
  continue;
100
113
  }
101
114
  if (!outcome.deleted) {
@@ -109,6 +122,12 @@ function deleteExpiredStreamRow(params) {
109
122
  err: error,
110
123
  streamId: stream.id
111
124
  }, 'Failed to reload raced expired log stream');
125
+ reportError(error, {
126
+ boundary: 'logs.maintenance',
127
+ extra: {
128
+ streamId: stream.id
129
+ }
130
+ });
112
131
  continue;
113
132
  }
114
133
  if (current) {
@@ -125,6 +144,12 @@ function deleteExpiredStreamRow(params) {
125
144
  err: error,
126
145
  streamId: stream.id
127
146
  }, 'Failed to delete raced expired log stream');
147
+ reportError(error, {
148
+ boundary: 'logs.maintenance',
149
+ extra: {
150
+ streamId: stream.id
151
+ }
152
+ });
128
153
  continue;
129
154
  }
130
155
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/core/retention.ts"],"sourcesContent":["import {logger} from '@shipfox/node-opentelemetry';\nimport {deleteObjectsByPrefix} from '#api/object-storage.js';\nimport {config} from '#config.js';\nimport type {AttemptStream} from '#core/entities/attempt-stream.js';\nimport {logObjectKey} from '#core/entities/log-object.js';\nimport {deleteJobAccounting} from '#db/accounting.js';\nimport {db} from '#db/db.js';\nimport {\n accountingHasNoStreams,\n deleteExpiredStream,\n getAttemptStreamById,\n listExpiredClosedStreams,\n} from '#db/streams.js';\n\nexport interface RetentionSweepResult {\n /** Streams whose objects and row were deleted. */\n deleted: number;\n /** Streams skipped because compaction changed `object_key` after we read it (retried next run). */\n raced: number;\n /** Streams whose object cleanup or guarded row delete threw; logged, skipped, and retried next run. */\n failed: number;\n accountingPruned: number;\n iterations: number;\n /** True when the sweep stopped on its wall-clock budget with backlog likely remaining. */\n timedOut: boolean;\n}\n\nexport interface RunRetentionSweepParams {\n retentionDays: number;\n batchLimit: number;\n timeBudgetMs: number;\n maxIterations: number;\n /** Wall clock; injectable so tests can drive the time budget deterministically. */\n now?: () => number;\n /** Liveness signal (e.g. the activity heartbeat); invoked once per processed stream. */\n onProgress?: () => void;\n}\n\nfunction deleteExpiredStreamObjects(stream: AttemptStream): Promise<void> {\n return deleteObjectsByPrefix(`${logObjectKey(config.LOG_STORAGE_S3_PREFIX, stream)}/`);\n}\n\nfunction deleteExpiredStreamRow(params: {\n stream: AttemptStream;\n retentionDays: number;\n}): Promise<{deleted: boolean; prunedAccounting: boolean}> {\n return db().transaction(async (tx) => {\n const {deleted, jobId} = await deleteExpiredStream(tx, {\n streamId: params.stream.id,\n observedObjectKey: params.stream.objectKey,\n });\n if (deleted && jobId && (await accountingHasNoStreams(tx, jobId))) {\n const pruned = await deleteJobAccounting(tx, {\n jobId,\n retentionDays: params.retentionDays,\n });\n return {deleted, prunedAccounting: pruned.deleted};\n }\n return {deleted, prunedAccounting: false};\n });\n}\n\n/**\n * Deletes expired closed streams and prunes accounting for emptied jobs.\n *\n * The loop self-bounds because a Temporal `startToCloseTimeout` marks the activity failed but\n * does not stop already-running JS. Objects are deleted before rows so a cleanup failure leaves\n * the row discoverable for the next sweep; the row delete stays guarded on the observed\n * `object_key`, so a racing compaction publish is re-read before the row is removed.\n */\nexport async function runRetentionSweep(\n params: RunRetentionSweepParams,\n): Promise<RetentionSweepResult> {\n const now = params.now ?? Date.now;\n const deadline = now() + params.timeBudgetMs;\n const result: RetentionSweepResult = {\n deleted: 0,\n raced: 0,\n failed: 0,\n accountingPruned: 0,\n iterations: 0,\n timedOut: false,\n };\n\n // Failed or raced rows retry next run; skipping them here keeps the rest of the backlog moving.\n const skip = new Set<string>();\n while (result.iterations < params.maxIterations) {\n if (now() >= deadline) {\n result.timedOut = true;\n break;\n }\n\n const batch = await listExpiredClosedStreams({\n retentionDays: params.retentionDays,\n limit: params.batchLimit,\n excludeIds: skip.size > 0 ? [...skip] : undefined,\n });\n if (batch.length === 0) break;\n\n let timedOutMidBatch = false;\n for (const stream of batch) {\n // Per-stream checks keep long batches within the heartbeat and wall-clock budgets.\n params.onProgress?.();\n if (now() >= deadline) {\n result.timedOut = true;\n timedOutMidBatch = true;\n break;\n }\n\n try {\n await deleteExpiredStreamObjects(stream);\n } catch (error) {\n result.failed += 1;\n skip.add(stream.id);\n logger().error(\n {err: error, streamId: stream.id},\n 'Failed to delete expired log stream objects',\n );\n continue;\n }\n\n let outcome: {deleted: boolean; prunedAccounting: boolean};\n try {\n outcome = await deleteExpiredStreamRow({stream, retentionDays: params.retentionDays});\n } catch (error) {\n result.failed += 1;\n skip.add(stream.id);\n logger().error(\n {err: error, streamId: stream.id},\n 'Failed to delete expired log stream row',\n );\n continue;\n }\n\n if (!outcome.deleted) {\n let current: AttemptStream | null;\n try {\n current = await getAttemptStreamById(stream.id);\n } catch (error) {\n result.failed += 1;\n skip.add(stream.id);\n logger().error(\n {err: error, streamId: stream.id},\n 'Failed to reload raced expired log stream',\n );\n continue;\n }\n if (current) {\n try {\n await deleteExpiredStreamObjects(current);\n outcome = await deleteExpiredStreamRow({\n stream: current,\n retentionDays: params.retentionDays,\n });\n } catch (error) {\n result.failed += 1;\n skip.add(stream.id);\n logger().error(\n {err: error, streamId: stream.id},\n 'Failed to delete raced expired log stream',\n );\n continue;\n }\n }\n if (!current || !outcome.deleted) {\n // `object_key` changed again or the row disappeared; the next sweep will re-read it.\n result.raced += 1;\n skip.add(stream.id);\n continue;\n }\n }\n\n result.deleted += 1;\n if (outcome.prunedAccounting) result.accountingPruned += 1;\n }\n\n if (timedOutMidBatch) break;\n\n result.iterations += 1;\n if (batch.length < params.batchLimit) break;\n }\n\n return result;\n}\n"],"names":["logger","deleteObjectsByPrefix","config","logObjectKey","deleteJobAccounting","db","accountingHasNoStreams","deleteExpiredStream","getAttemptStreamById","listExpiredClosedStreams","deleteExpiredStreamObjects","stream","LOG_STORAGE_S3_PREFIX","deleteExpiredStreamRow","params","transaction","tx","deleted","jobId","streamId","id","observedObjectKey","objectKey","pruned","retentionDays","prunedAccounting","runRetentionSweep","now","Date","deadline","timeBudgetMs","result","raced","failed","accountingPruned","iterations","timedOut","skip","Set","maxIterations","batch","limit","batchLimit","excludeIds","size","undefined","length","timedOutMidBatch","onProgress","error","add","err","outcome","current"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/core/retention.ts"],"sourcesContent":["import {reportError} from '@shipfox/node-error-monitoring';\nimport {logger} from '@shipfox/node-opentelemetry';\nimport {deleteObjectsByPrefix} from '#api/object-storage.js';\nimport {config} from '#config.js';\nimport type {AttemptStream} from '#core/entities/attempt-stream.js';\nimport {logObjectKey} from '#core/entities/log-object.js';\nimport {deleteJobAccounting} from '#db/accounting.js';\nimport {db} from '#db/db.js';\nimport {\n accountingHasNoStreams,\n deleteExpiredStream,\n getAttemptStreamById,\n listExpiredClosedStreams,\n} from '#db/streams.js';\n\nexport interface RetentionSweepResult {\n /** Streams whose objects and row were deleted. */\n deleted: number;\n /** Streams skipped because compaction changed `object_key` after we read it (retried next run). */\n raced: number;\n /** Streams whose object cleanup or guarded row delete threw; logged, skipped, and retried next run. */\n failed: number;\n accountingPruned: number;\n iterations: number;\n /** True when the sweep stopped on its wall-clock budget with backlog likely remaining. */\n timedOut: boolean;\n}\n\nexport interface RunRetentionSweepParams {\n retentionDays: number;\n batchLimit: number;\n timeBudgetMs: number;\n maxIterations: number;\n /** Wall clock; injectable so tests can drive the time budget deterministically. */\n now?: () => number;\n /** Liveness signal (e.g. the activity heartbeat); invoked once per processed stream. */\n onProgress?: () => void;\n}\n\nfunction deleteExpiredStreamObjects(stream: AttemptStream): Promise<void> {\n return deleteObjectsByPrefix(`${logObjectKey(config.LOG_STORAGE_S3_PREFIX, stream)}/`);\n}\n\nfunction deleteExpiredStreamRow(params: {\n stream: AttemptStream;\n retentionDays: number;\n}): Promise<{deleted: boolean; prunedAccounting: boolean}> {\n return db().transaction(async (tx) => {\n const {deleted, jobId} = await deleteExpiredStream(tx, {\n streamId: params.stream.id,\n observedObjectKey: params.stream.objectKey,\n });\n if (deleted && jobId && (await accountingHasNoStreams(tx, jobId))) {\n const pruned = await deleteJobAccounting(tx, {\n jobId,\n retentionDays: params.retentionDays,\n });\n return {deleted, prunedAccounting: pruned.deleted};\n }\n return {deleted, prunedAccounting: false};\n });\n}\n\n/**\n * Deletes expired closed streams and prunes accounting for emptied jobs.\n *\n * The loop self-bounds because a Temporal `startToCloseTimeout` marks the activity failed but\n * does not stop already-running JS. Objects are deleted before rows so a cleanup failure leaves\n * the row discoverable for the next sweep; the row delete stays guarded on the observed\n * `object_key`, so a racing compaction publish is re-read before the row is removed.\n */\nexport async function runRetentionSweep(\n params: RunRetentionSweepParams,\n): Promise<RetentionSweepResult> {\n const now = params.now ?? Date.now;\n const deadline = now() + params.timeBudgetMs;\n const result: RetentionSweepResult = {\n deleted: 0,\n raced: 0,\n failed: 0,\n accountingPruned: 0,\n iterations: 0,\n timedOut: false,\n };\n\n // Failed or raced rows retry next run; skipping them here keeps the rest of the backlog moving.\n const skip = new Set<string>();\n while (result.iterations < params.maxIterations) {\n if (now() >= deadline) {\n result.timedOut = true;\n break;\n }\n\n const batch = await listExpiredClosedStreams({\n retentionDays: params.retentionDays,\n limit: params.batchLimit,\n excludeIds: skip.size > 0 ? [...skip] : undefined,\n });\n if (batch.length === 0) break;\n\n let timedOutMidBatch = false;\n for (const stream of batch) {\n // Per-stream checks keep long batches within the heartbeat and wall-clock budgets.\n params.onProgress?.();\n if (now() >= deadline) {\n result.timedOut = true;\n timedOutMidBatch = true;\n break;\n }\n\n try {\n await deleteExpiredStreamObjects(stream);\n } catch (error) {\n result.failed += 1;\n skip.add(stream.id);\n logger().error(\n {err: error, streamId: stream.id},\n 'Failed to delete expired log stream objects',\n );\n reportError(error, {boundary: 'logs.maintenance', extra: {streamId: stream.id}});\n continue;\n }\n\n let outcome: {deleted: boolean; prunedAccounting: boolean};\n try {\n outcome = await deleteExpiredStreamRow({stream, retentionDays: params.retentionDays});\n } catch (error) {\n result.failed += 1;\n skip.add(stream.id);\n logger().error(\n {err: error, streamId: stream.id},\n 'Failed to delete expired log stream row',\n );\n reportError(error, {boundary: 'logs.maintenance', extra: {streamId: stream.id}});\n continue;\n }\n\n if (!outcome.deleted) {\n let current: AttemptStream | null;\n try {\n current = await getAttemptStreamById(stream.id);\n } catch (error) {\n result.failed += 1;\n skip.add(stream.id);\n logger().error(\n {err: error, streamId: stream.id},\n 'Failed to reload raced expired log stream',\n );\n reportError(error, {boundary: 'logs.maintenance', extra: {streamId: stream.id}});\n continue;\n }\n if (current) {\n try {\n await deleteExpiredStreamObjects(current);\n outcome = await deleteExpiredStreamRow({\n stream: current,\n retentionDays: params.retentionDays,\n });\n } catch (error) {\n result.failed += 1;\n skip.add(stream.id);\n logger().error(\n {err: error, streamId: stream.id},\n 'Failed to delete raced expired log stream',\n );\n reportError(error, {boundary: 'logs.maintenance', extra: {streamId: stream.id}});\n continue;\n }\n }\n if (!current || !outcome.deleted) {\n // `object_key` changed again or the row disappeared; the next sweep will re-read it.\n result.raced += 1;\n skip.add(stream.id);\n continue;\n }\n }\n\n result.deleted += 1;\n if (outcome.prunedAccounting) result.accountingPruned += 1;\n }\n\n if (timedOutMidBatch) break;\n\n result.iterations += 1;\n if (batch.length < params.batchLimit) break;\n }\n\n return result;\n}\n"],"names":["reportError","logger","deleteObjectsByPrefix","config","logObjectKey","deleteJobAccounting","db","accountingHasNoStreams","deleteExpiredStream","getAttemptStreamById","listExpiredClosedStreams","deleteExpiredStreamObjects","stream","LOG_STORAGE_S3_PREFIX","deleteExpiredStreamRow","params","transaction","tx","deleted","jobId","streamId","id","observedObjectKey","objectKey","pruned","retentionDays","prunedAccounting","runRetentionSweep","now","Date","deadline","timeBudgetMs","result","raced","failed","accountingPruned","iterations","timedOut","skip","Set","maxIterations","batch","limit","batchLimit","excludeIds","size","undefined","length","timedOutMidBatch","onProgress","error","add","err","boundary","extra","outcome","current"],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,4 +1,8 @@
1
+ import type { WorkflowsModuleClient } from '@shipfox/api-workflows-dto/inter-module';
1
2
  import { type ShipfoxModule } from '@shipfox/node-module';
2
3
  export { checkBucketReachable } from '#api/object-storage.js';
3
- export declare const logsModule: ShipfoxModule;
4
+ export declare function createLogsModule({ workflows, jobLeaseTokenTtlSeconds, }: {
5
+ workflows: WorkflowsModuleClient;
6
+ jobLeaseTokenTtlSeconds: number;
7
+ }): ShipfoxModule;
4
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAQA,OAAO,EAAC,KAAK,aAAa,EAAoB,MAAM,sBAAsB,CAAC;AAY3E,OAAO,EAAC,oBAAoB,EAAC,MAAM,wBAAwB,CAAC;AAO5D,eAAO,MAAM,UAAU,EAAE,aAiDxB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,yCAAyC,CAAC;AACnF,OAAO,EAAC,KAAK,aAAa,EAAoB,MAAM,sBAAsB,CAAC;AAa3E,OAAO,EAAC,oBAAoB,EAAC,MAAM,wBAAwB,CAAC;AAO5D,wBAAgB,gBAAgB,CAAC,EAC/B,SAAS,EACT,uBAAuB,GACxB,EAAE;IACD,SAAS,EAAE,qBAAqB,CAAC;IACjC,uBAAuB,EAAE,MAAM,CAAC;CACjC,GAAG,aAAa,CAqDhB"}
package/dist/index.js CHANGED
@@ -3,11 +3,12 @@ import { fileURLToPath } from 'node:url';
3
3
  import { LOG_STREAM_CLOSED, logsEventSchemas } from '@shipfox/api-logs-dto';
4
4
  import { WORKFLOWS_JOB_TERMINATED, WORKFLOWS_STEP_ATTEMPT_TERMINATED } from '@shipfox/api-workflows-dto';
5
5
  import { subscriberFactory } from '@shipfox/node-module';
6
+ import { validateLogStreamReapAfterSeconds } from '#config.js';
6
7
  import { db } from '#db/db.js';
7
8
  import { migrationsPath } from '#db/index.js';
8
9
  import { logsOutbox } from '#db/schema/outbox.js';
9
10
  import { registerLogsServiceMetrics } from '#metrics/service.js';
10
- import { logsRoutes } from '#presentation/routes/index.js';
11
+ import { createLogsRoutes } from '#presentation/routes/index.js';
11
12
  import { onJobTerminated } from '#presentation/subscribers/on-job-terminated.js';
12
13
  import { onLogStreamClosed } from '#presentation/subscribers/on-log-stream-closed.js';
13
14
  import { onStepAttemptTerminated } from '#presentation/subscribers/on-step-attempt-terminated.js';
@@ -17,65 +18,68 @@ export { checkBucketReachable } from '#api/object-storage.js';
17
18
  const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
18
19
  const workflowsPath = resolve(packageRoot, 'dist/temporal/workflows/index.js');
19
20
  const subscriber = subscriberFactory();
20
- export const logsModule = {
21
- name: 'logs',
22
- database: {
23
- db,
24
- migrationsPath
25
- },
26
- routes: logsRoutes,
27
- metrics: registerLogsServiceMetrics,
28
- // `logs.stream.closed` is written by the close paths: the job-terminated subscriber
29
- // force-closes streams the runner never ended, and the closed event drives compaction.
30
- publishers: [
31
- {
32
- name: 'logs',
33
- table: logsOutbox,
21
+ export function createLogsModule({ workflows, jobLeaseTokenTtlSeconds }) {
22
+ validateLogStreamReapAfterSeconds(jobLeaseTokenTtlSeconds);
23
+ return {
24
+ name: 'logs',
25
+ database: {
34
26
  db,
35
- eventSchemas: logsEventSchemas
36
- }
37
- ],
38
- subscribers: [
39
- subscriber(WORKFLOWS_STEP_ATTEMPT_TERMINATED, onStepAttemptTerminated),
40
- subscriber(WORKFLOWS_JOB_TERMINATED, onJobTerminated),
41
- subscriber(LOG_STREAM_CLOSED, onLogStreamClosed)
42
- ],
43
- workers: [
44
- {
45
- taskQueue: LOGS_LIFECYCLE_TASK_QUEUE,
46
- workflowsPath,
47
- activities: createLogsActivities,
48
- // Temporal cron schedules are fixed after creation, so making cadence configurable would
49
- // look adjustable while keeping the old schedule. Tune LOG_RETENTION_DAYS for the horizon.
50
- workflows: [
51
- {
52
- name: 'retentionSweepCron',
53
- id: 'logs-retention-sweep',
54
- cronSchedule: '0 * * * *'
55
- },
56
- // Offset from retention's top-of-hour sweep; stale-stream age is governed by
57
- // LOG_STREAM_REAP_AFTER_SECONDS.
58
- {
59
- name: 'reapStaleOpenStreamsCron',
60
- id: 'logs-reap-stale-open-streams',
61
- cronSchedule: '5,15,25,35,45,55 * * * *'
62
- }
63
- ]
27
+ migrationsPath
64
28
  },
65
- // Compaction runs on its own queue so long uploads cannot starve the lifecycle sweep.
66
- {
67
- taskQueue: LOGS_COMPACTION_TASK_QUEUE,
68
- workflowsPath,
69
- activities: createLogsActivities,
70
- workflows: [
71
- {
72
- name: 'compactionReconcileCron',
73
- id: 'logs-compaction-reconcile',
74
- cronSchedule: '*/10 * * * *'
75
- }
76
- ]
77
- }
78
- ]
79
- };
29
+ routes: createLogsRoutes(workflows),
30
+ metrics: registerLogsServiceMetrics,
31
+ // `logs.stream.closed` is written by the close paths: the job-terminated subscriber
32
+ // force-closes streams the runner never ended, and the closed event drives compaction.
33
+ publishers: [
34
+ {
35
+ name: 'logs',
36
+ table: logsOutbox,
37
+ db,
38
+ eventSchemas: logsEventSchemas
39
+ }
40
+ ],
41
+ subscribers: [
42
+ subscriber(WORKFLOWS_STEP_ATTEMPT_TERMINATED, onStepAttemptTerminated),
43
+ subscriber(WORKFLOWS_JOB_TERMINATED, onJobTerminated),
44
+ subscriber(LOG_STREAM_CLOSED, onLogStreamClosed)
45
+ ],
46
+ workers: [
47
+ {
48
+ taskQueue: LOGS_LIFECYCLE_TASK_QUEUE,
49
+ workflowsPath,
50
+ activities: createLogsActivities,
51
+ // Temporal cron schedules are fixed after creation, so making cadence configurable would
52
+ // look adjustable while keeping the old schedule. Tune LOG_RETENTION_DAYS for the horizon.
53
+ workflows: [
54
+ {
55
+ name: 'retentionSweepCron',
56
+ id: 'logs-retention-sweep',
57
+ cronSchedule: '0 * * * *'
58
+ },
59
+ // Offset from retention's top-of-hour sweep; stale-stream age is governed by
60
+ // LOG_STREAM_REAP_AFTER_SECONDS.
61
+ {
62
+ name: 'reapStaleOpenStreamsCron',
63
+ id: 'logs-reap-stale-open-streams',
64
+ cronSchedule: '5,15,25,35,45,55 * * * *'
65
+ }
66
+ ]
67
+ },
68
+ // Compaction runs on its own queue so long uploads cannot starve the lifecycle sweep.
69
+ {
70
+ taskQueue: LOGS_COMPACTION_TASK_QUEUE,
71
+ workflowsPath,
72
+ activities: createLogsActivities,
73
+ workflows: [
74
+ {
75
+ name: 'compactionReconcileCron',
76
+ id: 'logs-compaction-reconcile',
77
+ cronSchedule: '*/10 * * * *'
78
+ }
79
+ ]
80
+ }
81
+ ]
82
+ };
83
+ }
80
84
 
81
85
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import {dirname, resolve} from 'node:path';\nimport {fileURLToPath} from 'node:url';\nimport {LOG_STREAM_CLOSED, type LogsEventMap, logsEventSchemas} from '@shipfox/api-logs-dto';\nimport {\n WORKFLOWS_JOB_TERMINATED,\n WORKFLOWS_STEP_ATTEMPT_TERMINATED,\n type WorkflowsEventMapDto,\n} from '@shipfox/api-workflows-dto';\nimport {type ShipfoxModule, subscriberFactory} from '@shipfox/node-module';\nimport {db} from '#db/db.js';\nimport {migrationsPath} from '#db/index.js';\nimport {logsOutbox} from '#db/schema/outbox.js';\nimport {registerLogsServiceMetrics} from '#metrics/service.js';\nimport {logsRoutes} from '#presentation/routes/index.js';\nimport {onJobTerminated} from '#presentation/subscribers/on-job-terminated.js';\nimport {onLogStreamClosed} from '#presentation/subscribers/on-log-stream-closed.js';\nimport {onStepAttemptTerminated} from '#presentation/subscribers/on-step-attempt-terminated.js';\nimport {createLogsActivities} from '#temporal/activities/index.js';\nimport {LOGS_COMPACTION_TASK_QUEUE, LOGS_LIFECYCLE_TASK_QUEUE} from '#temporal/constants.js';\n\nexport {checkBucketReachable} from '#api/object-storage.js';\n\nconst packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');\nconst workflowsPath = resolve(packageRoot, 'dist/temporal/workflows/index.js');\n\nconst subscriber = subscriberFactory<WorkflowsEventMapDto & LogsEventMap>();\n\nexport const logsModule: ShipfoxModule = {\n name: 'logs',\n database: {db, migrationsPath},\n routes: logsRoutes,\n metrics: registerLogsServiceMetrics,\n // `logs.stream.closed` is written by the close paths: the job-terminated subscriber\n // force-closes streams the runner never ended, and the closed event drives compaction.\n publishers: [{name: 'logs', table: logsOutbox, db, eventSchemas: logsEventSchemas}],\n subscribers: [\n subscriber(WORKFLOWS_STEP_ATTEMPT_TERMINATED, onStepAttemptTerminated),\n subscriber(WORKFLOWS_JOB_TERMINATED, onJobTerminated),\n subscriber(LOG_STREAM_CLOSED, onLogStreamClosed),\n ],\n workers: [\n {\n taskQueue: LOGS_LIFECYCLE_TASK_QUEUE,\n workflowsPath,\n activities: createLogsActivities,\n // Temporal cron schedules are fixed after creation, so making cadence configurable would\n // look adjustable while keeping the old schedule. Tune LOG_RETENTION_DAYS for the horizon.\n workflows: [\n {\n name: 'retentionSweepCron',\n id: 'logs-retention-sweep',\n cronSchedule: '0 * * * *',\n },\n // Offset from retention's top-of-hour sweep; stale-stream age is governed by\n // LOG_STREAM_REAP_AFTER_SECONDS.\n {\n name: 'reapStaleOpenStreamsCron',\n id: 'logs-reap-stale-open-streams',\n cronSchedule: '5,15,25,35,45,55 * * * *',\n },\n ],\n },\n // Compaction runs on its own queue so long uploads cannot starve the lifecycle sweep.\n {\n taskQueue: LOGS_COMPACTION_TASK_QUEUE,\n workflowsPath,\n activities: createLogsActivities,\n workflows: [\n {\n name: 'compactionReconcileCron',\n id: 'logs-compaction-reconcile',\n cronSchedule: '*/10 * * * *',\n },\n ],\n },\n ],\n};\n"],"names":["dirname","resolve","fileURLToPath","LOG_STREAM_CLOSED","logsEventSchemas","WORKFLOWS_JOB_TERMINATED","WORKFLOWS_STEP_ATTEMPT_TERMINATED","subscriberFactory","db","migrationsPath","logsOutbox","registerLogsServiceMetrics","logsRoutes","onJobTerminated","onLogStreamClosed","onStepAttemptTerminated","createLogsActivities","LOGS_COMPACTION_TASK_QUEUE","LOGS_LIFECYCLE_TASK_QUEUE","checkBucketReachable","packageRoot","url","workflowsPath","subscriber","logsModule","name","database","routes","metrics","publishers","table","eventSchemas","subscribers","workers","taskQueue","activities","workflows","id","cronSchedule"],"mappings":"AAAA,SAAQA,OAAO,EAAEC,OAAO,QAAO,YAAY;AAC3C,SAAQC,aAAa,QAAO,WAAW;AACvC,SAAQC,iBAAiB,EAAqBC,gBAAgB,QAAO,wBAAwB;AAC7F,SACEC,wBAAwB,EACxBC,iCAAiC,QAE5B,6BAA6B;AACpC,SAA4BC,iBAAiB,QAAO,uBAAuB;AAC3E,SAAQC,EAAE,QAAO,YAAY;AAC7B,SAAQC,cAAc,QAAO,eAAe;AAC5C,SAAQC,UAAU,QAAO,uBAAuB;AAChD,SAAQC,0BAA0B,QAAO,sBAAsB;AAC/D,SAAQC,UAAU,QAAO,gCAAgC;AACzD,SAAQC,eAAe,QAAO,iDAAiD;AAC/E,SAAQC,iBAAiB,QAAO,oDAAoD;AACpF,SAAQC,uBAAuB,QAAO,0DAA0D;AAChG,SAAQC,oBAAoB,QAAO,gCAAgC;AACnE,SAAQC,0BAA0B,EAAEC,yBAAyB,QAAO,yBAAyB;AAE7F,SAAQC,oBAAoB,QAAO,yBAAyB;AAE5D,MAAMC,cAAcnB,QAAQD,QAAQE,cAAc,YAAYmB,GAAG,IAAI;AACrE,MAAMC,gBAAgBrB,QAAQmB,aAAa;AAE3C,MAAMG,aAAahB;AAEnB,OAAO,MAAMiB,aAA4B;IACvCC,MAAM;IACNC,UAAU;QAAClB;QAAIC;IAAc;IAC7BkB,QAAQf;IACRgB,SAASjB;IACT,oFAAoF;IACpF,uFAAuF;IACvFkB,YAAY;QAAC;YAACJ,MAAM;YAAQK,OAAOpB;YAAYF;YAAIuB,cAAc3B;QAAgB;KAAE;IACnF4B,aAAa;QACXT,WAAWjB,mCAAmCS;QAC9CQ,WAAWlB,0BAA0BQ;QACrCU,WAAWpB,mBAAmBW;KAC/B;IACDmB,SAAS;QACP;YACEC,WAAWhB;YACXI;YACAa,YAAYnB;YACZ,yFAAyF;YACzF,2FAA2F;YAC3FoB,WAAW;gBACT;oBACEX,MAAM;oBACNY,IAAI;oBACJC,cAAc;gBAChB;gBACA,6EAA6E;gBAC7E,iCAAiC;gBACjC;oBACEb,MAAM;oBACNY,IAAI;oBACJC,cAAc;gBAChB;aACD;QACH;QACA,sFAAsF;QACtF;YACEJ,WAAWjB;YACXK;YACAa,YAAYnB;YACZoB,WAAW;gBACT;oBACEX,MAAM;oBACNY,IAAI;oBACJC,cAAc;gBAChB;aACD;QACH;KACD;AACH,EAAE"}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import {dirname, resolve} from 'node:path';\nimport {fileURLToPath} from 'node:url';\nimport {LOG_STREAM_CLOSED, type LogsEventMap, logsEventSchemas} from '@shipfox/api-logs-dto';\nimport {\n WORKFLOWS_JOB_TERMINATED,\n WORKFLOWS_STEP_ATTEMPT_TERMINATED,\n type WorkflowsEventMapDto,\n} from '@shipfox/api-workflows-dto';\nimport type {WorkflowsModuleClient} from '@shipfox/api-workflows-dto/inter-module';\nimport {type ShipfoxModule, subscriberFactory} from '@shipfox/node-module';\nimport {validateLogStreamReapAfterSeconds} from '#config.js';\nimport {db} from '#db/db.js';\nimport {migrationsPath} from '#db/index.js';\nimport {logsOutbox} from '#db/schema/outbox.js';\nimport {registerLogsServiceMetrics} from '#metrics/service.js';\nimport {createLogsRoutes} from '#presentation/routes/index.js';\nimport {onJobTerminated} from '#presentation/subscribers/on-job-terminated.js';\nimport {onLogStreamClosed} from '#presentation/subscribers/on-log-stream-closed.js';\nimport {onStepAttemptTerminated} from '#presentation/subscribers/on-step-attempt-terminated.js';\nimport {createLogsActivities} from '#temporal/activities/index.js';\nimport {LOGS_COMPACTION_TASK_QUEUE, LOGS_LIFECYCLE_TASK_QUEUE} from '#temporal/constants.js';\n\nexport {checkBucketReachable} from '#api/object-storage.js';\n\nconst packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');\nconst workflowsPath = resolve(packageRoot, 'dist/temporal/workflows/index.js');\n\nconst subscriber = subscriberFactory<WorkflowsEventMapDto & LogsEventMap>();\n\nexport function createLogsModule({\n workflows,\n jobLeaseTokenTtlSeconds,\n}: {\n workflows: WorkflowsModuleClient;\n jobLeaseTokenTtlSeconds: number;\n}): ShipfoxModule {\n validateLogStreamReapAfterSeconds(jobLeaseTokenTtlSeconds);\n\n return {\n name: 'logs',\n database: {db, migrationsPath},\n routes: createLogsRoutes(workflows),\n metrics: registerLogsServiceMetrics,\n // `logs.stream.closed` is written by the close paths: the job-terminated subscriber\n // force-closes streams the runner never ended, and the closed event drives compaction.\n publishers: [{name: 'logs', table: logsOutbox, db, eventSchemas: logsEventSchemas}],\n subscribers: [\n subscriber(WORKFLOWS_STEP_ATTEMPT_TERMINATED, onStepAttemptTerminated),\n subscriber(WORKFLOWS_JOB_TERMINATED, onJobTerminated),\n subscriber(LOG_STREAM_CLOSED, onLogStreamClosed),\n ],\n workers: [\n {\n taskQueue: LOGS_LIFECYCLE_TASK_QUEUE,\n workflowsPath,\n activities: createLogsActivities,\n // Temporal cron schedules are fixed after creation, so making cadence configurable would\n // look adjustable while keeping the old schedule. Tune LOG_RETENTION_DAYS for the horizon.\n workflows: [\n {\n name: 'retentionSweepCron',\n id: 'logs-retention-sweep',\n cronSchedule: '0 * * * *',\n },\n // Offset from retention's top-of-hour sweep; stale-stream age is governed by\n // LOG_STREAM_REAP_AFTER_SECONDS.\n {\n name: 'reapStaleOpenStreamsCron',\n id: 'logs-reap-stale-open-streams',\n cronSchedule: '5,15,25,35,45,55 * * * *',\n },\n ],\n },\n // Compaction runs on its own queue so long uploads cannot starve the lifecycle sweep.\n {\n taskQueue: LOGS_COMPACTION_TASK_QUEUE,\n workflowsPath,\n activities: createLogsActivities,\n workflows: [\n {\n name: 'compactionReconcileCron',\n id: 'logs-compaction-reconcile',\n cronSchedule: '*/10 * * * *',\n },\n ],\n },\n ],\n };\n}\n"],"names":["dirname","resolve","fileURLToPath","LOG_STREAM_CLOSED","logsEventSchemas","WORKFLOWS_JOB_TERMINATED","WORKFLOWS_STEP_ATTEMPT_TERMINATED","subscriberFactory","validateLogStreamReapAfterSeconds","db","migrationsPath","logsOutbox","registerLogsServiceMetrics","createLogsRoutes","onJobTerminated","onLogStreamClosed","onStepAttemptTerminated","createLogsActivities","LOGS_COMPACTION_TASK_QUEUE","LOGS_LIFECYCLE_TASK_QUEUE","checkBucketReachable","packageRoot","url","workflowsPath","subscriber","createLogsModule","workflows","jobLeaseTokenTtlSeconds","name","database","routes","metrics","publishers","table","eventSchemas","subscribers","workers","taskQueue","activities","id","cronSchedule"],"mappings":"AAAA,SAAQA,OAAO,EAAEC,OAAO,QAAO,YAAY;AAC3C,SAAQC,aAAa,QAAO,WAAW;AACvC,SAAQC,iBAAiB,EAAqBC,gBAAgB,QAAO,wBAAwB;AAC7F,SACEC,wBAAwB,EACxBC,iCAAiC,QAE5B,6BAA6B;AAEpC,SAA4BC,iBAAiB,QAAO,uBAAuB;AAC3E,SAAQC,iCAAiC,QAAO,aAAa;AAC7D,SAAQC,EAAE,QAAO,YAAY;AAC7B,SAAQC,cAAc,QAAO,eAAe;AAC5C,SAAQC,UAAU,QAAO,uBAAuB;AAChD,SAAQC,0BAA0B,QAAO,sBAAsB;AAC/D,SAAQC,gBAAgB,QAAO,gCAAgC;AAC/D,SAAQC,eAAe,QAAO,iDAAiD;AAC/E,SAAQC,iBAAiB,QAAO,oDAAoD;AACpF,SAAQC,uBAAuB,QAAO,0DAA0D;AAChG,SAAQC,oBAAoB,QAAO,gCAAgC;AACnE,SAAQC,0BAA0B,EAAEC,yBAAyB,QAAO,yBAAyB;AAE7F,SAAQC,oBAAoB,QAAO,yBAAyB;AAE5D,MAAMC,cAAcpB,QAAQD,QAAQE,cAAc,YAAYoB,GAAG,IAAI;AACrE,MAAMC,gBAAgBtB,QAAQoB,aAAa;AAE3C,MAAMG,aAAajB;AAEnB,OAAO,SAASkB,iBAAiB,EAC/BC,SAAS,EACTC,uBAAuB,EAIxB;IACCnB,kCAAkCmB;IAElC,OAAO;QACLC,MAAM;QACNC,UAAU;YAACpB;YAAIC;QAAc;QAC7BoB,QAAQjB,iBAAiBa;QACzBK,SAASnB;QACT,oFAAoF;QACpF,uFAAuF;QACvFoB,YAAY;YAAC;gBAACJ,MAAM;gBAAQK,OAAOtB;gBAAYF;gBAAIyB,cAAc9B;YAAgB;SAAE;QACnF+B,aAAa;YACXX,WAAWlB,mCAAmCU;YAC9CQ,WAAWnB,0BAA0BS;YACrCU,WAAWrB,mBAAmBY;SAC/B;QACDqB,SAAS;YACP;gBACEC,WAAWlB;gBACXI;gBACAe,YAAYrB;gBACZ,yFAAyF;gBACzF,2FAA2F;gBAC3FS,WAAW;oBACT;wBACEE,MAAM;wBACNW,IAAI;wBACJC,cAAc;oBAChB;oBACA,6EAA6E;oBAC7E,iCAAiC;oBACjC;wBACEZ,MAAM;wBACNW,IAAI;wBACJC,cAAc;oBAChB;iBACD;YACH;YACA,sFAAsF;YACtF;gBACEH,WAAWnB;gBACXK;gBACAe,YAAYrB;gBACZS,WAAW;oBACT;wBACEE,MAAM;wBACNW,IAAI;wBACJC,cAAc;oBAChB;iBACD;YACH;SACD;IACH;AACF"}
@@ -1,2 +1,3 @@
1
- export declare const appendLogsRoute: import("@shipfox/node-fastify").RouteDefinition;
1
+ import type { WorkflowsModuleClient } from '@shipfox/api-workflows-dto/inter-module';
2
+ export declare function createAppendLogsRoute(workflows: WorkflowsModuleClient): import("@shipfox/node-fastify").RouteDefinition;
2
3
  //# sourceMappingURL=append-logs.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"append-logs.d.ts","sourceRoot":"","sources":["../../../src/presentation/routes/append-logs.ts"],"names":[],"mappings":"AAYA,eAAO,MAAM,eAAe,iDAqD1B,CAAC"}
1
+ {"version":3,"file":"append-logs.d.ts","sourceRoot":"","sources":["../../../src/presentation/routes/append-logs.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,yCAAyC,CAAC;AAMnF,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,qBAAqB,mDAwDrE"}
@@ -5,67 +5,67 @@ import { ClientError, defineRoute } from '@shipfox/node-fastify';
5
5
  import { z } from 'zod';
6
6
  import { appendLogs } from '#core/append-logs.js';
7
7
  import { LeaseStreamMismatchError, MalformedLogChunkError, OffsetGapError } from '#core/errors.js';
8
- export const appendLogsRoute = defineRoute({
9
- method: 'POST',
10
- path: '/steps/:stepId/logs',
11
- description: 'Append a chunk of logs for a step attempt of the leased job.',
12
- schema: {
13
- params: z.object({
14
- stepId: z.string().uuid()
15
- }),
16
- querystring: appendLogsQuerySchema,
17
- response: {
18
- 200: appendLogsResponseSchema,
19
- 409: offsetGapResponseSchema
8
+ export function createAppendLogsRoute(workflows) {
9
+ return defineRoute({
10
+ method: 'POST',
11
+ path: '/steps/:stepId/logs',
12
+ description: 'Append a chunk of logs for a step attempt of the leased job.',
13
+ schema: {
14
+ params: z.object({
15
+ stepId: z.string().uuid()
16
+ }),
17
+ querystring: appendLogsQuerySchema,
18
+ response: {
19
+ 200: appendLogsResponseSchema,
20
+ 409: offsetGapResponseSchema
21
+ }
22
+ },
23
+ errorHandler: (error)=>{
24
+ if (error instanceof OffsetGapError) {
25
+ throw new ClientError('Append offset is ahead of the committed length', 'offset-gap', {
26
+ status: 409,
27
+ details: {
28
+ committed_length: error.committedLength
29
+ }
30
+ });
31
+ }
32
+ if (error instanceof MalformedLogChunkError) {
33
+ throw new ClientError(error.message, 'malformed-log-chunk', {
34
+ status: 400
35
+ });
36
+ }
37
+ if (error instanceof LeaseStreamMismatchError) {
38
+ throw new ClientError(error.message, 'lease-stream-mismatch', {
39
+ status: 403
40
+ });
41
+ }
42
+ throw error;
43
+ },
44
+ handler: async (request)=>{
45
+ const leasedJob = requireLeasedJobContext(request);
46
+ const { stepId } = request.params;
47
+ const { attempt, offset } = request.query;
48
+ if (leasedJob.currentStepId !== stepId || leasedJob.currentStepAttempt !== attempt) {
49
+ throw new ClientError('Step not found for leased job execution', 'step-not-found', {
50
+ status: 404
51
+ });
52
+ }
53
+ const result = await appendLogs({
54
+ jobId: leasedJob.jobId,
55
+ workspaceId: leasedJob.workspaceId,
56
+ projectId: leasedJob.projectId,
57
+ workflowRunAttemptId: leasedJob.workflowRunAttemptId,
58
+ stepId,
59
+ attempt,
60
+ offset,
61
+ body: request.body ?? Buffer.alloc(0)
62
+ }, workflows);
63
+ return {
64
+ committed_length: result.committedLength,
65
+ capped: result.capped
66
+ };
20
67
  }
21
- },
22
- errorHandler: (error)=>{
23
- if (error instanceof OffsetGapError) {
24
- throw new ClientError('Append offset is ahead of the committed length', 'offset-gap', {
25
- status: 409,
26
- details: {
27
- committed_length: error.committedLength
28
- }
29
- });
30
- }
31
- if (error instanceof MalformedLogChunkError) {
32
- throw new ClientError(error.message, 'malformed-log-chunk', {
33
- status: 400
34
- });
35
- }
36
- if (error instanceof LeaseStreamMismatchError) {
37
- throw new ClientError(error.message, 'lease-stream-mismatch', {
38
- status: 403
39
- });
40
- }
41
- throw error;
42
- },
43
- handler: async (request)=>{
44
- const leasedJob = requireLeasedJobContext(request);
45
- const { stepId } = request.params;
46
- const { attempt, offset } = request.query;
47
- // The scoped lease proves membership in the dispatched step attempt; active-step
48
- // completion remains governed by workflow/report state, not the log append route.
49
- if (leasedJob.currentStepId !== stepId || leasedJob.currentStepAttempt !== attempt) {
50
- throw new ClientError('Step not found for leased job execution', 'step-not-found', {
51
- status: 404
52
- });
53
- }
54
- const result = await appendLogs({
55
- jobId: leasedJob.jobId,
56
- workspaceId: leasedJob.workspaceId,
57
- projectId: leasedJob.projectId,
58
- workflowRunAttemptId: leasedJob.workflowRunAttemptId,
59
- stepId,
60
- attempt,
61
- offset,
62
- body: request.body ?? Buffer.alloc(0)
63
- });
64
- return {
65
- committed_length: result.committedLength,
66
- capped: result.capped
67
- };
68
- }
69
- });
68
+ });
69
+ }
70
70
 
71
71
  //# sourceMappingURL=append-logs.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/presentation/routes/append-logs.ts"],"sourcesContent":["import {Buffer} from 'node:buffer';\nimport {requireLeasedJobContext} from '@shipfox/api-auth-context';\nimport {\n appendLogsQuerySchema,\n appendLogsResponseSchema,\n offsetGapResponseSchema,\n} from '@shipfox/api-logs-dto';\nimport {ClientError, defineRoute} from '@shipfox/node-fastify';\nimport {z} from 'zod';\nimport {appendLogs} from '#core/append-logs.js';\nimport {LeaseStreamMismatchError, MalformedLogChunkError, OffsetGapError} from '#core/errors.js';\n\nexport const appendLogsRoute = defineRoute({\n method: 'POST',\n path: '/steps/:stepId/logs',\n description: 'Append a chunk of logs for a step attempt of the leased job.',\n schema: {\n params: z.object({stepId: z.string().uuid()}),\n querystring: appendLogsQuerySchema,\n response: {\n 200: appendLogsResponseSchema,\n 409: offsetGapResponseSchema,\n },\n },\n errorHandler: (error) => {\n if (error instanceof OffsetGapError) {\n throw new ClientError('Append offset is ahead of the committed length', 'offset-gap', {\n status: 409,\n details: {committed_length: error.committedLength},\n });\n }\n if (error instanceof MalformedLogChunkError) {\n throw new ClientError(error.message, 'malformed-log-chunk', {status: 400});\n }\n if (error instanceof LeaseStreamMismatchError) {\n throw new ClientError(error.message, 'lease-stream-mismatch', {status: 403});\n }\n throw error;\n },\n handler: async (request) => {\n const leasedJob = requireLeasedJobContext(request);\n const {stepId} = request.params;\n const {attempt, offset} = request.query;\n\n // The scoped lease proves membership in the dispatched step attempt; active-step\n // completion remains governed by workflow/report state, not the log append route.\n if (leasedJob.currentStepId !== stepId || leasedJob.currentStepAttempt !== attempt) {\n throw new ClientError('Step not found for leased job execution', 'step-not-found', {\n status: 404,\n });\n }\n\n const result = await appendLogs({\n jobId: leasedJob.jobId,\n workspaceId: leasedJob.workspaceId,\n projectId: leasedJob.projectId,\n workflowRunAttemptId: leasedJob.workflowRunAttemptId,\n stepId,\n attempt,\n offset,\n body: (request.body as Buffer | undefined) ?? Buffer.alloc(0),\n });\n\n return {committed_length: result.committedLength, capped: result.capped};\n },\n});\n"],"names":["Buffer","requireLeasedJobContext","appendLogsQuerySchema","appendLogsResponseSchema","offsetGapResponseSchema","ClientError","defineRoute","z","appendLogs","LeaseStreamMismatchError","MalformedLogChunkError","OffsetGapError","appendLogsRoute","method","path","description","schema","params","object","stepId","string","uuid","querystring","response","errorHandler","error","status","details","committed_length","committedLength","message","handler","request","leasedJob","attempt","offset","query","currentStepId","currentStepAttempt","result","jobId","workspaceId","projectId","workflowRunAttemptId","body","alloc","capped"],"mappings":"AAAA,SAAQA,MAAM,QAAO,cAAc;AACnC,SAAQC,uBAAuB,QAAO,4BAA4B;AAClE,SACEC,qBAAqB,EACrBC,wBAAwB,EACxBC,uBAAuB,QAClB,wBAAwB;AAC/B,SAAQC,WAAW,EAAEC,WAAW,QAAO,wBAAwB;AAC/D,SAAQC,CAAC,QAAO,MAAM;AACtB,SAAQC,UAAU,QAAO,uBAAuB;AAChD,SAAQC,wBAAwB,EAAEC,sBAAsB,EAAEC,cAAc,QAAO,kBAAkB;AAEjG,OAAO,MAAMC,kBAAkBN,YAAY;IACzCO,QAAQ;IACRC,MAAM;IACNC,aAAa;IACbC,QAAQ;QACNC,QAAQV,EAAEW,MAAM,CAAC;YAACC,QAAQZ,EAAEa,MAAM,GAAGC,IAAI;QAAE;QAC3CC,aAAapB;QACbqB,UAAU;YACR,KAAKpB;YACL,KAAKC;QACP;IACF;IACAoB,cAAc,CAACC;QACb,IAAIA,iBAAiBd,gBAAgB;YACnC,MAAM,IAAIN,YAAY,kDAAkD,cAAc;gBACpFqB,QAAQ;gBACRC,SAAS;oBAACC,kBAAkBH,MAAMI,eAAe;gBAAA;YACnD;QACF;QACA,IAAIJ,iBAAiBf,wBAAwB;YAC3C,MAAM,IAAIL,YAAYoB,MAAMK,OAAO,EAAE,uBAAuB;gBAACJ,QAAQ;YAAG;QAC1E;QACA,IAAID,iBAAiBhB,0BAA0B;YAC7C,MAAM,IAAIJ,YAAYoB,MAAMK,OAAO,EAAE,yBAAyB;gBAACJ,QAAQ;YAAG;QAC5E;QACA,MAAMD;IACR;IACAM,SAAS,OAAOC;QACd,MAAMC,YAAYhC,wBAAwB+B;QAC1C,MAAM,EAACb,MAAM,EAAC,GAAGa,QAAQf,MAAM;QAC/B,MAAM,EAACiB,OAAO,EAAEC,MAAM,EAAC,GAAGH,QAAQI,KAAK;QAEvC,iFAAiF;QACjF,kFAAkF;QAClF,IAAIH,UAAUI,aAAa,KAAKlB,UAAUc,UAAUK,kBAAkB,KAAKJ,SAAS;YAClF,MAAM,IAAI7B,YAAY,2CAA2C,kBAAkB;gBACjFqB,QAAQ;YACV;QACF;QAEA,MAAMa,SAAS,MAAM/B,WAAW;YAC9BgC,OAAOP,UAAUO,KAAK;YACtBC,aAAaR,UAAUQ,WAAW;YAClCC,WAAWT,UAAUS,SAAS;YAC9BC,sBAAsBV,UAAUU,oBAAoB;YACpDxB;YACAe;YACAC;YACAS,MAAM,AAACZ,QAAQY,IAAI,IAA2B5C,OAAO6C,KAAK,CAAC;QAC7D;QAEA,OAAO;YAACjB,kBAAkBW,OAAOV,eAAe;YAAEiB,QAAQP,OAAOO,MAAM;QAAA;IACzE;AACF,GAAG"}
1
+ {"version":3,"sources":["../../../src/presentation/routes/append-logs.ts"],"sourcesContent":["import {Buffer} from 'node:buffer';\nimport {requireLeasedJobContext} from '@shipfox/api-auth-context';\nimport {\n appendLogsQuerySchema,\n appendLogsResponseSchema,\n offsetGapResponseSchema,\n} from '@shipfox/api-logs-dto';\nimport type {WorkflowsModuleClient} from '@shipfox/api-workflows-dto/inter-module';\nimport {ClientError, defineRoute} from '@shipfox/node-fastify';\nimport {z} from 'zod';\nimport {appendLogs} from '#core/append-logs.js';\nimport {LeaseStreamMismatchError, MalformedLogChunkError, OffsetGapError} from '#core/errors.js';\n\nexport function createAppendLogsRoute(workflows: WorkflowsModuleClient) {\n return defineRoute({\n method: 'POST',\n path: '/steps/:stepId/logs',\n description: 'Append a chunk of logs for a step attempt of the leased job.',\n schema: {\n params: z.object({stepId: z.string().uuid()}),\n querystring: appendLogsQuerySchema,\n response: {\n 200: appendLogsResponseSchema,\n 409: offsetGapResponseSchema,\n },\n },\n errorHandler: (error) => {\n if (error instanceof OffsetGapError) {\n throw new ClientError('Append offset is ahead of the committed length', 'offset-gap', {\n status: 409,\n details: {committed_length: error.committedLength},\n });\n }\n if (error instanceof MalformedLogChunkError) {\n throw new ClientError(error.message, 'malformed-log-chunk', {status: 400});\n }\n if (error instanceof LeaseStreamMismatchError) {\n throw new ClientError(error.message, 'lease-stream-mismatch', {status: 403});\n }\n throw error;\n },\n handler: async (request) => {\n const leasedJob = requireLeasedJobContext(request);\n const {stepId} = request.params;\n const {attempt, offset} = request.query;\n\n if (leasedJob.currentStepId !== stepId || leasedJob.currentStepAttempt !== attempt) {\n throw new ClientError('Step not found for leased job execution', 'step-not-found', {\n status: 404,\n });\n }\n\n const result = await appendLogs(\n {\n jobId: leasedJob.jobId,\n workspaceId: leasedJob.workspaceId,\n projectId: leasedJob.projectId,\n workflowRunAttemptId: leasedJob.workflowRunAttemptId,\n stepId,\n attempt,\n offset,\n body: (request.body as Buffer | undefined) ?? Buffer.alloc(0),\n },\n workflows,\n );\n\n return {committed_length: result.committedLength, capped: result.capped};\n },\n });\n}\n"],"names":["Buffer","requireLeasedJobContext","appendLogsQuerySchema","appendLogsResponseSchema","offsetGapResponseSchema","ClientError","defineRoute","z","appendLogs","LeaseStreamMismatchError","MalformedLogChunkError","OffsetGapError","createAppendLogsRoute","workflows","method","path","description","schema","params","object","stepId","string","uuid","querystring","response","errorHandler","error","status","details","committed_length","committedLength","message","handler","request","leasedJob","attempt","offset","query","currentStepId","currentStepAttempt","result","jobId","workspaceId","projectId","workflowRunAttemptId","body","alloc","capped"],"mappings":"AAAA,SAAQA,MAAM,QAAO,cAAc;AACnC,SAAQC,uBAAuB,QAAO,4BAA4B;AAClE,SACEC,qBAAqB,EACrBC,wBAAwB,EACxBC,uBAAuB,QAClB,wBAAwB;AAE/B,SAAQC,WAAW,EAAEC,WAAW,QAAO,wBAAwB;AAC/D,SAAQC,CAAC,QAAO,MAAM;AACtB,SAAQC,UAAU,QAAO,uBAAuB;AAChD,SAAQC,wBAAwB,EAAEC,sBAAsB,EAAEC,cAAc,QAAO,kBAAkB;AAEjG,OAAO,SAASC,sBAAsBC,SAAgC;IACpE,OAAOP,YAAY;QACjBQ,QAAQ;QACRC,MAAM;QACNC,aAAa;QACbC,QAAQ;YACNC,QAAQX,EAAEY,MAAM,CAAC;gBAACC,QAAQb,EAAEc,MAAM,GAAGC,IAAI;YAAE;YAC3CC,aAAarB;YACbsB,UAAU;gBACR,KAAKrB;gBACL,KAAKC;YACP;QACF;QACAqB,cAAc,CAACC;YACb,IAAIA,iBAAiBf,gBAAgB;gBACnC,MAAM,IAAIN,YAAY,kDAAkD,cAAc;oBACpFsB,QAAQ;oBACRC,SAAS;wBAACC,kBAAkBH,MAAMI,eAAe;oBAAA;gBACnD;YACF;YACA,IAAIJ,iBAAiBhB,wBAAwB;gBAC3C,MAAM,IAAIL,YAAYqB,MAAMK,OAAO,EAAE,uBAAuB;oBAACJ,QAAQ;gBAAG;YAC1E;YACA,IAAID,iBAAiBjB,0BAA0B;gBAC7C,MAAM,IAAIJ,YAAYqB,MAAMK,OAAO,EAAE,yBAAyB;oBAACJ,QAAQ;gBAAG;YAC5E;YACA,MAAMD;QACR;QACAM,SAAS,OAAOC;YACd,MAAMC,YAAYjC,wBAAwBgC;YAC1C,MAAM,EAACb,MAAM,EAAC,GAAGa,QAAQf,MAAM;YAC/B,MAAM,EAACiB,OAAO,EAAEC,MAAM,EAAC,GAAGH,QAAQI,KAAK;YAEvC,IAAIH,UAAUI,aAAa,KAAKlB,UAAUc,UAAUK,kBAAkB,KAAKJ,SAAS;gBAClF,MAAM,IAAI9B,YAAY,2CAA2C,kBAAkB;oBACjFsB,QAAQ;gBACV;YACF;YAEA,MAAMa,SAAS,MAAMhC,WACnB;gBACEiC,OAAOP,UAAUO,KAAK;gBACtBC,aAAaR,UAAUQ,WAAW;gBAClCC,WAAWT,UAAUS,SAAS;gBAC9BC,sBAAsBV,UAAUU,oBAAoB;gBACpDxB;gBACAe;gBACAC;gBACAS,MAAM,AAACZ,QAAQY,IAAI,IAA2B7C,OAAO8C,KAAK,CAAC;YAC7D,GACAjC;YAGF,OAAO;gBAACgB,kBAAkBW,OAAOV,eAAe;gBAAEiB,QAAQP,OAAOO,MAAM;YAAA;QACzE;IACF;AACF"}
@@ -1,3 +1,4 @@
1
+ import type { WorkflowsModuleClient } from '@shipfox/api-workflows-dto/inter-module';
1
2
  import { type RouteGroup } from '@shipfox/node-fastify';
2
- export declare const logsRoutes: RouteGroup[];
3
+ export declare function createLogsRoutes(workflows: WorkflowsModuleClient): RouteGroup[];
3
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/presentation/routes/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAsB,KAAK,UAAU,EAAC,MAAM,uBAAuB,CAAC;AAQ3E,eAAO,MAAM,UAAU,EAAE,UAAU,EAiBlC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/presentation/routes/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,yCAAyC,CAAC;AACnF,OAAO,EAAsB,KAAK,UAAU,EAAC,MAAM,uBAAuB,CAAC;AAQ3E,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,qBAAqB,GAAG,UAAU,EAAE,CAmB/E"}
@@ -1,32 +1,34 @@
1
1
  import { AUTH_LEASED_JOB, AUTH_USER } from '@shipfox/api-auth-context';
2
2
  import { createRawBodyPlugin } from '@shipfox/node-fastify';
3
3
  import { config } from '#config.js';
4
- import { appendLogsRoute } from './append-logs.js';
4
+ import { createAppendLogsRoute } from './append-logs.js';
5
5
  import { readLogsRoute } from './read-logs.js';
6
6
  // Keep the lease-authed append in its own Fastify scope so the raw NDJSON parser does not
7
7
  // disturb the JSON read route (or workflow routes). The body limit also bounds one-append
8
8
  // budget overshoot. The read route is session-authed and workspace-scoped via the row.
9
- export const logsRoutes = [
10
- {
11
- prefix: '/runs/jobs/current',
12
- auth: AUTH_LEASED_JOB,
13
- plugins: [
14
- createRawBodyPlugin({
15
- contentType: 'application/x-ndjson',
16
- bodyLimit: config.LOG_APPEND_BODY_LIMIT_BYTES
17
- })
18
- ],
19
- routes: [
20
- appendLogsRoute
21
- ]
22
- },
23
- {
24
- prefix: '/steps',
25
- auth: AUTH_USER,
26
- routes: [
27
- readLogsRoute
28
- ]
29
- }
30
- ];
9
+ export function createLogsRoutes(workflows) {
10
+ return [
11
+ {
12
+ prefix: '/runs/jobs/current',
13
+ auth: AUTH_LEASED_JOB,
14
+ plugins: [
15
+ createRawBodyPlugin({
16
+ contentType: 'application/x-ndjson',
17
+ bodyLimit: config.LOG_APPEND_BODY_LIMIT_BYTES
18
+ })
19
+ ],
20
+ routes: [
21
+ createAppendLogsRoute(workflows)
22
+ ]
23
+ },
24
+ {
25
+ prefix: '/steps',
26
+ auth: AUTH_USER,
27
+ routes: [
28
+ readLogsRoute
29
+ ]
30
+ }
31
+ ];
32
+ }
31
33
 
32
34
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/presentation/routes/index.ts"],"sourcesContent":["import {AUTH_LEASED_JOB, AUTH_USER} from '@shipfox/api-auth-context';\nimport {createRawBodyPlugin, type RouteGroup} from '@shipfox/node-fastify';\nimport {config} from '#config.js';\nimport {appendLogsRoute} from './append-logs.js';\nimport {readLogsRoute} from './read-logs.js';\n\n// Keep the lease-authed append in its own Fastify scope so the raw NDJSON parser does not\n// disturb the JSON read route (or workflow routes). The body limit also bounds one-append\n// budget overshoot. The read route is session-authed and workspace-scoped via the row.\nexport const logsRoutes: RouteGroup[] = [\n {\n prefix: '/runs/jobs/current',\n auth: AUTH_LEASED_JOB,\n plugins: [\n createRawBodyPlugin({\n contentType: 'application/x-ndjson',\n bodyLimit: config.LOG_APPEND_BODY_LIMIT_BYTES,\n }),\n ],\n routes: [appendLogsRoute],\n },\n {\n prefix: '/steps',\n auth: AUTH_USER,\n routes: [readLogsRoute],\n },\n];\n"],"names":["AUTH_LEASED_JOB","AUTH_USER","createRawBodyPlugin","config","appendLogsRoute","readLogsRoute","logsRoutes","prefix","auth","plugins","contentType","bodyLimit","LOG_APPEND_BODY_LIMIT_BYTES","routes"],"mappings":"AAAA,SAAQA,eAAe,EAAEC,SAAS,QAAO,4BAA4B;AACrE,SAAQC,mBAAmB,QAAwB,wBAAwB;AAC3E,SAAQC,MAAM,QAAO,aAAa;AAClC,SAAQC,eAAe,QAAO,mBAAmB;AACjD,SAAQC,aAAa,QAAO,iBAAiB;AAE7C,0FAA0F;AAC1F,0FAA0F;AAC1F,uFAAuF;AACvF,OAAO,MAAMC,aAA2B;IACtC;QACEC,QAAQ;QACRC,MAAMR;QACNS,SAAS;YACPP,oBAAoB;gBAClBQ,aAAa;gBACbC,WAAWR,OAAOS,2BAA2B;YAC/C;SACD;QACDC,QAAQ;YAACT;SAAgB;IAC3B;IACA;QACEG,QAAQ;QACRC,MAAMP;QACNY,QAAQ;YAACR;SAAc;IACzB;CACD,CAAC"}
1
+ {"version":3,"sources":["../../../src/presentation/routes/index.ts"],"sourcesContent":["import {AUTH_LEASED_JOB, AUTH_USER} from '@shipfox/api-auth-context';\nimport type {WorkflowsModuleClient} from '@shipfox/api-workflows-dto/inter-module';\nimport {createRawBodyPlugin, type RouteGroup} from '@shipfox/node-fastify';\nimport {config} from '#config.js';\nimport {createAppendLogsRoute} from './append-logs.js';\nimport {readLogsRoute} from './read-logs.js';\n\n// Keep the lease-authed append in its own Fastify scope so the raw NDJSON parser does not\n// disturb the JSON read route (or workflow routes). The body limit also bounds one-append\n// budget overshoot. The read route is session-authed and workspace-scoped via the row.\nexport function createLogsRoutes(workflows: WorkflowsModuleClient): RouteGroup[] {\n return [\n {\n prefix: '/runs/jobs/current',\n auth: AUTH_LEASED_JOB,\n plugins: [\n createRawBodyPlugin({\n contentType: 'application/x-ndjson',\n bodyLimit: config.LOG_APPEND_BODY_LIMIT_BYTES,\n }),\n ],\n routes: [createAppendLogsRoute(workflows)],\n },\n {\n prefix: '/steps',\n auth: AUTH_USER,\n routes: [readLogsRoute],\n },\n ];\n}\n"],"names":["AUTH_LEASED_JOB","AUTH_USER","createRawBodyPlugin","config","createAppendLogsRoute","readLogsRoute","createLogsRoutes","workflows","prefix","auth","plugins","contentType","bodyLimit","LOG_APPEND_BODY_LIMIT_BYTES","routes"],"mappings":"AAAA,SAAQA,eAAe,EAAEC,SAAS,QAAO,4BAA4B;AAErE,SAAQC,mBAAmB,QAAwB,wBAAwB;AAC3E,SAAQC,MAAM,QAAO,aAAa;AAClC,SAAQC,qBAAqB,QAAO,mBAAmB;AACvD,SAAQC,aAAa,QAAO,iBAAiB;AAE7C,0FAA0F;AAC1F,0FAA0F;AAC1F,uFAAuF;AACvF,OAAO,SAASC,iBAAiBC,SAAgC;IAC/D,OAAO;QACL;YACEC,QAAQ;YACRC,MAAMT;YACNU,SAAS;gBACPR,oBAAoB;oBAClBS,aAAa;oBACbC,WAAWT,OAAOU,2BAA2B;gBAC/C;aACD;YACDC,QAAQ;gBAACV,sBAAsBG;aAAW;QAC5C;QACA;YACEC,QAAQ;YACRC,MAAMR;YACNa,QAAQ;gBAACT;aAAc;QACzB;KACD;AACH"}
@@ -1 +1 @@
1
- {"version":3,"file":"compact-stream.d.ts","sourceRoot":"","sources":["../../../src/temporal/activities/compact-stream.ts"],"names":[],"mappings":"AAEA,OAAO,EAAC,mBAAmB,EAAC,MAAM,qBAAqB,CAAC;AAExD,OAAO,EAAK,KAAK,WAAW,EAAC,MAAM,WAAW,CAAC;AAI/C,MAAM,MAAM,mBAAmB,GAC3B;IAAC,OAAO,EAAE,MAAM,CAAA;CAAC,GACjB;IAAC,OAAO,EAAE,mBAAmB,CAAA;CAAC,GAC9B;IAAC,OAAO,EAAE,YAAY,CAAA;CAAC,GACvB;IAAC,OAAO,EAAE,iBAAiB,CAAA;CAAC,GAC5B;IAAC,OAAO,EAAE,WAAW,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,iBAAiB,EAAE,MAAM,CAAA;CAAC,CAAC;AAE7F,UAAU,yBAAyB;IACjC,mBAAmB,EAAE,OAAO,mBAAmB,CAAC;IAChD,2BAA2B,EAAE,CAC3B,EAAE,EAAE,WAAW,EACf,MAAM,EAAE;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAC,KAC1C,OAAO,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAC,CAAC,CAAC;CAClC;AAsFD,wBAAgB,2BAA2B,CACzC,YAAY,GAAE,yBAA+C,GAC5D,CAAC,MAAM,EAAE;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAC,KAAK,OAAO,CAAC,mBAAmB,CAAC,CAW9D;AAED,eAAO,MAAM,qBAAqB,WAbtB;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAC,KAAK,OAAO,CAAC,mBAAmB,CAaI,CAAC"}
1
+ {"version":3,"file":"compact-stream.d.ts","sourceRoot":"","sources":["../../../src/temporal/activities/compact-stream.ts"],"names":[],"mappings":"AAIA,OAAO,EAAC,mBAAmB,EAAC,MAAM,qBAAqB,CAAC;AAExD,OAAO,EAAK,KAAK,WAAW,EAAC,MAAM,WAAW,CAAC;AAI/C,MAAM,MAAM,mBAAmB,GAC3B;IAAC,OAAO,EAAE,MAAM,CAAA;CAAC,GACjB;IAAC,OAAO,EAAE,mBAAmB,CAAA;CAAC,GAC9B;IAAC,OAAO,EAAE,YAAY,CAAA;CAAC,GACvB;IAAC,OAAO,EAAE,iBAAiB,CAAA;CAAC,GAC5B;IAAC,OAAO,EAAE,WAAW,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,iBAAiB,EAAE,MAAM,CAAA;CAAC,CAAC;AAE7F,UAAU,yBAAyB;IACjC,mBAAmB,EAAE,OAAO,mBAAmB,CAAC;IAChD,2BAA2B,EAAE,CAC3B,EAAE,EAAE,WAAW,EACf,MAAM,EAAE;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAC,KAC1C,OAAO,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAC,CAAC,CAAC;CAClC;AAgGD,wBAAgB,2BAA2B,CACzC,YAAY,GAAE,yBAA+C,GAC5D,CAAC,MAAM,EAAE;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAC,KAAK,OAAO,CAAC,mBAAmB,CAAC,CAW9D;AAED,eAAO,MAAM,qBAAqB,WAbtB;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAC,KAAK,OAAO,CAAC,mBAAmB,CAaI,CAAC"}
@@ -1,3 +1,5 @@
1
+ import { reportError } from '@shipfox/node-error-monitoring';
2
+ import { logger } from '@shipfox/node-opentelemetry';
1
3
  import { Context } from '@temporalio/activity';
2
4
  import { compactedObjectKey, deleteObject, putCompactedObject } from '#api/object-storage.js';
3
5
  import { compactedGzipStream } from '#core/compaction.js';
@@ -55,7 +57,21 @@ const defaultDependencies = {
55
57
  }
56
58
  });
57
59
  if (stats.chunkCount !== expected.count || stats.lastSeq !== expected.maxSeq || stats.uncompressedBytes !== expected.uncompressedBytes) {
58
- await deleteObject(uploadKey).catch(()=>undefined);
60
+ await deleteObject(uploadKey).catch((error)=>{
61
+ logger().error({
62
+ err: error,
63
+ streamId: stream.id,
64
+ objectKey: uploadKey
65
+ }, 'Failed to delete invalid compacted log object');
66
+ reportError(error, {
67
+ boundary: 'logs.cleanup',
68
+ operation: 'delete-invalid-compaction-object',
69
+ extra: {
70
+ streamId: stream.id,
71
+ objectKey: uploadKey
72
+ }
73
+ });
74
+ });
59
75
  throw new Error(`Compaction integrity check failed for stream ${stream.id}: streamed ${stats.chunkCount} chunks / ${stats.uncompressedBytes} bytes up to seq ${stats.lastSeq}, table holds ${expected.count} / ${expected.uncompressedBytes} bytes up to seq ${expected.maxSeq}`);
60
76
  }
61
77
  const { updated } = await db().transaction((tx)=>dependencies.setObjectKeyAndDeleteChunks(tx, {