ai-workflows 2.1.1 → 2.1.3

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 (78) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-test.log +165 -3
  3. package/CHANGELOG.md +10 -1
  4. package/LICENSE +21 -0
  5. package/README.md +303 -184
  6. package/dist/barrier.d.ts +153 -0
  7. package/dist/barrier.d.ts.map +1 -0
  8. package/dist/barrier.js +339 -0
  9. package/dist/barrier.js.map +1 -0
  10. package/dist/cascade-context.d.ts +149 -0
  11. package/dist/cascade-context.d.ts.map +1 -0
  12. package/dist/cascade-context.js +324 -0
  13. package/dist/cascade-context.js.map +1 -0
  14. package/dist/cascade-executor.d.ts +196 -0
  15. package/dist/cascade-executor.d.ts.map +1 -0
  16. package/dist/cascade-executor.js +384 -0
  17. package/dist/cascade-executor.js.map +1 -0
  18. package/dist/context.d.ts.map +1 -1
  19. package/dist/context.js +4 -1
  20. package/dist/context.js.map +1 -1
  21. package/dist/dependency-graph.d.ts +157 -0
  22. package/dist/dependency-graph.d.ts.map +1 -0
  23. package/dist/dependency-graph.js +382 -0
  24. package/dist/dependency-graph.js.map +1 -0
  25. package/dist/every.d.ts +31 -2
  26. package/dist/every.d.ts.map +1 -1
  27. package/dist/every.js +63 -32
  28. package/dist/every.js.map +1 -1
  29. package/dist/graph/index.d.ts +8 -0
  30. package/dist/graph/index.d.ts.map +1 -0
  31. package/dist/graph/index.js +8 -0
  32. package/dist/graph/index.js.map +1 -0
  33. package/dist/graph/topological-sort.d.ts +121 -0
  34. package/dist/graph/topological-sort.d.ts.map +1 -0
  35. package/dist/graph/topological-sort.js +292 -0
  36. package/dist/graph/topological-sort.js.map +1 -0
  37. package/dist/index.d.ts +6 -1
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +10 -0
  40. package/dist/index.js.map +1 -1
  41. package/dist/on.d.ts +35 -10
  42. package/dist/on.d.ts.map +1 -1
  43. package/dist/on.js +52 -18
  44. package/dist/on.js.map +1 -1
  45. package/dist/timer-registry.d.ts +52 -0
  46. package/dist/timer-registry.d.ts.map +1 -0
  47. package/dist/timer-registry.js +120 -0
  48. package/dist/timer-registry.js.map +1 -0
  49. package/dist/types.d.ts +88 -0
  50. package/dist/types.d.ts.map +1 -1
  51. package/dist/types.js +17 -1
  52. package/dist/types.js.map +1 -1
  53. package/dist/workflow.d.ts.map +1 -1
  54. package/dist/workflow.js +15 -11
  55. package/dist/workflow.js.map +1 -1
  56. package/package.json +11 -11
  57. package/src/barrier.ts +466 -0
  58. package/src/cascade-context.ts +488 -0
  59. package/src/cascade-executor.ts +587 -0
  60. package/src/context.ts +12 -7
  61. package/src/dependency-graph.ts +518 -0
  62. package/src/every.ts +104 -35
  63. package/src/graph/index.ts +19 -0
  64. package/src/graph/topological-sort.ts +414 -0
  65. package/src/index.ts +78 -0
  66. package/src/on.ts +81 -25
  67. package/src/timer-registry.ts +145 -0
  68. package/src/types.ts +121 -0
  69. package/src/workflow.ts +23 -16
  70. package/test/barrier-join.test.ts +434 -0
  71. package/test/barrier-unhandled-rejections.test.ts +359 -0
  72. package/test/cascade-context.test.ts +390 -0
  73. package/test/cascade-executor.test.ts +859 -0
  74. package/test/dependency-graph.test.ts +512 -0
  75. package/test/graph/topological-sort.test.ts +586 -0
  76. package/test/schedule-timer-cleanup.test.ts +344 -0
  77. package/test/send-race-conditions.test.ts +410 -0
  78. package/test/type-safety-every.test.ts +303 -0
@@ -0,0 +1,414 @@
1
+ /**
2
+ * Topological Sort Implementation for Workflow Execution Ordering
3
+ *
4
+ * Provides multiple algorithms for topologically sorting workflow steps:
5
+ * - Kahn's algorithm (BFS-based, good for detecting cycles)
6
+ * - DFS-based algorithm (classic approach, provides cycle path)
7
+ *
8
+ * Features:
9
+ * - Cycle detection with path reporting
10
+ * - Parallel execution level grouping
11
+ * - Stable, deterministic ordering
12
+ * - Support for missing dependency handling
13
+ */
14
+
15
+ /**
16
+ * A node that can be topologically sorted
17
+ */
18
+ export interface SortableNode {
19
+ /** Unique identifier for the node */
20
+ id: string
21
+ /** IDs of nodes this node depends on */
22
+ dependencies: string[]
23
+ }
24
+
25
+ /**
26
+ * Execution level containing nodes that can run in parallel
27
+ */
28
+ export interface ExecutionLevel {
29
+ /** Level number (0 = no dependencies, 1 = depends on level 0, etc.) */
30
+ level: number
31
+ /** Node IDs that can run concurrently at this level */
32
+ nodes: string[]
33
+ }
34
+
35
+ /**
36
+ * Options for topological sort
37
+ */
38
+ export interface TopologicalSortOptions {
39
+ /** Throw CycleDetectedError instead of returning hasCycle: true (default: false) */
40
+ throwOnCycle?: boolean
41
+ /** Which algorithm to use (default: 'kahn') */
42
+ algorithm?: 'kahn' | 'dfs'
43
+ /** Throw on missing dependencies (default: false) */
44
+ strict?: boolean
45
+ }
46
+
47
+ /**
48
+ * Result of topological sort operation
49
+ */
50
+ export interface TopologicalSortResult {
51
+ /** Sorted node IDs in execution order */
52
+ order: string[]
53
+ /** Whether a cycle was detected */
54
+ hasCycle: boolean
55
+ /** Path of nodes forming the cycle (if detected) */
56
+ cyclePath?: string[]
57
+ /** Additional metadata from the algorithm */
58
+ metadata?: {
59
+ /** In-degrees for each node (Kahn's algorithm) */
60
+ inDegrees?: Record<string, number>
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Error thrown when a cycle is detected in the dependency graph
66
+ */
67
+ export class CycleDetectedError extends Error {
68
+ /** The path of nodes forming the cycle */
69
+ cyclePath: string[]
70
+
71
+ constructor(cyclePath: string[]) {
72
+ const pathStr = cyclePath.join(' -> ')
73
+ super(`Cycle detected in dependency graph: ${pathStr}`)
74
+ this.name = 'CycleDetectedError'
75
+ this.cyclePath = cyclePath
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Error thrown when a dependency references a non-existent node
81
+ */
82
+ export class MissingNodeError extends Error {
83
+ /** The missing node ID */
84
+ missingNode: string
85
+ /** The node that references the missing node */
86
+ referencedBy: string
87
+
88
+ constructor(missingNode: string, referencedBy: string) {
89
+ super(
90
+ `Missing dependency '${missingNode}' referenced by '${referencedBy}'`
91
+ )
92
+ this.name = 'MissingNodeError'
93
+ this.missingNode = missingNode
94
+ this.referencedBy = referencedBy
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Build adjacency list and compute in-degrees for Kahn's algorithm
100
+ */
101
+ function buildGraph(
102
+ nodes: SortableNode[],
103
+ strict: boolean
104
+ ): {
105
+ adjacencyList: Map<string, string[]>
106
+ inDegrees: Map<string, number>
107
+ nodeSet: Set<string>
108
+ } {
109
+ const nodeSet = new Set(nodes.map((n) => n.id))
110
+ const adjacencyList = new Map<string, string[]>()
111
+ const inDegrees = new Map<string, number>()
112
+
113
+ // Initialize all nodes
114
+ for (const node of nodes) {
115
+ adjacencyList.set(node.id, [])
116
+ inDegrees.set(node.id, 0)
117
+ }
118
+
119
+ // Build edges (dependency -> dependent)
120
+ for (const node of nodes) {
121
+ // Deduplicate dependencies
122
+ const uniqueDeps = [...new Set(node.dependencies)]
123
+
124
+ for (const dep of uniqueDeps) {
125
+ if (!nodeSet.has(dep)) {
126
+ if (strict) {
127
+ throw new MissingNodeError(dep, node.id)
128
+ }
129
+ // Skip missing dependencies in non-strict mode
130
+ continue
131
+ }
132
+
133
+ // Add edge from dependency to this node
134
+ adjacencyList.get(dep)!.push(node.id)
135
+ inDegrees.set(node.id, inDegrees.get(node.id)! + 1)
136
+ }
137
+ }
138
+
139
+ return { adjacencyList, inDegrees, nodeSet }
140
+ }
141
+
142
+ /**
143
+ * Topological sort using Kahn's algorithm (BFS-based)
144
+ *
145
+ * Algorithm:
146
+ * 1. Calculate in-degree for each node
147
+ * 2. Add all nodes with in-degree 0 to queue
148
+ * 3. While queue not empty:
149
+ * - Remove node from queue, add to result
150
+ * - Decrease in-degree of all dependents
151
+ * - Add nodes with in-degree 0 to queue
152
+ * 4. If result size < node count, cycle exists
153
+ */
154
+ export function topologicalSortKahn(
155
+ nodes: SortableNode[],
156
+ options: TopologicalSortOptions = {}
157
+ ): TopologicalSortResult {
158
+ const { strict = false } = options
159
+
160
+ if (nodes.length === 0) {
161
+ return { order: [], hasCycle: false }
162
+ }
163
+
164
+ const { adjacencyList, inDegrees, nodeSet } = buildGraph(nodes, strict)
165
+
166
+ const order: string[] = []
167
+ const inDegreesCopy = new Map(inDegrees)
168
+
169
+ // Start with nodes that have no dependencies (in-degree 0)
170
+ // Sort alphabetically for determinism
171
+ const queue: string[] = [...nodeSet]
172
+ .filter((id) => inDegreesCopy.get(id) === 0)
173
+ .sort()
174
+
175
+ while (queue.length > 0) {
176
+ // Sort queue for deterministic ordering
177
+ queue.sort()
178
+ const current = queue.shift()!
179
+ order.push(current)
180
+
181
+ // Decrease in-degree for all dependents
182
+ for (const dependent of adjacencyList.get(current) || []) {
183
+ const newDegree = inDegreesCopy.get(dependent)! - 1
184
+ inDegreesCopy.set(dependent, newDegree)
185
+
186
+ if (newDegree === 0) {
187
+ queue.push(dependent)
188
+ }
189
+ }
190
+ }
191
+
192
+ // If we didn't process all nodes, there's a cycle
193
+ const hasCycle = order.length < nodeSet.size
194
+
195
+ // Convert in-degrees to record
196
+ const inDegreesRecord: Record<string, number> = {}
197
+ for (const [id, degree] of inDegrees) {
198
+ inDegreesRecord[id] = degree
199
+ }
200
+
201
+ return {
202
+ order,
203
+ hasCycle,
204
+ metadata: {
205
+ inDegrees: inDegreesRecord,
206
+ },
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Topological sort using DFS-based algorithm
212
+ *
213
+ * Algorithm:
214
+ * 1. For each unvisited node:
215
+ * - Mark as "in progress"
216
+ * - Recursively visit all dependencies
217
+ * - Mark as "visited" and add to result (in reverse)
218
+ * 2. If we encounter a node "in progress", we found a cycle
219
+ */
220
+ export function topologicalSortDFS(
221
+ nodes: SortableNode[],
222
+ options: TopologicalSortOptions = {}
223
+ ): TopologicalSortResult {
224
+ const { strict = false } = options
225
+
226
+ if (nodes.length === 0) {
227
+ return { order: [], hasCycle: false }
228
+ }
229
+
230
+ const nodeMap = new Map(nodes.map((n) => [n.id, n]))
231
+ const nodeSet = new Set(nodes.map((n) => n.id))
232
+
233
+ const visited = new Set<string>()
234
+ const inProgress = new Set<string>()
235
+ const order: string[] = []
236
+ let cyclePath: string[] | undefined
237
+
238
+ function dfs(nodeId: string, path: string[]): boolean {
239
+ if (inProgress.has(nodeId)) {
240
+ // Found a cycle - construct the cycle path
241
+ const cycleStart = path.indexOf(nodeId)
242
+ cyclePath = [...path.slice(cycleStart), nodeId]
243
+ return true // Cycle detected
244
+ }
245
+
246
+ if (visited.has(nodeId)) {
247
+ return false // Already processed
248
+ }
249
+
250
+ inProgress.add(nodeId)
251
+ path.push(nodeId)
252
+
253
+ const node = nodeMap.get(nodeId)
254
+ if (node) {
255
+ // Deduplicate and sort dependencies for determinism
256
+ const uniqueDeps = [...new Set(node.dependencies)].sort()
257
+
258
+ for (const dep of uniqueDeps) {
259
+ if (!nodeSet.has(dep)) {
260
+ if (strict) {
261
+ throw new MissingNodeError(dep, nodeId)
262
+ }
263
+ continue // Skip missing deps in non-strict mode
264
+ }
265
+
266
+ if (dfs(dep, path)) {
267
+ return true // Propagate cycle detection
268
+ }
269
+ }
270
+ }
271
+
272
+ path.pop()
273
+ inProgress.delete(nodeId)
274
+ visited.add(nodeId)
275
+ order.push(nodeId)
276
+
277
+ return false
278
+ }
279
+
280
+ // Process nodes in sorted order for determinism
281
+ const sortedNodeIds = [...nodeSet].sort()
282
+
283
+ for (const nodeId of sortedNodeIds) {
284
+ if (!visited.has(nodeId)) {
285
+ if (dfs(nodeId, [])) {
286
+ break // Stop on first cycle
287
+ }
288
+ }
289
+ }
290
+
291
+ const hasCycle = cyclePath !== undefined
292
+
293
+ return {
294
+ order: hasCycle ? order : order, // DFS produces correct order
295
+ hasCycle,
296
+ cyclePath,
297
+ }
298
+ }
299
+
300
+ /**
301
+ * Main topological sort function with algorithm selection
302
+ *
303
+ * @param nodes - Array of nodes to sort
304
+ * @param options - Sort options
305
+ * @returns Sorted result with order and cycle information
306
+ */
307
+ export function topologicalSort(
308
+ nodes: SortableNode[],
309
+ options: TopologicalSortOptions = {}
310
+ ): TopologicalSortResult {
311
+ const { algorithm = 'dfs', throwOnCycle = false } = options
312
+
313
+ let result: TopologicalSortResult
314
+
315
+ if (algorithm === 'kahn') {
316
+ result = topologicalSortKahn(nodes, options)
317
+
318
+ // Kahn's algorithm doesn't provide cycle path, so detect it separately
319
+ if (result.hasCycle) {
320
+ const dfsResult = topologicalSortDFS(nodes, options)
321
+ result.cyclePath = dfsResult.cyclePath
322
+ }
323
+ } else {
324
+ result = topologicalSortDFS(nodes, options)
325
+ }
326
+
327
+ if (result.hasCycle && throwOnCycle) {
328
+ throw new CycleDetectedError(result.cyclePath || ['unknown'])
329
+ }
330
+
331
+ return result
332
+ }
333
+
334
+ /**
335
+ * Get execution levels for parallel execution
336
+ *
337
+ * Groups nodes by their execution level:
338
+ * - Level 0: Nodes with no dependencies
339
+ * - Level N: Nodes whose dependencies are all at level < N
340
+ *
341
+ * @param nodes - Array of nodes to analyze
342
+ * @returns Array of execution levels, sorted by level number
343
+ * @throws CycleDetectedError if a cycle is detected
344
+ */
345
+ export function getExecutionLevels(nodes: SortableNode[]): ExecutionLevel[] {
346
+ if (nodes.length === 0) {
347
+ return []
348
+ }
349
+
350
+ // First, verify no cycles exist
351
+ const sortResult = topologicalSort(nodes, { throwOnCycle: true })
352
+
353
+ const nodeMap = new Map(nodes.map((n) => [n.id, n]))
354
+ const nodeSet = new Set(nodes.map((n) => n.id))
355
+ const levels = new Map<string, number>()
356
+
357
+ // Calculate level for each node
358
+ function calculateLevel(nodeId: string): number {
359
+ if (levels.has(nodeId)) {
360
+ return levels.get(nodeId)!
361
+ }
362
+
363
+ const node = nodeMap.get(nodeId)
364
+ if (!node) {
365
+ return 0
366
+ }
367
+
368
+ // Filter to only existing dependencies
369
+ const validDeps = node.dependencies.filter((d) => nodeSet.has(d))
370
+
371
+ if (validDeps.length === 0) {
372
+ levels.set(nodeId, 0)
373
+ return 0
374
+ }
375
+
376
+ let maxDepLevel = -1
377
+ for (const dep of validDeps) {
378
+ const depLevel = calculateLevel(dep)
379
+ maxDepLevel = Math.max(maxDepLevel, depLevel)
380
+ }
381
+
382
+ const level = maxDepLevel + 1
383
+ levels.set(nodeId, level)
384
+ return level
385
+ }
386
+
387
+ // Calculate levels for all nodes
388
+ for (const node of nodes) {
389
+ calculateLevel(node.id)
390
+ }
391
+
392
+ // Group nodes by level
393
+ const levelGroups = new Map<number, string[]>()
394
+ for (const [nodeId, level] of levels) {
395
+ if (!levelGroups.has(level)) {
396
+ levelGroups.set(level, [])
397
+ }
398
+ levelGroups.get(level)!.push(nodeId)
399
+ }
400
+
401
+ // Convert to sorted array of ExecutionLevels
402
+ const result: ExecutionLevel[] = []
403
+ const sortedLevels = [...levelGroups.keys()].sort((a, b) => a - b)
404
+
405
+ for (const level of sortedLevels) {
406
+ result.push({
407
+ level,
408
+ // Sort nodes within level for determinism
409
+ nodes: levelGroups.get(level)!.sort(),
410
+ })
411
+ }
412
+
413
+ return result
414
+ }
package/src/index.ts CHANGED
@@ -84,6 +84,82 @@ export { send, getEventBus } from './send.js'
84
84
  // Context
85
85
  export { createWorkflowContext, createIsolatedContext } from './context.js'
86
86
 
87
+ // Cascade Context - Correlation IDs and step metadata
88
+ export {
89
+ createCascadeContext,
90
+ recordStep,
91
+ withCascadeContext,
92
+ type CascadeContext,
93
+ type CascadeStep,
94
+ type CascadeContextOptions,
95
+ type SerializedCascadeContext,
96
+ type SerializedCascadeStep,
97
+ type TraceContext,
98
+ type FiveWHEvent,
99
+ type StepStatus,
100
+ } from './cascade-context.js'
101
+
102
+ // Dependency Graph
103
+ export {
104
+ DependencyGraph,
105
+ CircularDependencyError,
106
+ MissingDependencyError,
107
+ type GraphNode,
108
+ type ParallelGroup,
109
+ type GraphJSON,
110
+ type EventRegistrationWithDeps,
111
+ } from './dependency-graph.js'
112
+
113
+ // Topological Sort - Execution ordering algorithms
114
+ export {
115
+ topologicalSort,
116
+ topologicalSortKahn,
117
+ topologicalSortDFS,
118
+ getExecutionLevels,
119
+ CycleDetectedError,
120
+ MissingNodeError,
121
+ type SortableNode,
122
+ type ExecutionLevel,
123
+ type TopologicalSortOptions,
124
+ type TopologicalSortResult,
125
+ } from './graph/topological-sort.js'
126
+
127
+ // Barrier/Join Semantics - Parallel step coordination
128
+ export {
129
+ Barrier,
130
+ BarrierTimeoutError,
131
+ createBarrier,
132
+ waitForAll,
133
+ waitForAny,
134
+ withConcurrencyLimit,
135
+ type BarrierOptions,
136
+ type BarrierProgress,
137
+ type BarrierResult,
138
+ type WaitForAllOptions,
139
+ type WaitForAnyOptions,
140
+ type WaitForAnyResult,
141
+ type ConcurrencyOptions,
142
+ } from './barrier.js'
143
+
144
+ // Cascade Executor - code -> generative -> agentic -> human pattern
145
+ export {
146
+ CascadeExecutor,
147
+ CascadeTimeoutError,
148
+ TierSkippedError,
149
+ AllTiersFailedError,
150
+ TIER_ORDER,
151
+ DEFAULT_TIER_TIMEOUTS,
152
+ type CapabilityTier,
153
+ type TierHandler,
154
+ type TierContext,
155
+ type TierResult,
156
+ type TierRetryConfig,
157
+ type CascadeConfig,
158
+ type CascadeResult,
159
+ type CascadeMetrics,
160
+ type SkipCondition,
161
+ } from './cascade-executor.js'
162
+
87
163
  // Types
88
164
  export type {
89
165
  EventHandler,
@@ -103,4 +179,6 @@ export type {
103
179
  DatabaseContext,
104
180
  ActionData,
105
181
  ArtifactData,
182
+ DependencyConfig,
183
+ DependencyType,
106
184
  } from './types.js'
package/src/on.ts CHANGED
@@ -5,9 +5,21 @@
5
5
  * on.Customer.created(customer => { ... })
6
6
  * on.Order.completed(order => { ... })
7
7
  * on.Payment.failed(payment => { ... })
8
+ *
9
+ * With dependencies:
10
+ * on.Step2.complete(handler, { dependsOn: 'Step1.complete' })
11
+ * on.Step3.complete(handler, { dependsOn: ['Step1.complete', 'Step2.complete'] })
8
12
  */
9
13
 
10
- import type { EventHandler, EventRegistration } from './types.js'
14
+ import type {
15
+ EventHandler,
16
+ EventRegistration,
17
+ DependencyConfig,
18
+ OnProxy,
19
+ NounEventProxy,
20
+ OnProxyHandler,
21
+ NounEventProxyHandler,
22
+ } from './types.js'
11
23
 
12
24
  /**
13
25
  * Registry of event handlers
@@ -34,50 +46,94 @@ export function clearEventHandlers(): void {
34
46
  export function registerEventHandler(
35
47
  noun: string,
36
48
  event: string,
37
- handler: EventHandler
49
+ handler: EventHandler,
50
+ dependencies?: DependencyConfig
38
51
  ): void {
39
52
  eventRegistry.push({
40
53
  noun,
41
54
  event,
42
55
  handler,
43
56
  source: handler.toString(),
57
+ dependencies,
44
58
  })
45
59
  }
46
60
 
47
61
  /**
48
- * Event proxy type for on.Noun.event pattern
62
+ * Handler registration callback type
63
+ * Used by createTypedOnProxy to customize handler registration
49
64
  */
50
- type EventProxy = {
51
- [noun: string]: {
52
- [event: string]: (handler: EventHandler) => void
53
- }
54
- }
65
+ export type OnProxyRegistrationCallback = (
66
+ noun: string,
67
+ event: string,
68
+ handler: EventHandler,
69
+ dependencies?: DependencyConfig
70
+ ) => void
55
71
 
56
72
  /**
57
- * Create the `on` proxy
73
+ * Create a typed OnProxy with proper TypeScript generics
58
74
  *
59
- * This creates a proxy that allows:
60
- * on.Customer.created(handler)
61
- * on.Order.shipped(handler)
75
+ * This factory function creates a two-level proxy that allows:
76
+ * proxy.Customer.created(handler)
77
+ * proxy.Order.shipped(handler)
62
78
  *
63
79
  * The first property access captures the noun (Customer, Order)
64
80
  * The second property access captures the event (created, shipped)
65
- * The function call registers the handler
81
+ * The function call invokes the registration callback
82
+ *
83
+ * @param registerCallback - Function called when a handler is registered
84
+ * @returns A properly typed OnProxy
85
+ *
86
+ * @example
87
+ * ```ts
88
+ * // Create proxy with custom registration
89
+ * const myOn = createTypedOnProxy((noun, event, handler, deps) => {
90
+ * myRegistry.push({ noun, event, handler, deps })
91
+ * })
92
+ *
93
+ * myOn.Customer.created(handler) // Properly typed!
94
+ * ```
66
95
  */
67
- function createOnProxy(): EventProxy {
68
- return new Proxy({} as EventProxy, {
69
- get(_target, noun: string) {
70
- // Return a proxy for the event level
71
- return new Proxy({}, {
72
- get(_eventTarget, event: string) {
73
- // Return a function that registers the handler
74
- return (handler: EventHandler) => {
75
- registerEventHandler(noun, event, handler)
76
- }
77
- }
78
- })
96
+ export function createTypedOnProxy(registerCallback: OnProxyRegistrationCallback): OnProxy {
97
+ // Create typed handler for the noun level (event accessors)
98
+ const createNounHandler = (noun: string): NounEventProxyHandler => ({
99
+ get(
100
+ _target: Record<string, (handler: EventHandler, dependencies?: DependencyConfig) => void>,
101
+ event: string,
102
+ _receiver: unknown
103
+ ): (handler: EventHandler, dependencies?: DependencyConfig) => void {
104
+ // Return a function that registers the handler with optional dependencies
105
+ return (handler: EventHandler, dependencies?: DependencyConfig) => {
106
+ registerCallback(noun, event, handler, dependencies)
107
+ }
79
108
  }
80
109
  })
110
+
111
+ // Create typed handler for the top-level proxy (noun accessors)
112
+ const onHandler: OnProxyHandler = {
113
+ get(
114
+ _target: Record<string, NounEventProxy>,
115
+ noun: string,
116
+ _receiver: unknown
117
+ ): NounEventProxy {
118
+ // Return a proxy for the event level with typed handler
119
+ const eventTarget: Record<string, (handler: EventHandler, dependencies?: DependencyConfig) => void> = {}
120
+ return new Proxy(eventTarget, createNounHandler(noun)) as NounEventProxy
121
+ }
122
+ }
123
+
124
+ // Create and return the typed OnProxy
125
+ const target: Record<string, NounEventProxy> = {}
126
+ return new Proxy(target, onHandler) as OnProxy
127
+ }
128
+
129
+ /**
130
+ * Create the `on` proxy using the global event registry
131
+ *
132
+ * This is the default implementation that uses registerEventHandler
133
+ * for backward compatibility with the standalone `on` export.
134
+ */
135
+ function createOnProxy(): OnProxy {
136
+ return createTypedOnProxy(registerEventHandler)
81
137
  }
82
138
 
83
139
  /**