@jagreehal/workflow 1.13.0 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +5 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/visualize.cjs +74 -68
- package/dist/visualize.cjs.map +1 -1
- package/dist/visualize.d.cts +117 -2
- package/dist/visualize.d.ts +117 -2
- package/dist/visualize.js +74 -68
- package/dist/visualize.js.map +1 -1
- package/docs/pino-logging-example.md +396 -0
- package/docs/visualization.md +35 -0
- package/package.json +7 -2
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
# Logging Workflow Events to Pino
|
|
2
|
+
|
|
3
|
+
Workflow events are structured JSON objects that work perfectly with Pino's structured logging.
|
|
4
|
+
|
|
5
|
+
## Using `renderAs('logger')` (Recommended)
|
|
6
|
+
|
|
7
|
+
The easiest way to log workflow data is using the built-in logger renderer:
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { createWorkflow, createVisualizer } from '@jagreehal/workflow';
|
|
11
|
+
import pino from 'pino';
|
|
12
|
+
|
|
13
|
+
const logger = pino();
|
|
14
|
+
const viz = createVisualizer({ workflowName: 'checkout' });
|
|
15
|
+
|
|
16
|
+
const workflow = createWorkflow({ fetchUser, processPayment }, {
|
|
17
|
+
onEvent: viz.handleEvent,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const result = await workflow(async (step) => {
|
|
21
|
+
const user = await step(() => fetchUser('1'), { name: 'Fetch user' });
|
|
22
|
+
await step(() => processPayment(user.id), { name: 'Process payment' });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Log structured workflow data
|
|
26
|
+
// Note: renderAs() returns a JSON string for API consistency across renderers
|
|
27
|
+
const logData = JSON.parse(viz.renderAs('logger'));
|
|
28
|
+
logger.info(logData, 'Workflow completed');
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Logger Output
|
|
32
|
+
|
|
33
|
+
The renderer returns this structure (Pino will add `level`, `time`, and `msg` when logging):
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"workflow": {
|
|
38
|
+
"id": "8757b92c-d415-4555-93cd-37076cbec9ee",
|
|
39
|
+
"name": "checkout",
|
|
40
|
+
"state": "success",
|
|
41
|
+
"durationMs": 33,
|
|
42
|
+
"startedAt": 1768506411038
|
|
43
|
+
},
|
|
44
|
+
"steps": [
|
|
45
|
+
{
|
|
46
|
+
"id": "user:1",
|
|
47
|
+
"name": "Fetch user",
|
|
48
|
+
"state": "success",
|
|
49
|
+
"durationMs": 11
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"id": "payment:1",
|
|
53
|
+
"name": "Process payment",
|
|
54
|
+
"state": "success",
|
|
55
|
+
"durationMs": 21,
|
|
56
|
+
"retryCount": 1
|
|
57
|
+
}
|
|
58
|
+
],
|
|
59
|
+
"summary": {
|
|
60
|
+
"totalSteps": 2,
|
|
61
|
+
"successCount": 2,
|
|
62
|
+
"errorCount": 0,
|
|
63
|
+
"cacheHits": 0,
|
|
64
|
+
"totalRetries": 1,
|
|
65
|
+
"slowestStep": { "name": "Process payment", "durationMs": 21 }
|
|
66
|
+
},
|
|
67
|
+
"diagram": "┌── checkout ─────────────────────┐\n│ ✓ Fetch user [11ms] │\n│ ✓ Process payment [21ms] │\n└─────────────────────────────────┘"
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Works With Any Logger
|
|
72
|
+
|
|
73
|
+
The logger renderer is logger-agnostic:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
// Pino
|
|
77
|
+
logger.info(JSON.parse(viz.renderAs('logger')), 'Workflow completed');
|
|
78
|
+
|
|
79
|
+
// Winston
|
|
80
|
+
logger.info('Workflow completed', JSON.parse(viz.renderAs('logger')));
|
|
81
|
+
|
|
82
|
+
// Console
|
|
83
|
+
console.log('Workflow completed:', JSON.parse(viz.renderAs('logger')));
|
|
84
|
+
|
|
85
|
+
// OpenTelemetry
|
|
86
|
+
span.addEvent('workflow_completed', JSON.parse(viz.renderAs('logger')));
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Why Use `renderAs('logger')`?
|
|
90
|
+
|
|
91
|
+
- **Structured data**: Workflow state, step details, and summary stats as queryable fields
|
|
92
|
+
- **Summary calculations**: Slowest step, retry counts, error counts computed for you
|
|
93
|
+
- **ASCII diagram included**: ANSI colors stripped for clean JSON
|
|
94
|
+
- **One call**: No need to parse events manually
|
|
95
|
+
|
|
96
|
+
### When to Use Each Approach
|
|
97
|
+
|
|
98
|
+
| Use Case | Recommended Approach |
|
|
99
|
+
|----------|---------------------|
|
|
100
|
+
| Log workflow summary after completion | `renderAs('logger')` |
|
|
101
|
+
| Need custom filtering of steps/events | Manual approach |
|
|
102
|
+
| Real-time logging during execution | Manual approach with `onEvent` |
|
|
103
|
+
| Just want structured output quickly | `renderAs('logger')` |
|
|
104
|
+
| Need to transform events before logging | Manual approach |
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Manual Approach (More Control)
|
|
109
|
+
|
|
110
|
+
For more control over what gets logged, you can collect events manually.
|
|
111
|
+
|
|
112
|
+
## Basic Example
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
import { createWorkflow, createEventCollector } from '@jagreehal/workflow';
|
|
116
|
+
import pino from 'pino';
|
|
117
|
+
|
|
118
|
+
const logger = pino();
|
|
119
|
+
|
|
120
|
+
const collector = createEventCollector();
|
|
121
|
+
const workflow = createWorkflow({ fetchUser, fetchPosts }, {
|
|
122
|
+
onEvent: collector.handleEvent,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const result = await workflow(async (step) => {
|
|
126
|
+
const user = await step(() => fetchUser('1'), { name: 'Fetch user' });
|
|
127
|
+
const posts = await step(() => fetchPosts(user.id), { name: 'Fetch posts' });
|
|
128
|
+
return { user, posts };
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// After workflow completes, log all events
|
|
132
|
+
const events = collector.getWorkflowEvents();
|
|
133
|
+
|
|
134
|
+
// Log summary
|
|
135
|
+
if (result.ok) {
|
|
136
|
+
logger.info({
|
|
137
|
+
workflowId: events[0]?.workflowId,
|
|
138
|
+
duration: events.find(e => e.type === 'workflow_success')?.durationMs,
|
|
139
|
+
stepCount: events.filter(e => e.type === 'step_success').length,
|
|
140
|
+
}, 'Workflow completed successfully');
|
|
141
|
+
} else {
|
|
142
|
+
logger.error({
|
|
143
|
+
workflowId: events[0]?.workflowId,
|
|
144
|
+
error: result.error,
|
|
145
|
+
duration: events.find(e => e.type === 'workflow_error')?.durationMs,
|
|
146
|
+
}, 'Workflow failed');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Log all events (optional - can be verbose)
|
|
150
|
+
events.forEach(event => {
|
|
151
|
+
logger.debug({ event }, 'Workflow event');
|
|
152
|
+
});
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Pino Output Format
|
|
156
|
+
|
|
157
|
+
Pino will serialize workflow events as clean JSON. Here's what the output looks like:
|
|
158
|
+
|
|
159
|
+
```json
|
|
160
|
+
{
|
|
161
|
+
"level": 30,
|
|
162
|
+
"time": 1704067200000,
|
|
163
|
+
"workflowId": "abc-123",
|
|
164
|
+
"duration": 245,
|
|
165
|
+
"stepCount": 2,
|
|
166
|
+
"msg": "Workflow completed successfully"
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
For individual events:
|
|
171
|
+
|
|
172
|
+
```json
|
|
173
|
+
{
|
|
174
|
+
"level": 20,
|
|
175
|
+
"time": 1704067200000,
|
|
176
|
+
"event": {
|
|
177
|
+
"type": "step_success",
|
|
178
|
+
"workflowId": "abc-123",
|
|
179
|
+
"stepId": "step-1",
|
|
180
|
+
"name": "Fetch user",
|
|
181
|
+
"stepKey": "user:1",
|
|
182
|
+
"ts": 1704067200100,
|
|
183
|
+
"durationMs": 45,
|
|
184
|
+
"context": {
|
|
185
|
+
"requestId": "req-456",
|
|
186
|
+
"userId": "user-789"
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
"msg": "Workflow event"
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Helper Function
|
|
194
|
+
|
|
195
|
+
Create a reusable Pino logger helper:
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
import { createEventCollector, type WorkflowEvent } from '@jagreehal/workflow';
|
|
199
|
+
import pino from 'pino';
|
|
200
|
+
|
|
201
|
+
export function createPinoLogger(logger: pino.Logger) {
|
|
202
|
+
const collector = createEventCollector();
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
handleEvent: collector.handleEvent,
|
|
206
|
+
|
|
207
|
+
logAfterCompletion: (result: { ok: boolean; error?: unknown }) => {
|
|
208
|
+
const events = collector.getWorkflowEvents();
|
|
209
|
+
const workflowId = events[0]?.workflowId;
|
|
210
|
+
|
|
211
|
+
// Find workflow completion event
|
|
212
|
+
const completion = events.find(
|
|
213
|
+
e => e.type === 'workflow_success' || e.type === 'workflow_error'
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
if (result.ok && completion?.type === 'workflow_success') {
|
|
217
|
+
logger.info({
|
|
218
|
+
workflowId,
|
|
219
|
+
duration: completion.durationMs,
|
|
220
|
+
stepCount: events.filter(e => e.type === 'step_success').length,
|
|
221
|
+
cacheHits: events.filter(e => e.type === 'step_cache_hit').length,
|
|
222
|
+
context: completion.context,
|
|
223
|
+
}, 'Workflow completed');
|
|
224
|
+
} else if (!result.ok && completion?.type === 'workflow_error') {
|
|
225
|
+
logger.error({
|
|
226
|
+
workflowId,
|
|
227
|
+
error: result.error,
|
|
228
|
+
duration: completion.durationMs,
|
|
229
|
+
failedStep: events.find(e => e.type === 'step_error')?.name,
|
|
230
|
+
context: completion.context,
|
|
231
|
+
}, 'Workflow failed');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Log slow steps
|
|
235
|
+
events
|
|
236
|
+
.filter((e): e is WorkflowEvent<unknown> & { durationMs: number } =>
|
|
237
|
+
e.type === 'step_success' && e.durationMs > 1000
|
|
238
|
+
)
|
|
239
|
+
.forEach(step => {
|
|
240
|
+
logger.warn({
|
|
241
|
+
workflowId,
|
|
242
|
+
stepName: step.name,
|
|
243
|
+
stepKey: step.stepKey,
|
|
244
|
+
duration: step.durationMs,
|
|
245
|
+
}, 'Slow step detected');
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Log retries
|
|
249
|
+
events
|
|
250
|
+
.filter(e => e.type === 'step_retry')
|
|
251
|
+
.forEach(retry => {
|
|
252
|
+
logger.warn({
|
|
253
|
+
workflowId,
|
|
254
|
+
stepName: retry.name,
|
|
255
|
+
attempt: retry.attempt,
|
|
256
|
+
maxAttempts: retry.maxAttempts,
|
|
257
|
+
error: retry.error,
|
|
258
|
+
}, 'Step retry');
|
|
259
|
+
});
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Usage
|
|
265
|
+
const pinoLogger = createPinoLogger(pino());
|
|
266
|
+
const workflow = createWorkflow(deps, {
|
|
267
|
+
onEvent: pinoLogger.handleEvent,
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const result = await workflow(async (step) => {
|
|
271
|
+
// ... workflow logic
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
pinoLogger.logAfterCompletion(result);
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## Why This Works Well
|
|
278
|
+
|
|
279
|
+
1. **Structured Events**: All workflow events are plain objects with consistent properties
|
|
280
|
+
2. **JSON-Serializable**: Events contain only JSON-safe values (strings, numbers, booleans, objects)
|
|
281
|
+
3. **Context Support**: Events include `context` field for correlation IDs, user IDs, etc.
|
|
282
|
+
4. **Type Safety**: TypeScript knows all event properties
|
|
283
|
+
5. **Pino-Friendly**: Pino automatically serializes objects - no custom formatting needed
|
|
284
|
+
|
|
285
|
+
## Logging ASCII Visualization
|
|
286
|
+
|
|
287
|
+
You can log the ASCII diagram directly to Pino:
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
import { createWorkflow, createVisualizer } from '@jagreehal/workflow';
|
|
291
|
+
import pino from 'pino';
|
|
292
|
+
|
|
293
|
+
const logger = pino();
|
|
294
|
+
const viz = createVisualizer({ workflowName: 'checkout' });
|
|
295
|
+
|
|
296
|
+
const workflow = createWorkflow({ fetchUser, fetchPosts }, {
|
|
297
|
+
onEvent: viz.handleEvent,
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
const result = await workflow(async (step) => {
|
|
301
|
+
const user = await step(() => fetchUser('1'), { name: 'Fetch user' });
|
|
302
|
+
const posts = await step(() => fetchPosts(user.id), { name: 'Fetch posts' });
|
|
303
|
+
return { user, posts };
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// Log ASCII visualization
|
|
307
|
+
const asciiDiagram = viz.render();
|
|
308
|
+
logger.info({
|
|
309
|
+
workflowId: result.ok ? 'success' : 'failed',
|
|
310
|
+
visualization: asciiDiagram,
|
|
311
|
+
}, 'Workflow completed');
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### Pino Output with ASCII
|
|
315
|
+
|
|
316
|
+
In JSON format, the ASCII will be escaped with `\n`:
|
|
317
|
+
|
|
318
|
+
```json
|
|
319
|
+
{
|
|
320
|
+
"level": 30,
|
|
321
|
+
"time": 1704067200000,
|
|
322
|
+
"workflowId": "success",
|
|
323
|
+
"visualization": "┌── checkout ───────────────────────────────┐\n│ │\n│ ✓ Fetch user [45ms] │\n│ ✓ Fetch posts [67ms] │\n│ │\n│ Completed in 112ms │\n│ │\n└──────────────────────────────────────────┘",
|
|
324
|
+
"msg": "Workflow completed"
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### With pino-pretty
|
|
329
|
+
|
|
330
|
+
When using `pino-pretty` or viewing in a terminal, the ASCII renders correctly:
|
|
331
|
+
|
|
332
|
+
```
|
|
333
|
+
[2024-01-01 12:00:00] INFO: Workflow completed
|
|
334
|
+
workflowId: "success"
|
|
335
|
+
visualization: "
|
|
336
|
+
┌── checkout ───────────────────────────────┐
|
|
337
|
+
│ │
|
|
338
|
+
│ ✓ Fetch user [45ms] │
|
|
339
|
+
│ ✓ Fetch posts [67ms] │
|
|
340
|
+
│ │
|
|
341
|
+
│ Completed in 112ms │
|
|
342
|
+
│ │
|
|
343
|
+
└──────────────────────────────────────────┘
|
|
344
|
+
"
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### ANSI Colors
|
|
348
|
+
|
|
349
|
+
The ASCII output includes ANSI color codes. They work in:
|
|
350
|
+
- ✅ Terminal output (pino-pretty, console)
|
|
351
|
+
- ✅ Log viewers that support ANSI (most modern ones)
|
|
352
|
+
- ⚠️ Raw JSON logs (colors are included as escape sequences)
|
|
353
|
+
|
|
354
|
+
To strip colors for cleaner JSON logs:
|
|
355
|
+
|
|
356
|
+
```typescript
|
|
357
|
+
// Simple regex to strip ANSI codes
|
|
358
|
+
function stripAnsi(str: string): string {
|
|
359
|
+
return str.replace(/\x1b\[[0-9;]*m/g, '');
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const asciiDiagram = viz.render();
|
|
363
|
+
const asciiWithoutColors = stripAnsi(asciiDiagram);
|
|
364
|
+
|
|
365
|
+
logger.info({
|
|
366
|
+
visualization: asciiWithoutColors, // Clean box-drawing, no ANSI codes
|
|
367
|
+
}, 'Workflow completed');
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
**Note:** The ASCII output includes ANSI color codes. They render correctly in terminals and log viewers that support ANSI (like `pino-pretty`), but appear as escape sequences in raw JSON logs. Use `stripAnsi()` if you want clean box-drawing characters without color codes.
|
|
371
|
+
|
|
372
|
+
## Example Output
|
|
373
|
+
|
|
374
|
+
```json
|
|
375
|
+
{
|
|
376
|
+
"level": 30,
|
|
377
|
+
"time": 1704067200000,
|
|
378
|
+
"workflowId": "checkout-abc-123",
|
|
379
|
+
"duration": 1234,
|
|
380
|
+
"stepCount": 5,
|
|
381
|
+
"cacheHits": 2,
|
|
382
|
+
"context": {
|
|
383
|
+
"requestId": "req-456",
|
|
384
|
+
"userId": "user-789",
|
|
385
|
+
"orderId": "order-123"
|
|
386
|
+
},
|
|
387
|
+
"msg": "Workflow completed"
|
|
388
|
+
}
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
The format looks great in Pino because:
|
|
392
|
+
- All fields are at the top level (easy to query/filter)
|
|
393
|
+
- Context is nested but accessible
|
|
394
|
+
- Duration and counts are numeric (good for metrics)
|
|
395
|
+
- Error objects serialize cleanly
|
|
396
|
+
- ASCII visualization can be included as a multi-line string field
|
package/docs/visualization.md
CHANGED
|
@@ -95,6 +95,41 @@ flowchart TD
|
|
|
95
95
|
|
|
96
96
|
Paste into GitHub, Notion, or any Mermaid-compatible tool.
|
|
97
97
|
|
|
98
|
+
#### Enhanced Mermaid Edges
|
|
99
|
+
|
|
100
|
+
Mermaid diagrams now show retry loops, error paths, and timeouts as visual edges (not just labels):
|
|
101
|
+
|
|
102
|
+
```mermaid
|
|
103
|
+
flowchart TD
|
|
104
|
+
A["✓ Fetch user 45ms"] --> B["✗ Process payment 201ms"]
|
|
105
|
+
A -.->|"↻ 2 retries"| A
|
|
106
|
+
B -->|error| ERR{{PAYMENT_FAILED}}
|
|
107
|
+
B -.->|timeout| TO{{⏱ Timeout 5000ms}}
|
|
108
|
+
style ERR fill:#fee2e2,stroke:#dc2626
|
|
109
|
+
style TO fill:#fef3c7,stroke:#f59e0b
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**What the edges show:**
|
|
113
|
+
|
|
114
|
+
- **Retry loops** (`-.->`) - Self-loop showing how many retries occurred
|
|
115
|
+
- **Error paths** (`-->|error|`) - Flow to error node showing the error value
|
|
116
|
+
- **Timeout paths** (`-.->|timeout|`) - Dashed edge to timeout node
|
|
117
|
+
|
|
118
|
+
These are enabled by default. To disable, pass options when rendering:
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
import type { MermaidRenderOptions } from '@jagreehal/workflow/visualize';
|
|
122
|
+
|
|
123
|
+
const options: MermaidRenderOptions = {
|
|
124
|
+
showTimings: true,
|
|
125
|
+
showKeys: false,
|
|
126
|
+
colors: defaultColorScheme,
|
|
127
|
+
showRetryEdges: false, // Hide retry self-loops
|
|
128
|
+
showErrorEdges: false, // Hide error path edges
|
|
129
|
+
showTimeoutEdges: false, // Hide timeout path edges
|
|
130
|
+
};
|
|
131
|
+
```
|
|
132
|
+
|
|
98
133
|
### JSON (IR)
|
|
99
134
|
|
|
100
135
|
```typescript
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jagreehal/workflow",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.15.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Typed async workflows with automatic error inference. Build type-safe workflows with Result types, step caching, resume state, and human-in-the-loop support.",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -100,6 +100,7 @@
|
|
|
100
100
|
"@total-typescript/tsconfig": "^1.0.4",
|
|
101
101
|
"@types/node": "^25.0.9",
|
|
102
102
|
"@types/picomatch": "^4.0.2",
|
|
103
|
+
"@types/ws": "^8.18.1",
|
|
103
104
|
"@typescript-eslint/eslint-plugin": "^8.53.0",
|
|
104
105
|
"@typescript-eslint/parser": "^8.53.0",
|
|
105
106
|
"@typescript-eslint/rule-tester": "^8.53.0",
|
|
@@ -108,11 +109,15 @@
|
|
|
108
109
|
"eslint": "9.39.2",
|
|
109
110
|
"eslint-config-prettier": "^10.1.8",
|
|
110
111
|
"eslint-plugin-unicorn": "^62.0.0",
|
|
112
|
+
"jsdom": "^27.4.0",
|
|
113
|
+
"mermaid": "^11.12.2",
|
|
114
|
+
"pino": "^10.2.0",
|
|
111
115
|
"tsd": "^0.33.0",
|
|
112
116
|
"tsup": "^8.5.1",
|
|
113
117
|
"typescript": "^5.9.3",
|
|
114
118
|
"typescript-eslint": "^8.53.0",
|
|
115
|
-
"vitest": "^4.0.17"
|
|
119
|
+
"vitest": "^4.0.17",
|
|
120
|
+
"ws": "^8.19.0"
|
|
116
121
|
},
|
|
117
122
|
"engines": {
|
|
118
123
|
"node": ">=18.0.0"
|