@maestro-js/agent-skills 1.0.0-alpha.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.
@@ -0,0 +1,391 @@
1
+ ---
2
+ name: db
3
+ description: "Use when working with @maestro-js/db, the database access package for Maestro. Covers MySQL driver setup, parameterized SQL queries (select, insert, update, delete, scalar, statement), transactions with automatic commit/rollback, streaming results via selectIterable, connection pooling, query formatting, and the provider pattern for multiple database instances. Maestro uses raw parameterized SQL with no ORM. Depends on service-registry and log. Used by queue for message storage. Works with the sql package for query builders."
4
+ ---
5
+
6
+ # @maestro-js/db
7
+
8
+ ## Overview
9
+
10
+ The `@maestro-js/db` package provides typed database access through the Maestro provider pattern. It wraps `mysql2` connection pools with parameterized query methods, automatic transactions, streaming result sets with backpressure, and structured logging. All queries use raw parameterized SQL -- there is no ORM.
11
+
12
+ ## Quick Setup
13
+
14
+ ```ts
15
+ import { Db } from '@maestro-js/db'
16
+ import { Log } from '@maestro-js/log'
17
+
18
+ Db.Provider.register('default', Db.Provider.create({
19
+ driver: Db.drivers.mysql({
20
+ host: process.env.DB_HOST!,
21
+ user: process.env.DB_USER!,
22
+ password: process.env.DB_PASSWORD!,
23
+ database: process.env.DB_DATABASE!,
24
+ port: Number(process.env.DB_PORT ?? 3306)
25
+ })
26
+ }))
27
+
28
+ // Query immediately
29
+ const users = await Db.select<User>('SELECT * FROM users WHERE active = ?', [true])
30
+ ```
31
+
32
+ ## Driver: MySQL (mysql2)
33
+
34
+ The only built-in driver. Created via `Db.drivers.mysql(config)`.
35
+
36
+ ### Configuration
37
+
38
+ ```ts
39
+ Db.drivers.mysql({
40
+ // Required
41
+ host: 'localhost',
42
+ user: 'root',
43
+ password: '', // empty string is allowed, but the field is required
44
+ database: 'myapp',
45
+ port: 3306,
46
+
47
+ // Optional
48
+ connectionLimit: 10, // max pool connections (default: 10)
49
+ debug: false,
50
+ multipleStatements: false,
51
+ ssl: undefined, // string or tls.SecureContextOptions
52
+
53
+ // Optional loggers (default: Log.create())
54
+ connectionLogger: Log.create(), // logs connection lifecycle events
55
+ queryLogger: Log.create() // logs query start/complete/fail and transaction events
56
+ })
57
+ ```
58
+
59
+ ### Type Casting
60
+
61
+ The MySQL driver automatically casts:
62
+ - `DATE` columns to ISO date strings
63
+ - `TIME` columns to `iso-fns` time objects
64
+ - `DATETIME` columns to ISO 8601 strings (UTC)
65
+ - `TINYINT(1)` columns to booleans (`true`/`false`), with `null` preserved for nullable columns
66
+ - JSON columns returned as strings (`jsonStrings: true`)
67
+ - Decimal columns returned as numbers (`decimalNumbers: true`)
68
+
69
+ ### Connection Behavior
70
+
71
+ - Pool uses `utf8mb4_0900_ai_ci` charset (MySQL 8.0+ default)
72
+ - Connect timeout: 60 minutes
73
+ - Connections are acquired with retry (up to 10 attempts, 1s delay)
74
+ - Deadlock and lock-timeout errors are retried automatically (up to 3 attempts) for non-transaction queries
75
+
76
+ ## API Reference
77
+
78
+ ### select
79
+
80
+ Execute a SELECT query and return typed results as an array.
81
+
82
+ ```ts
83
+ select<T = Record<string, unknown>>(query: string, parameters?: any[]): Promise<T[]>
84
+ ```
85
+
86
+ ```ts
87
+ interface User { id: number; name: string; active: boolean }
88
+
89
+ const admins = await Db.select<User>('SELECT * FROM users WHERE role = ?', ['admin'])
90
+ // [{ id: 1, name: 'Alice', active: true }, { id: 2, name: 'Bob', active: true }]
91
+ ```
92
+
93
+ ### selectIterable
94
+
95
+ Execute a SELECT query and return a streaming async iterable with backpressure support. Uses `Helpers.AsyncIterableCollection` which provides `map`, `filter`, and `toArray` chainable methods.
96
+
97
+ ```ts
98
+ selectIterable<T = Record<string, unknown>>(query: string, parameters?: any[]): Helpers.AsyncIterableCollection<T>
99
+ ```
100
+
101
+ ```ts
102
+ const orders = Db.selectIterable<Order>('SELECT * FROM orders WHERE status = ?', ['pending'])
103
+
104
+ for await (const order of orders) {
105
+ await processOrder(order)
106
+ }
107
+
108
+ // Or with chaining
109
+ const names = await Db.selectIterable<User>('SELECT * FROM users')
110
+ .filter(user => user.active)
111
+ .map(user => user.name)
112
+ .toArray()
113
+ ```
114
+
115
+ Backpressure: the driver pauses the MySQL connection when the internal row queue exceeds 3000 rows and resumes when it drains below that threshold.
116
+
117
+ ### insert
118
+
119
+ Execute an INSERT query and return the affected row count and auto-increment ID.
120
+
121
+ ```ts
122
+ insert(query: string, parameters?: any[]): Promise<{ affectedRows: number; insertId: number }>
123
+ ```
124
+
125
+ ```ts
126
+ const { insertId } = await Db.insert(
127
+ 'INSERT INTO users (name, email) VALUES (?, ?)',
128
+ ['Alice', 'alice@example.com']
129
+ )
130
+ // insertId: 42
131
+ ```
132
+
133
+ ### update
134
+
135
+ Execute an UPDATE query and return matched and changed row counts.
136
+
137
+ ```ts
138
+ update(query: string, parameters?: any[]): Promise<{ affectedRows: number; changedRows: number }>
139
+ ```
140
+
141
+ ```ts
142
+ const { changedRows } = await Db.update(
143
+ 'UPDATE users SET active = ? WHERE last_login < ?',
144
+ [false, '2024-01-01']
145
+ )
146
+ // changedRows: 15
147
+ ```
148
+
149
+ ### delete
150
+
151
+ Execute a DELETE query and return the number of removed rows.
152
+
153
+ ```ts
154
+ delete(query: string, parameters?: any[]): Promise<{ affectedRows: number }>
155
+ ```
156
+
157
+ ```ts
158
+ const { affectedRows } = await Db.delete(
159
+ 'DELETE FROM sessions WHERE expires_at < ?',
160
+ [new Date().toISOString()]
161
+ )
162
+ ```
163
+
164
+ ### scalar
165
+
166
+ Return the first column of the first row, or `null` if no results. Returns `null` both when the query returns zero rows and when the value itself is SQL NULL.
167
+
168
+ ```ts
169
+ scalar<T = unknown>(query: string, parameters?: any[]): Promise<T | null>
170
+ ```
171
+
172
+ ```ts
173
+ const count = await Db.scalar<number>('SELECT COUNT(*) FROM users WHERE active = ?', [true])
174
+ // 42
175
+
176
+ const name = await Db.scalar<string>('SELECT name FROM users WHERE id = ?', [999])
177
+ // null (no matching row)
178
+ ```
179
+
180
+ ### statement
181
+
182
+ Execute an arbitrary SQL statement and return the raw result. Use for DDL or other statements that do not fit the typed methods.
183
+
184
+ ```ts
185
+ statement<T = unknown>(query: string, parameters?: any[]): Promise<T>
186
+ ```
187
+
188
+ ```ts
189
+ await Db.statement('CREATE INDEX idx_users_email ON users (email)')
190
+ ```
191
+
192
+ ### transaction
193
+
194
+ Wrap a callback in BEGIN/COMMIT/ROLLBACK. All `Db` calls awaited inside the callback share the same connection via `AsyncLocalStorage`. Nested transaction calls reuse the outer transaction (no savepoints).
195
+
196
+ ```ts
197
+ transaction<T>(callback: () => Promise<T>): Promise<T>
198
+ ```
199
+
200
+ ```ts
201
+ await Db.transaction(async () => {
202
+ await Db.update('UPDATE accounts SET balance = balance - ? WHERE id = ?', [100, fromId])
203
+ await Db.update('UPDATE accounts SET balance = balance + ? WHERE id = ?', [100, toId])
204
+ })
205
+ // Both updates commit together; if either throws, both roll back
206
+ ```
207
+
208
+ ### format
209
+
210
+ Interpolate parameters into a query string for debugging. Does not execute the query.
211
+
212
+ ```ts
213
+ format(query: string, params?: any[]): string
214
+ ```
215
+
216
+ ```ts
217
+ const sql = Db.format('SELECT * FROM users WHERE id = ?', [42])
218
+ // "SELECT * FROM users WHERE id = 42"
219
+ ```
220
+
221
+ ### destroy
222
+
223
+ Close all connections in the pool and prevent new ones from being created.
224
+
225
+ ```ts
226
+ destroy(): Promise<void>
227
+ ```
228
+
229
+ ## Common Patterns
230
+
231
+ ### Parameterized Queries
232
+
233
+ Always use `?` placeholders. Never interpolate values into SQL strings.
234
+
235
+ ```ts
236
+ // Correct
237
+ const users = await Db.select<User>('SELECT * FROM users WHERE name = ?', ['Alice'])
238
+
239
+ // WRONG - SQL injection risk
240
+ const users = await Db.select<User>(`SELECT * FROM users WHERE name = '${name}'`)
241
+ ```
242
+
243
+ ### Avoid Table Name Aliases
244
+
245
+ Write full table names in SQL queries instead of aliases when possible.
246
+
247
+ ```ts
248
+ // Preferred
249
+ await Db.select('SELECT users.name, orders.total FROM users JOIN orders ON orders.user_id = users.id')
250
+
251
+ // Avoid
252
+ await Db.select('SELECT u.name, o.total FROM users u JOIN orders o ON o.user_id = u.id')
253
+ ```
254
+
255
+ ### Transactions with Error Handling
256
+
257
+ ```ts
258
+ try {
259
+ const orderId = await Db.transaction(async () => {
260
+ const { insertId } = await Db.insert(
261
+ 'INSERT INTO orders (user_id, total) VALUES (?, ?)',
262
+ [userId, total]
263
+ )
264
+ await Db.insert(
265
+ 'INSERT INTO order_items (order_id, product_id, quantity) VALUES (?, ?, ?)',
266
+ [insertId, productId, quantity]
267
+ )
268
+ await Db.update(
269
+ 'UPDATE products SET stock = stock - ? WHERE id = ?',
270
+ [quantity, productId]
271
+ )
272
+ return insertId
273
+ })
274
+ } catch (e) {
275
+ // All three statements rolled back
276
+ }
277
+ ```
278
+
279
+ ### Streaming Large Result Sets
280
+
281
+ Use `selectIterable` for queries returning many rows to avoid loading everything into memory.
282
+
283
+ ```ts
284
+ const rows = Db.selectIterable<AuditLog>('SELECT * FROM audit_logs WHERE created_at > ?', ['2024-01-01'])
285
+
286
+ let count = 0
287
+ for await (const row of rows) {
288
+ await archiveToS3(row)
289
+ count++
290
+ }
291
+ ```
292
+
293
+ ### Multiple Providers
294
+
295
+ Register named providers for separate databases.
296
+
297
+ ```ts
298
+ Db.Provider.register('default', Db.Provider.create({
299
+ driver: Db.drivers.mysql({ host: 'primary-db', user: 'app', password: '', database: 'myapp', port: 3306 })
300
+ }))
301
+
302
+ Db.Provider.register('analytics', Db.Provider.create({
303
+ driver: Db.drivers.mysql({ host: 'analytics-db', user: 'app', password: '', database: 'analytics', port: 3306 })
304
+ }))
305
+
306
+ // Use the default provider
307
+ const users = await Db.select<User>('SELECT * FROM users')
308
+
309
+ // Use a named provider
310
+ const events = await Db.provider('analytics').select<Event>('SELECT * FROM events')
311
+ ```
312
+
313
+ ## Query Logging
314
+
315
+ The MySQL driver emits structured log events via the `queryLogger` and `connectionLogger` options.
316
+
317
+ Query logger events: `queryStarted`, `queryCompleted`, `queryFailed`, `transactionStarted`, `transactionCompleted`, `transactionFailed`. Each event includes the query ID, formatted query, duration (on completion/failure), and transaction ID when applicable.
318
+
319
+ Connection logger events: `newConnection`, `connectionAcquisitionStarted`, `connectionAcquisitionCompleted`, `connectionAcquisitionFailed`.
320
+
321
+ The log message type is exported as `Db.drivers.mysql.MysqlDbDriverLogMessageData`.
322
+
323
+ ## Testing
324
+
325
+ The `@maestro-js/db` package does not currently have a mock driver or built-in tests. To test code that uses `Db`:
326
+
327
+ 1. **Use a real test database** -- spin up a MySQL instance (e.g., via Docker) and register a provider pointing to it.
328
+ 2. **Wrap tests in transactions** -- roll back after each test to keep the database clean.
329
+ 3. **Abstract database calls** -- write functions that accept a `Db.DbService` so you can substitute a test double.
330
+
331
+ ```ts
332
+ // Test setup with a real database
333
+ import { Db } from '@maestro-js/db'
334
+
335
+ Db.Provider.register('default', Db.Provider.create({
336
+ driver: Db.drivers.mysql({
337
+ host: 'localhost',
338
+ user: 'test',
339
+ password: '',
340
+ database: 'myapp_test',
341
+ port: 3306
342
+ })
343
+ }))
344
+
345
+ // Clean up after tests
346
+ afterAll(async () => {
347
+ await Db.destroy()
348
+ })
349
+ ```
350
+
351
+ ## Cross-Package Integration
352
+
353
+ ### Depends On
354
+
355
+ - **@maestro-js/service-registry** -- provider pattern (create, register, resolve)
356
+ - **@maestro-js/log** -- structured logging for queries and connections
357
+ - **@maestro-js/context** -- `AsyncLocalStorage`-based request context for scoping log data
358
+ - **@maestro-js/helpers** -- retry logic, `AsyncIterableCollection` for streaming
359
+
360
+ ### Used By
361
+
362
+ - **@maestro-js/queue** -- stores queue messages and job state in MySQL tables via `Db`
363
+
364
+ ### Works With
365
+
366
+ - **@maestro-js/sql** -- provides query builder utilities (`Sql.where`, CTEs) that produce parameterized SQL strings compatible with `Db.select`, `Db.update`, etc.
367
+
368
+ ```ts
369
+ import { Db } from '@maestro-js/db'
370
+ import { Sql } from '@maestro-js/sql'
371
+
372
+ const where = Sql.where({ active: true, role: 'admin' })
373
+ const users = await Db.select<User>(`SELECT * FROM users ${where.sql}`, where.params)
374
+ ```
375
+
376
+ ## DbDriver Interface
377
+
378
+ Implement this interface to create a custom driver.
379
+
380
+ ```ts
381
+ interface DbDriver {
382
+ selectIterable(query: string, parameters?: any[]): AsyncIterable<Record<string, unknown>>
383
+ execute(
384
+ query: string,
385
+ parameters?: any[]
386
+ ): Promise<Record<string, unknown>[] | { affectedRows: number; changedRows: number; insertId: number }>
387
+ format(query: string, parameters: any[]): string
388
+ transaction<T>(callback: () => Promise<T>): Promise<T>
389
+ destroy(): Promise<void>
390
+ }
391
+ ```
@@ -0,0 +1,295 @@
1
+ ---
2
+ name: events
3
+ description: "Guide for @maestro-js/events — a pub/sub messaging package with pluggable drivers (Redis, WebSocket, Local). Use when working with @maestro-js/events, adding event-driven communication, creating typed event handles, configuring Redis pub/sub, setting up IPC via WebSocket, using in-process local events, or integrating with the Broadcast package. Covers the Provider pattern, driver configuration, typed Event handles, listener lifecycle, and cross-package usage with service-registry and broadcast."
4
+ ---
5
+
6
+ # @maestro-js/events
7
+
8
+ Pub/sub messaging layer with pluggable drivers for Redis (cross-server), WebSocket (IPC between Node processes), and Local (in-process). Follows the standard Maestro Provider pattern built on `@maestro-js/service-registry`.
9
+
10
+ ## Quick Setup
11
+
12
+ ```ts
13
+ import { Events } from '@maestro-js/events'
14
+
15
+ // Create and register with a driver
16
+ const service = Events.Provider.create({ driver: Events.drivers.local() })
17
+ Events.Provider.register('default', service)
18
+
19
+ // Use via the default facade
20
+ Events.emit('order-placed', { orderId: 'order-789', total: 59.99 })
21
+ ```
22
+
23
+ ## Drivers
24
+
25
+ ### Local (In-Process)
26
+
27
+ Use `Events.drivers.local()` for in-process events. Returns a Node.js `EventEmitter`. Best for testing and single-process applications.
28
+
29
+ ```ts
30
+ const service = Events.Provider.create({
31
+ driver: Events.drivers.local()
32
+ })
33
+ Events.Provider.register('default', service)
34
+ ```
35
+
36
+ ### Redis (Cross-Server)
37
+
38
+ Use `Events.drivers.redis(config)` for cross-server pub/sub via Redis. Creates separate publisher and subscriber connections with lazy initialization. Messages are JSON-serialized over Redis channels prefixed with `maestro.events.`.
39
+
40
+ ```ts
41
+ const service = Events.Provider.create({
42
+ driver: Events.drivers.redis({
43
+ url: 'redis-host.example.com',
44
+ port: 6379,
45
+ operationTimeout: 5000, // optional, default 5000ms
46
+ connectTimeout: 10000, // optional, default 10000ms
47
+ keyPrefix: 'myapp:', // optional, default ''
48
+ logger: customLogger // optional, defaults to Log.create()
49
+ })
50
+ })
51
+ Events.Provider.register('default', service)
52
+ ```
53
+
54
+ Redis driver config interface:
55
+
56
+ | Option | Type | Default | Description |
57
+ |--------|------|---------|-------------|
58
+ | `url` | `string` | required | Redis host URL |
59
+ | `port` | `number` | required | Redis port |
60
+ | `operationTimeout` | `number` | `5000` | Timeout for publish operations (ms) |
61
+ | `connectTimeout` | `number` | `10000` | Connection timeout (ms) |
62
+ | `keyPrefix` | `string` | `''` | Prefix prepended to all channel names |
63
+ | `logger` | `Log.LogFunctions` | `Log.create()` | Logger instance for connection/publish/subscribe logs |
64
+
65
+ Redis channel naming: channels are formatted as `${keyPrefix}maestro.events.${eventName}`.
66
+
67
+ ### WebSocket (IPC Between Processes)
68
+
69
+ Use `Events.drivers.socket(config)` for IPC between Node.js processes via TCP sockets. Automatically creates a server if none exists and connects as a client. Reconnects on socket close.
70
+
71
+ ```ts
72
+ const service = Events.Provider.create({
73
+ driver: Events.drivers.socket({ port: 8444 })
74
+ })
75
+ Events.Provider.register('default', service)
76
+ ```
77
+
78
+ The socket driver serializes messages as newline-delimited JSON (`{ eventName, data }\n`). One process acts as the server (created automatically if the port is available), and all processes connect as clients.
79
+
80
+ ## API Reference
81
+
82
+ ### Provider Pattern
83
+
84
+ ```ts
85
+ // Create a service instance with a driver
86
+ Events.Provider.create(config: { driver: Events.EventsDriver }): Events.EventsService
87
+
88
+ // Register under a named key
89
+ Events.Provider.register(name: string, service: Events.EventsService): void
90
+
91
+ // Resolve a named instance (deferred via Proxy)
92
+ Events.provider(key: string): Events.EventsService
93
+ ```
94
+
95
+ ### Direct API (Untyped)
96
+
97
+ Operate on the default facade without typed handles.
98
+
99
+ ```ts
100
+ // Emit an event with a payload
101
+ Events.emit(name: string, message: Record<string, any>): void
102
+
103
+ // Subscribe to an event, returns a handle with remove()
104
+ Events.addListener<T>(event: string, listener: (message: T) => unknown): { remove(): void }
105
+
106
+ // Remove a specific listener by reference
107
+ Events.removeListener(event: string, listener: (message: Record<string, any>) => unknown): void
108
+
109
+ // Remove all listeners and tear down driver connections
110
+ Events.removeAllListeners(): void
111
+ ```
112
+
113
+ ### Typed Event Handles
114
+
115
+ Create a typed event scoped to a single name. All methods enforce the payload type `T` at compile time.
116
+
117
+ ```ts
118
+ // Create a typed event handle
119
+ Events.create<T extends Record<string, any>>(eventName: string): Events.Event<T>
120
+ ```
121
+
122
+ The returned `Event<T>` interface:
123
+
124
+ ```ts
125
+ interface Event<T extends Record<string, any>> {
126
+ emit(message: T): void
127
+ addListener(listener: (message: T) => unknown): { remove(): void }
128
+ removeListener(listener: (message: T) => unknown): void
129
+ }
130
+ ```
131
+
132
+ ### EventsDriver Interface
133
+
134
+ All drivers implement this interface:
135
+
136
+ ```ts
137
+ type EventsDriver = {
138
+ addListener(event: string, listener: (message: Record<string, any>) => void): unknown
139
+ removeListener(event: string, listener: (message: Record<string, any>) => void): unknown
140
+ emit(name: string, message: Record<string, any>): unknown
141
+ removeAllListeners(): unknown
142
+ }
143
+ ```
144
+
145
+ ### Type Augmentation for Provider Keys
146
+
147
+ Extend provider keys for type-safe multi-instance resolution:
148
+
149
+ ```ts
150
+ declare namespace Events {
151
+ namespace Provider {
152
+ interface Keys {
153
+ default: unknown
154
+ analytics: unknown
155
+ }
156
+ }
157
+ }
158
+ ```
159
+
160
+ ## Common Patterns
161
+
162
+ ### Typed Event Handles
163
+
164
+ Define typed events for compile-time payload safety:
165
+
166
+ ```ts
167
+ const orderPlaced = Events.create<{ orderId: string; total: number }>('order-placed')
168
+
169
+ const sub = orderPlaced.addListener((msg) => {
170
+ console.log(`Order ${msg.orderId} placed for $${msg.total}`)
171
+ })
172
+
173
+ orderPlaced.emit({ orderId: 'order-1', total: 42.5 })
174
+
175
+ // Unsubscribe
176
+ sub.remove()
177
+ ```
178
+
179
+ ### Listener Removal by Reference
180
+
181
+ ```ts
182
+ const listener = (msg: Record<string, any>) => console.log(msg)
183
+ Events.addListener('my-event', listener)
184
+
185
+ // Later, remove by reference
186
+ Events.removeListener('my-event', listener)
187
+ ```
188
+
189
+ ### Multiple Listeners on One Event
190
+
191
+ ```ts
192
+ Events.addListener('user-signup', (msg) => sendWelcomeEmail(msg))
193
+ Events.addListener('user-signup', (msg) => trackAnalytics(msg))
194
+
195
+ Events.emit('user-signup', { userId: 'u-123', email: 'alice@example.com' })
196
+ // Both listeners fire
197
+ ```
198
+
199
+ ### Multiple Named Instances
200
+
201
+ ```ts
202
+ const redisEvents = Events.Provider.create({
203
+ driver: Events.drivers.redis({ url: 'redis-host', port: 6379 })
204
+ })
205
+ Events.Provider.register('default', redisEvents)
206
+
207
+ const localEvents = Events.Provider.create({
208
+ driver: Events.drivers.local()
209
+ })
210
+ Events.Provider.register('internal', localEvents)
211
+
212
+ // Use each instance
213
+ Events.provider('default').emit('cross-server-event', { data: 1 })
214
+ Events.provider('internal').emit('local-only-event', { data: 2 })
215
+ ```
216
+
217
+ ### Cleanup on Shutdown
218
+
219
+ ```ts
220
+ // Tears down driver connections (Redis disconnect, socket close) and removes all listeners
221
+ Events.removeAllListeners()
222
+ ```
223
+
224
+ ## Testing
225
+
226
+ Use the Local driver for tests. It is synchronous and requires no external services.
227
+
228
+ ```ts
229
+ import { Events } from '@maestro-js/events'
230
+ import { test } from 'beartest-js'
231
+ import { expect } from 'expect'
232
+
233
+ const service = Events.Provider.create({ driver: Events.drivers.local() })
234
+
235
+ test('emit + addListener fires listener with correct payload', () => {
236
+ const received: any[] = []
237
+ service.addListener('test-event', (msg) => received.push(msg))
238
+ service.emit('test-event', { hello: 'world' })
239
+ expect(received).toEqual([{ hello: 'world' }])
240
+ service.removeAllListeners()
241
+ })
242
+
243
+ test('typed events via create()', () => {
244
+ const orderPlaced = service.create<{ orderId: string; total: number }>('order-placed')
245
+ const received: any[] = []
246
+ orderPlaced.addListener((msg) => received.push(msg))
247
+ orderPlaced.emit({ orderId: 'order-1', total: 42.5 })
248
+ expect(received).toEqual([{ orderId: 'order-1', total: 42.5 }])
249
+ service.removeAllListeners()
250
+ })
251
+ ```
252
+
253
+ Always call `service.removeAllListeners()` after each test to prevent listener leaks.
254
+
255
+ Run tests:
256
+
257
+ ```bash
258
+ pnpm --filter @maestro-js/events test
259
+ cd packages/events && npx beartest ./tests/events.test.ts
260
+ ```
261
+
262
+ ## Cross-Package Integration
263
+
264
+ ### Dependencies
265
+
266
+ - **@maestro-js/service-registry** -- foundation for the Provider pattern (create, register, resolve)
267
+ - **@maestro-js/log** -- used by the Redis driver for structured logging of connections, publishes, and subscribes
268
+
269
+ ### Used By: Broadcast
270
+
271
+ The `@maestro-js/broadcast` package accepts an Events service instance as its driver. The Events service satisfies the `BroadcastingDriver` interface (`emit`, `addListener`, `removeListener`, `removeAllListeners`).
272
+
273
+ ```ts
274
+ import { Events } from '@maestro-js/events'
275
+ import { Broadcast } from '@maestro-js/broadcast'
276
+
277
+ const users = Broadcast.channel('/user/:id').create<{ name: string }>()
278
+
279
+ const broadcast = Broadcast.Provider.create({
280
+ channels: [users],
281
+ driver: Events // Events facade satisfies BroadcastingDriver
282
+ })
283
+ Broadcast.Provider.register('default', broadcast)
284
+ ```
285
+
286
+ ## Source Files
287
+
288
+ | File | Purpose |
289
+ |------|---------|
290
+ | `packages/events/src/index.ts` | Main export, Provider pattern, facade, driver registry |
291
+ | `packages/events/src/events-types.ts` | `EventsDriver` interface definition |
292
+ | `packages/events/src/redis-events-driver.ts` | Redis pub/sub driver using ioredis |
293
+ | `packages/events/src/sockets-events-driver.ts` | TCP socket IPC driver for inter-process events |
294
+ | `packages/events/tests/events.test.ts` | Unit tests using Local driver |
295
+ | `packages/events/tests/socket-events-driver.test.ts` | Socket driver integration test (skipped by default) |