duron 0.2.2 → 0.3.0-beta.1

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 (77) hide show
  1. package/dist/action-job.d.ts +2 -0
  2. package/dist/action-job.d.ts.map +1 -1
  3. package/dist/action-job.js +20 -1
  4. package/dist/action-manager.d.ts +2 -0
  5. package/dist/action-manager.d.ts.map +1 -1
  6. package/dist/action-manager.js +3 -0
  7. package/dist/action.d.ts +27 -0
  8. package/dist/action.d.ts.map +1 -1
  9. package/dist/action.js +9 -0
  10. package/dist/adapters/adapter.d.ts +10 -2
  11. package/dist/adapters/adapter.d.ts.map +1 -1
  12. package/dist/adapters/adapter.js +59 -1
  13. package/dist/adapters/postgres/base.d.ts +9 -4
  14. package/dist/adapters/postgres/base.d.ts.map +1 -1
  15. package/dist/adapters/postgres/base.js +269 -19
  16. package/dist/adapters/postgres/schema.d.ts +249 -105
  17. package/dist/adapters/postgres/schema.d.ts.map +1 -1
  18. package/dist/adapters/postgres/schema.default.d.ts +249 -106
  19. package/dist/adapters/postgres/schema.default.d.ts.map +1 -1
  20. package/dist/adapters/postgres/schema.default.js +2 -2
  21. package/dist/adapters/postgres/schema.js +29 -1
  22. package/dist/adapters/schemas.d.ts +140 -7
  23. package/dist/adapters/schemas.d.ts.map +1 -1
  24. package/dist/adapters/schemas.js +52 -4
  25. package/dist/client.d.ts +8 -1
  26. package/dist/client.d.ts.map +1 -1
  27. package/dist/client.js +28 -0
  28. package/dist/errors.d.ts +6 -0
  29. package/dist/errors.d.ts.map +1 -1
  30. package/dist/errors.js +16 -1
  31. package/dist/index.d.ts +4 -2
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +4 -2
  34. package/dist/server.d.ts +220 -16
  35. package/dist/server.d.ts.map +1 -1
  36. package/dist/server.js +123 -8
  37. package/dist/step-manager.d.ts +8 -2
  38. package/dist/step-manager.d.ts.map +1 -1
  39. package/dist/step-manager.js +174 -15
  40. package/dist/telemetry/adapter.d.ts +85 -0
  41. package/dist/telemetry/adapter.d.ts.map +1 -0
  42. package/dist/telemetry/adapter.js +128 -0
  43. package/dist/telemetry/index.d.ts +5 -0
  44. package/dist/telemetry/index.d.ts.map +1 -0
  45. package/dist/telemetry/index.js +4 -0
  46. package/dist/telemetry/local.d.ts +21 -0
  47. package/dist/telemetry/local.d.ts.map +1 -0
  48. package/dist/telemetry/local.js +180 -0
  49. package/dist/telemetry/noop.d.ts +16 -0
  50. package/dist/telemetry/noop.d.ts.map +1 -0
  51. package/dist/telemetry/noop.js +39 -0
  52. package/dist/telemetry/opentelemetry.d.ts +24 -0
  53. package/dist/telemetry/opentelemetry.d.ts.map +1 -0
  54. package/dist/telemetry/opentelemetry.js +202 -0
  55. package/migrations/postgres/20260117231749_clumsy_penance/migration.sql +3 -0
  56. package/migrations/postgres/20260117231749_clumsy_penance/snapshot.json +988 -0
  57. package/migrations/postgres/20260118202533_wealthy_mysterio/migration.sql +24 -0
  58. package/migrations/postgres/20260118202533_wealthy_mysterio/snapshot.json +1362 -0
  59. package/package.json +6 -4
  60. package/src/action-job.ts +35 -0
  61. package/src/action-manager.ts +5 -0
  62. package/src/action.ts +199 -0
  63. package/src/adapters/adapter.ts +151 -0
  64. package/src/adapters/postgres/base.ts +342 -23
  65. package/src/adapters/postgres/schema.default.ts +2 -2
  66. package/src/adapters/postgres/schema.ts +49 -1
  67. package/src/adapters/schemas.ts +81 -5
  68. package/src/client.ts +78 -0
  69. package/src/errors.ts +45 -1
  70. package/src/index.ts +10 -2
  71. package/src/server.ts +163 -8
  72. package/src/step-manager.ts +293 -13
  73. package/src/telemetry/adapter.ts +468 -0
  74. package/src/telemetry/index.ts +17 -0
  75. package/src/telemetry/local.ts +336 -0
  76. package/src/telemetry/noop.ts +95 -0
  77. package/src/telemetry/opentelemetry.ts +310 -0
@@ -0,0 +1,336 @@
1
+ import type { Adapter, InsertMetricOptions } from '../adapters/adapter.js'
2
+ import {
3
+ type AddSpanAttributeOptions,
4
+ type AddSpanEventOptions,
5
+ type EndSpanOptions,
6
+ type RecordMetricOptions,
7
+ type Span,
8
+ type StartDatabaseSpanOptions,
9
+ type StartJobSpanOptions,
10
+ type StartStepSpanOptions,
11
+ TelemetryAdapter,
12
+ } from './adapter.js'
13
+
14
+ // ============================================================================
15
+ // Types
16
+ // ============================================================================
17
+
18
+ export interface LocalTelemetryAdapterOptions {
19
+ /**
20
+ * Delay in milliseconds before flushing queued metrics to the database.
21
+ * Metrics are batched and inserted after this delay of inactivity.
22
+ * @default 1000
23
+ */
24
+ flushDelayMs?: number
25
+ }
26
+
27
+ // ============================================================================
28
+ // Constants
29
+ // ============================================================================
30
+
31
+ const DEFAULT_FLUSH_DELAY_MS = 1000
32
+
33
+ // ============================================================================
34
+ // Local Telemetry Adapter
35
+ // ============================================================================
36
+
37
+ /**
38
+ * Local telemetry adapter that stores metrics directly in the Duron database.
39
+ * Perfect for development and self-hosted deployments.
40
+ *
41
+ * This adapter automatically uses the database adapter configured in the Duron client.
42
+ * Metrics are batched and inserted after a configurable delay of inactivity to reduce database load.
43
+ *
44
+ * @example
45
+ * ```typescript
46
+ * const client = duron({
47
+ * database: postgresAdapter({ connection: 'postgres://...' }),
48
+ * telemetry: localTelemetryAdapter({ flushDelayMs: 500 }), // Custom 500ms delay
49
+ * actions: { ... }
50
+ * })
51
+ * ```
52
+ */
53
+ export class LocalTelemetryAdapter extends TelemetryAdapter {
54
+ #spanStartTimes = new Map<string, number>()
55
+ #metricsQueue: InsertMetricOptions[] = []
56
+ #flushTimer: ReturnType<typeof setTimeout> | null = null
57
+ #flushPromise: Promise<void> | null = null
58
+ #flushDelayMs: number
59
+
60
+ constructor(options?: LocalTelemetryAdapterOptions) {
61
+ super()
62
+ this.#flushDelayMs = options?.flushDelayMs ?? DEFAULT_FLUSH_DELAY_MS
63
+ }
64
+
65
+ /**
66
+ * Get the database adapter from the Duron client.
67
+ * @throws Error if the client is not set
68
+ */
69
+ get #database(): Adapter {
70
+ const client = this.client
71
+ if (!client) {
72
+ throw new Error(
73
+ 'LocalTelemetryAdapter requires the Duron client to be set. This is done automatically by the Duron client.',
74
+ )
75
+ }
76
+ return client.database
77
+ }
78
+
79
+ // ============================================================================
80
+ // Queue Management
81
+ // ============================================================================
82
+
83
+ /**
84
+ * Queue a metric for batch insertion.
85
+ * The metric will be inserted after 1 second of inactivity.
86
+ */
87
+ #queueMetric(options: InsertMetricOptions): void {
88
+ this.#metricsQueue.push(options)
89
+ this.#scheduleFlush()
90
+ }
91
+
92
+ /**
93
+ * Schedule a flush of the metrics queue.
94
+ * Resets the timer on each call (debounce behavior).
95
+ */
96
+ #scheduleFlush(): void {
97
+ if (this.#flushTimer) {
98
+ clearTimeout(this.#flushTimer)
99
+ }
100
+
101
+ this.#flushTimer = setTimeout(() => {
102
+ this.#flushTimer = null
103
+ this.#flushPromise = this.#flushQueue().finally(() => {
104
+ this.#flushPromise = null
105
+ })
106
+ }, this.#flushDelayMs)
107
+ }
108
+
109
+ /**
110
+ * Flush all queued metrics to the database.
111
+ */
112
+ async #flushQueue(): Promise<void> {
113
+ if (this.#metricsQueue.length === 0) {
114
+ return
115
+ }
116
+
117
+ // Take all metrics from the queue
118
+ const metrics = this.#metricsQueue.splice(0, this.#metricsQueue.length)
119
+
120
+ // Batch insert all metrics in a single database operation
121
+ await this.#database.insertMetrics(metrics)
122
+ }
123
+
124
+ /**
125
+ * Force flush the queue immediately.
126
+ * Used during shutdown to ensure all metrics are persisted.
127
+ */
128
+ async #forceFlush(): Promise<void> {
129
+ // Clear any pending timer
130
+ if (this.#flushTimer) {
131
+ clearTimeout(this.#flushTimer)
132
+ this.#flushTimer = null
133
+ }
134
+
135
+ // Wait for any in-progress flush
136
+ if (this.#flushPromise) {
137
+ await this.#flushPromise
138
+ }
139
+
140
+ // Flush remaining metrics
141
+ await this.#flushQueue()
142
+ }
143
+
144
+ // ============================================================================
145
+ // Lifecycle Methods
146
+ // ============================================================================
147
+
148
+ protected async _start(): Promise<void> {
149
+ // Database adapter should already be started by the client
150
+ }
151
+
152
+ protected async _stop(): Promise<void> {
153
+ // Flush any remaining metrics before stopping
154
+ await this.#forceFlush()
155
+ this.#spanStartTimes.clear()
156
+ }
157
+
158
+ // ============================================================================
159
+ // Span Methods
160
+ // ============================================================================
161
+
162
+ protected async _startJobSpan(options: StartJobSpanOptions): Promise<Span> {
163
+ const spanId = `job:${options.jobId}`
164
+ this.#spanStartTimes.set(spanId, Date.now())
165
+
166
+ // Record span start as a metric
167
+ this.#queueMetric({
168
+ jobId: options.jobId,
169
+ name: 'duron.job.span.start',
170
+ value: Date.now(),
171
+ type: 'span_event',
172
+ attributes: {
173
+ actionName: options.actionName,
174
+ groupKey: options.groupKey,
175
+ spanId,
176
+ },
177
+ })
178
+
179
+ return {
180
+ id: spanId,
181
+ jobId: options.jobId,
182
+ stepId: null,
183
+ parentSpanId: null,
184
+ }
185
+ }
186
+
187
+ protected async _endJobSpan(span: Span, options: EndSpanOptions): Promise<void> {
188
+ const startTime = this.#spanStartTimes.get(span.id)
189
+ const duration = startTime ? Date.now() - startTime : 0
190
+ this.#spanStartTimes.delete(span.id)
191
+
192
+ // Record span end with duration
193
+ this.#queueMetric({
194
+ jobId: span.jobId,
195
+ name: 'duron.job.span.end',
196
+ value: duration,
197
+ type: 'span_event',
198
+ attributes: {
199
+ spanId: span.id,
200
+ status: options.status,
201
+ error: options.error?.message ?? null,
202
+ durationMs: duration,
203
+ },
204
+ })
205
+ }
206
+
207
+ protected async _startStepSpan(options: StartStepSpanOptions): Promise<Span> {
208
+ const spanId = `step:${options.stepId}`
209
+ this.#spanStartTimes.set(spanId, Date.now())
210
+
211
+ // Record span start as a metric
212
+ this.#queueMetric({
213
+ jobId: options.jobId,
214
+ stepId: options.stepId,
215
+ name: 'duron.step.span.start',
216
+ value: Date.now(),
217
+ type: 'span_event',
218
+ attributes: {
219
+ stepName: options.stepName,
220
+ parentStepId: options.parentStepId,
221
+ parentSpanId: options.parentSpan?.id ?? null,
222
+ spanId,
223
+ },
224
+ })
225
+
226
+ return {
227
+ id: spanId,
228
+ jobId: options.jobId,
229
+ stepId: options.stepId,
230
+ parentSpanId: options.parentSpan?.id ?? null,
231
+ }
232
+ }
233
+
234
+ protected async _endStepSpan(span: Span, options: EndSpanOptions): Promise<void> {
235
+ const startTime = this.#spanStartTimes.get(span.id)
236
+ const duration = startTime ? Date.now() - startTime : 0
237
+ this.#spanStartTimes.delete(span.id)
238
+
239
+ // Record span end with duration
240
+ this.#queueMetric({
241
+ jobId: span.jobId,
242
+ stepId: span.stepId ?? undefined,
243
+ name: 'duron.step.span.end',
244
+ value: duration,
245
+ type: 'span_event',
246
+ attributes: {
247
+ spanId: span.id,
248
+ status: options.status,
249
+ error: options.error?.message ?? null,
250
+ durationMs: duration,
251
+ },
252
+ })
253
+ }
254
+
255
+ protected async _startDatabaseSpan(_options: StartDatabaseSpanOptions): Promise<Span | null> {
256
+ // Local adapter doesn't trace database operations to avoid infinite loops
257
+ return null
258
+ }
259
+
260
+ protected async _endDatabaseSpan(_span: Span, _options: EndSpanOptions): Promise<void> {
261
+ // No-op for local adapter
262
+ }
263
+
264
+ // ============================================================================
265
+ // Metrics Methods
266
+ // ============================================================================
267
+
268
+ protected async _recordMetric(options: RecordMetricOptions): Promise<void> {
269
+ this.#queueMetric({
270
+ jobId: options.jobId,
271
+ stepId: options.stepId,
272
+ name: options.name,
273
+ value: options.value,
274
+ type: 'metric',
275
+ attributes: options.attributes,
276
+ })
277
+ }
278
+
279
+ protected async _addSpanEvent(options: AddSpanEventOptions): Promise<void> {
280
+ this.#queueMetric({
281
+ jobId: options.span.jobId,
282
+ stepId: options.span.stepId ?? undefined,
283
+ name: options.name,
284
+ value: Date.now(),
285
+ type: 'span_event',
286
+ attributes: {
287
+ spanId: options.span.id,
288
+ ...options.attributes,
289
+ },
290
+ })
291
+ }
292
+
293
+ protected async _addSpanAttribute(options: AddSpanAttributeOptions): Promise<void> {
294
+ this.#queueMetric({
295
+ jobId: options.span.jobId,
296
+ stepId: options.span.stepId ?? undefined,
297
+ name: `attribute:${options.key}`,
298
+ value: typeof options.value === 'number' ? options.value : 0,
299
+ type: 'span_attribute',
300
+ attributes: {
301
+ spanId: options.span.id,
302
+ key: options.key,
303
+ value: String(options.value),
304
+ },
305
+ })
306
+ }
307
+ }
308
+
309
+ /**
310
+ * Create a local telemetry adapter that stores metrics in the Duron database.
311
+ * Perfect for development and self-hosted deployments.
312
+ *
313
+ * The database adapter is automatically obtained from the Duron client.
314
+ * Metrics are batched and inserted after a configurable delay of inactivity to reduce database load.
315
+ *
316
+ * @param options - Configuration options
317
+ * @param options.flushDelayMs - Delay in milliseconds before flushing queued metrics (default: 1000)
318
+ * @returns LocalTelemetryAdapter instance
319
+ *
320
+ * @example
321
+ * ```typescript
322
+ * const client = duron({
323
+ * database: postgresAdapter({ connection: 'postgres://...' }),
324
+ * telemetry: localTelemetryAdapter(), // Uses default 1 second delay
325
+ * actions: { ... }
326
+ * })
327
+ *
328
+ * // Or with custom flush delay
329
+ * const client = duron({
330
+ * database: postgresAdapter({ connection: 'postgres://...' }),
331
+ * telemetry: localTelemetryAdapter({ flushDelayMs: 500 }), // 500ms delay
332
+ * actions: { ... }
333
+ * })
334
+ * ```
335
+ */
336
+ export const localTelemetryAdapter = (options?: LocalTelemetryAdapterOptions) => new LocalTelemetryAdapter(options)
@@ -0,0 +1,95 @@
1
+ import {
2
+ type AddSpanAttributeOptions,
3
+ type AddSpanEventOptions,
4
+ type EndSpanOptions,
5
+ type RecordMetricOptions,
6
+ type Span,
7
+ type StartDatabaseSpanOptions,
8
+ type StartJobSpanOptions,
9
+ type StartStepSpanOptions,
10
+ TelemetryAdapter,
11
+ } from './adapter.js'
12
+
13
+ // ============================================================================
14
+ // Noop Telemetry Adapter
15
+ // ============================================================================
16
+
17
+ /**
18
+ * No-operation telemetry adapter.
19
+ * Used when telemetry is disabled. All methods are no-ops.
20
+ */
21
+ export class NoopTelemetryAdapter extends TelemetryAdapter {
22
+ // ============================================================================
23
+ // Lifecycle Methods
24
+ // ============================================================================
25
+
26
+ protected async _start(): Promise<void> {
27
+ // No-op
28
+ }
29
+
30
+ protected async _stop(): Promise<void> {
31
+ // No-op
32
+ }
33
+
34
+ // ============================================================================
35
+ // Span Methods
36
+ // ============================================================================
37
+
38
+ protected async _startJobSpan(options: StartJobSpanOptions): Promise<Span> {
39
+ return {
40
+ id: 'noop',
41
+ jobId: options.jobId,
42
+ stepId: null,
43
+ parentSpanId: null,
44
+ }
45
+ }
46
+
47
+ protected async _endJobSpan(_span: Span, _options: EndSpanOptions): Promise<void> {
48
+ // No-op
49
+ }
50
+
51
+ protected async _startStepSpan(options: StartStepSpanOptions): Promise<Span> {
52
+ return {
53
+ id: 'noop',
54
+ jobId: options.jobId,
55
+ stepId: options.stepId,
56
+ parentSpanId: options.parentSpan?.id ?? null,
57
+ }
58
+ }
59
+
60
+ protected async _endStepSpan(_span: Span, _options: EndSpanOptions): Promise<void> {
61
+ // No-op
62
+ }
63
+
64
+ protected async _startDatabaseSpan(_options: StartDatabaseSpanOptions): Promise<Span | null> {
65
+ return null
66
+ }
67
+
68
+ protected async _endDatabaseSpan(_span: Span, _options: EndSpanOptions): Promise<void> {
69
+ // No-op
70
+ }
71
+
72
+ // ============================================================================
73
+ // Metrics Methods
74
+ // ============================================================================
75
+
76
+ protected async _recordMetric(_options: RecordMetricOptions): Promise<void> {
77
+ // No-op
78
+ }
79
+
80
+ protected async _addSpanEvent(_options: AddSpanEventOptions): Promise<void> {
81
+ // No-op
82
+ }
83
+
84
+ protected async _addSpanAttribute(_options: AddSpanAttributeOptions): Promise<void> {
85
+ // No-op
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Create a no-operation telemetry adapter.
91
+ * Use this when telemetry should be disabled.
92
+ *
93
+ * @returns NoopTelemetryAdapter instance
94
+ */
95
+ export const noopTelemetryAdapter = () => new NoopTelemetryAdapter()