@zenithbuild/core 0.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 (101) hide show
  1. package/.eslintignore +15 -0
  2. package/.gitattributes +2 -0
  3. package/.github/ISSUE_TEMPLATE/compiler-errors-for-invalid-state-declarations.md +25 -0
  4. package/.github/ISSUE_TEMPLATE/new_ticket.yaml +34 -0
  5. package/.github/pull_request_template.md +15 -0
  6. package/.github/workflows/discord-changelog.yml +141 -0
  7. package/.github/workflows/discord-notify.yml +242 -0
  8. package/.github/workflows/discord-version.yml +195 -0
  9. package/.prettierignore +13 -0
  10. package/.prettierrc +21 -0
  11. package/.zen.d.ts +15 -0
  12. package/LICENSE +21 -0
  13. package/README.md +55 -0
  14. package/app/components/Button.zen +46 -0
  15. package/app/components/Link.zen +11 -0
  16. package/app/favicon.ico +0 -0
  17. package/app/layouts/Main.zen +59 -0
  18. package/app/pages/about.zen +23 -0
  19. package/app/pages/blog/[id].zen +53 -0
  20. package/app/pages/blog/index.zen +32 -0
  21. package/app/pages/dynamic-dx.zen +712 -0
  22. package/app/pages/dynamic-primitives.zen +453 -0
  23. package/app/pages/index.zen +154 -0
  24. package/app/pages/navigation-demo.zen +229 -0
  25. package/app/pages/posts/[...slug].zen +61 -0
  26. package/app/pages/primitives-demo.zen +273 -0
  27. package/assets/logos/0E3B5DDD-605C-4839-BB2E-DFCA8ADC9604.PNG +0 -0
  28. package/assets/logos/760971E5-79A1-44F9-90B9-925DF30F4278.PNG +0 -0
  29. package/assets/logos/8A06ED80-9ED2-4689-BCBD-13B2E95EE8E4.JPG +0 -0
  30. package/assets/logos/C691FF58-ED13-4E8D-B6A3-02E835849340.PNG +0 -0
  31. package/assets/logos/C691FF58-ED13-4E8D-B6A3-02E835849340.svg +601 -0
  32. package/assets/logos/README.md +54 -0
  33. package/assets/logos/zen.icns +0 -0
  34. package/bun.lock +39 -0
  35. package/compiler/README.md +380 -0
  36. package/compiler/errors/compilerError.ts +24 -0
  37. package/compiler/finalize/finalizeOutput.ts +163 -0
  38. package/compiler/finalize/generateFinalBundle.ts +82 -0
  39. package/compiler/index.ts +44 -0
  40. package/compiler/ir/types.ts +83 -0
  41. package/compiler/legacy/binding.ts +254 -0
  42. package/compiler/legacy/bindings.ts +338 -0
  43. package/compiler/legacy/component-process.ts +1208 -0
  44. package/compiler/legacy/component.ts +301 -0
  45. package/compiler/legacy/event.ts +50 -0
  46. package/compiler/legacy/expression.ts +1149 -0
  47. package/compiler/legacy/mutation.ts +280 -0
  48. package/compiler/legacy/parse.ts +299 -0
  49. package/compiler/legacy/split.ts +608 -0
  50. package/compiler/legacy/types.ts +32 -0
  51. package/compiler/output/types.ts +34 -0
  52. package/compiler/parse/detectMapExpressions.ts +102 -0
  53. package/compiler/parse/parseScript.ts +22 -0
  54. package/compiler/parse/parseTemplate.ts +425 -0
  55. package/compiler/parse/parseZenFile.ts +66 -0
  56. package/compiler/parse/trackLoopContext.ts +82 -0
  57. package/compiler/runtime/dataExposure.ts +291 -0
  58. package/compiler/runtime/generateDOM.ts +144 -0
  59. package/compiler/runtime/generateHydrationBundle.ts +383 -0
  60. package/compiler/runtime/hydration.ts +309 -0
  61. package/compiler/runtime/navigation.ts +432 -0
  62. package/compiler/runtime/thinRuntime.ts +160 -0
  63. package/compiler/runtime/transformIR.ts +256 -0
  64. package/compiler/runtime/wrapExpression.ts +84 -0
  65. package/compiler/runtime/wrapExpressionWithLoop.ts +77 -0
  66. package/compiler/spa-build.ts +1000 -0
  67. package/compiler/test/validate-test.ts +104 -0
  68. package/compiler/transform/generateBindings.ts +47 -0
  69. package/compiler/transform/generateHTML.ts +28 -0
  70. package/compiler/transform/transformNode.ts +126 -0
  71. package/compiler/transform/transformTemplate.ts +38 -0
  72. package/compiler/validate/validateExpressions.ts +168 -0
  73. package/core/index.ts +135 -0
  74. package/core/lifecycle/index.ts +49 -0
  75. package/core/lifecycle/zen-mount.ts +182 -0
  76. package/core/lifecycle/zen-unmount.ts +88 -0
  77. package/core/reactivity/index.ts +54 -0
  78. package/core/reactivity/tracking.ts +167 -0
  79. package/core/reactivity/zen-batch.ts +57 -0
  80. package/core/reactivity/zen-effect.ts +139 -0
  81. package/core/reactivity/zen-memo.ts +146 -0
  82. package/core/reactivity/zen-ref.ts +52 -0
  83. package/core/reactivity/zen-signal.ts +121 -0
  84. package/core/reactivity/zen-state.ts +180 -0
  85. package/core/reactivity/zen-untrack.ts +44 -0
  86. package/docs/COMMENTS.md +111 -0
  87. package/docs/COMMITS.md +36 -0
  88. package/docs/CONTRIBUTING.md +116 -0
  89. package/docs/STYLEGUIDE.md +62 -0
  90. package/package.json +44 -0
  91. package/router/index.ts +76 -0
  92. package/router/manifest.ts +314 -0
  93. package/router/navigation/ZenLink.zen +231 -0
  94. package/router/navigation/index.ts +78 -0
  95. package/router/navigation/zen-link.ts +584 -0
  96. package/router/runtime.ts +458 -0
  97. package/router/types.ts +168 -0
  98. package/runtime/build.ts +17 -0
  99. package/runtime/serve.ts +93 -0
  100. package/scripts/webhook-proxy.ts +213 -0
  101. package/tsconfig.json +28 -0
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Zenith OnMount - Post-Mount Lifecycle Hook
3
+ *
4
+ * Registers a callback to run after a component's DOM is inserted.
5
+ * This is an effect wrapper that defers execution until the mount phase.
6
+ *
7
+ * Features:
8
+ * - Runs after DOM is available
9
+ * - Only runs once per mount
10
+ * - Supports cleanup function return
11
+ * - Works with component lifecycle system
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * zenOnMount(() => {
16
+ * console.log('Component mounted!')
17
+ * const el = document.querySelector('.my-element')
18
+ *
19
+ * // Optional cleanup - runs on unmount
20
+ * return () => {
21
+ * console.log('Component will unmount')
22
+ * }
23
+ * })
24
+ * ```
25
+ *
26
+ * Note: This hook registers callbacks that will be executed by the
27
+ * component lifecycle system. If no mount scheduler is active,
28
+ * callbacks are queued for later execution.
29
+ */
30
+
31
+ /**
32
+ * Mount callback type - can optionally return a cleanup function
33
+ */
34
+ export type MountCallback = () => void | (() => void)
35
+
36
+ /**
37
+ * Mount hook state
38
+ */
39
+ interface MountHookState {
40
+ callback: MountCallback
41
+ cleanup: (() => void) | null
42
+ mounted: boolean
43
+ }
44
+
45
+ /**
46
+ * Queue of pending mount callbacks
47
+ * These are registered but not yet executed because mount hasn't occurred
48
+ */
49
+ const pendingMountCallbacks: MountHookState[] = []
50
+
51
+ /**
52
+ * Currently active mount hooks (for cleanup on unmount)
53
+ */
54
+ const activeMountHooks: Set<MountHookState> = new Set()
55
+
56
+ /**
57
+ * Flag indicating whether we're in a mounted state
58
+ * This is controlled by the component lifecycle system
59
+ */
60
+ let isMounted = false
61
+
62
+ /**
63
+ * Register a callback to run after component mount
64
+ *
65
+ * @param callback - Function to run after mount (can return cleanup function)
66
+ * @returns Dispose function to cancel the mount callback
67
+ */
68
+ export function zenOnMount(callback: MountCallback): () => void {
69
+ const state: MountHookState = {
70
+ callback,
71
+ cleanup: null,
72
+ mounted: false
73
+ }
74
+
75
+ if (isMounted) {
76
+ // Already mounted - run immediately
77
+ executeMountCallback(state)
78
+ } else {
79
+ // Queue for later execution
80
+ pendingMountCallbacks.push(state)
81
+ }
82
+
83
+ activeMountHooks.add(state)
84
+
85
+ // Return dispose function
86
+ return () => {
87
+ // Remove from pending if not yet executed
88
+ const pendingIndex = pendingMountCallbacks.indexOf(state)
89
+ if (pendingIndex !== -1) {
90
+ pendingMountCallbacks.splice(pendingIndex, 1)
91
+ }
92
+
93
+ // Run cleanup if already mounted
94
+ if (state.mounted && state.cleanup) {
95
+ state.cleanup()
96
+ state.cleanup = null
97
+ }
98
+
99
+ activeMountHooks.delete(state)
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Execute a mount callback
105
+ */
106
+ function executeMountCallback(state: MountHookState): void {
107
+ if (state.mounted) return
108
+
109
+ state.mounted = true
110
+
111
+ try {
112
+ const result = state.callback()
113
+
114
+ if (typeof result === 'function') {
115
+ state.cleanup = result
116
+ }
117
+ } catch (error) {
118
+ console.error('[Zenith] Error in onMount callback:', error)
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Trigger mount phase - called by component lifecycle system
124
+ * Executes all pending mount callbacks
125
+ *
126
+ * @internal
127
+ */
128
+ export function triggerMount(): void {
129
+ isMounted = true
130
+
131
+ // Execute all pending callbacks
132
+ const callbacks = [...pendingMountCallbacks]
133
+ pendingMountCallbacks.length = 0
134
+
135
+ for (const state of callbacks) {
136
+ executeMountCallback(state)
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Trigger unmount phase - called by component lifecycle system
142
+ * Runs cleanup functions for all active mount hooks
143
+ *
144
+ * @internal
145
+ */
146
+ export function triggerUnmount(): void {
147
+ isMounted = false
148
+
149
+ // Run all cleanup functions
150
+ for (const state of activeMountHooks) {
151
+ if (state.cleanup) {
152
+ try {
153
+ state.cleanup()
154
+ } catch (error) {
155
+ console.error('[Zenith] Error in onMount cleanup:', error)
156
+ }
157
+ state.cleanup = null
158
+ }
159
+ state.mounted = false
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Check if currently in mounted state
165
+ *
166
+ * @internal
167
+ */
168
+ export function getIsMounted(): boolean {
169
+ return isMounted
170
+ }
171
+
172
+ /**
173
+ * Reset mount state - for testing purposes
174
+ *
175
+ * @internal
176
+ */
177
+ export function resetMountState(): void {
178
+ isMounted = false
179
+ pendingMountCallbacks.length = 0
180
+ activeMountHooks.clear()
181
+ }
182
+
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Zenith OnUnmount - Pre-Unmount Lifecycle Hook
3
+ *
4
+ * Registers a cleanup callback to run before a component is disposed.
5
+ * Useful for cleaning up subscriptions, timers, event listeners, etc.
6
+ *
7
+ * Features:
8
+ * - Runs before component is removed from DOM
9
+ * - Can register multiple callbacks
10
+ * - Callbacks run in registration order
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * zenOnUnmount(() => {
15
+ * console.log('Cleaning up...')
16
+ * subscription.unsubscribe()
17
+ * clearInterval(timerId)
18
+ * })
19
+ * ```
20
+ *
21
+ * Note: This hook registers callbacks that will be executed by the
22
+ * component lifecycle system when the component is disposed.
23
+ */
24
+
25
+ /**
26
+ * Unmount callback type
27
+ */
28
+ export type UnmountCallback = () => void
29
+
30
+ /**
31
+ * Queue of registered unmount callbacks
32
+ */
33
+ const unmountCallbacks: Set<UnmountCallback> = new Set()
34
+
35
+ /**
36
+ * Register a callback to run before component unmount
37
+ *
38
+ * @param callback - Function to run before unmount
39
+ * @returns Dispose function to cancel the unmount callback
40
+ */
41
+ export function zenOnUnmount(callback: UnmountCallback): () => void {
42
+ unmountCallbacks.add(callback)
43
+
44
+ // Return dispose function
45
+ return () => {
46
+ unmountCallbacks.delete(callback)
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Execute all unmount callbacks
52
+ * Called by the component lifecycle system before disposal
53
+ *
54
+ * @internal
55
+ */
56
+ export function executeUnmountCallbacks(): void {
57
+ // Execute in registration order
58
+ for (const callback of unmountCallbacks) {
59
+ try {
60
+ callback()
61
+ } catch (error) {
62
+ console.error('[Zenith] Error in onUnmount callback:', error)
63
+ }
64
+ }
65
+
66
+ // Clear all callbacks after execution
67
+ unmountCallbacks.clear()
68
+ }
69
+
70
+ /**
71
+ * Get count of registered unmount callbacks
72
+ * Useful for testing
73
+ *
74
+ * @internal
75
+ */
76
+ export function getUnmountCallbackCount(): number {
77
+ return unmountCallbacks.size
78
+ }
79
+
80
+ /**
81
+ * Reset unmount state - for testing purposes
82
+ *
83
+ * @internal
84
+ */
85
+ export function resetUnmountState(): void {
86
+ unmountCallbacks.clear()
87
+ }
88
+
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Zenith Reactivity System
3
+ *
4
+ * This module exports all reactive primitives for the Zenith framework.
5
+ *
6
+ * Exports both explicit `zen*` names (internal) and clean aliases (public DX).
7
+ */
8
+
9
+ // Core primitives - explicit names
10
+ import { zenSignal as _zenSignal, type Signal } from './zen-signal'
11
+ import { zenState as _zenState } from './zen-state'
12
+ import { zenEffect as _zenEffect, type EffectFn, type DisposeFn } from './zen-effect'
13
+ import { zenMemo as _zenMemo, type Memo } from './zen-memo'
14
+ import { zenRef as _zenRef, type Ref } from './zen-ref'
15
+ import { zenBatch as _zenBatch } from './zen-batch'
16
+ import { zenUntrack as _zenUntrack } from './zen-untrack'
17
+
18
+ // Re-export with explicit names
19
+ export const zenSignal = _zenSignal
20
+ export const zenState = _zenState
21
+ export const zenEffect = _zenEffect
22
+ export const zenMemo = _zenMemo
23
+ export const zenRef = _zenRef
24
+ export const zenBatch = _zenBatch
25
+ export const zenUntrack = _zenUntrack
26
+
27
+ // Re-export types
28
+ export type { Signal, Memo, Ref, EffectFn, DisposeFn }
29
+
30
+ // Internal tracking utilities (for advanced use)
31
+ export {
32
+ type Subscriber,
33
+ type TrackingContext,
34
+ trackDependency,
35
+ notifySubscribers,
36
+ getCurrentContext,
37
+ pushContext,
38
+ popContext,
39
+ cleanupContext,
40
+ runUntracked,
41
+ startBatch,
42
+ endBatch,
43
+ isBatching
44
+ } from './tracking'
45
+
46
+ // Public DX aliases - clean names
47
+ export const signal = _zenSignal
48
+ export const state = _zenState
49
+ export const effect = _zenEffect
50
+ export const memo = _zenMemo
51
+ export const ref = _zenRef
52
+ export const batch = _zenBatch
53
+ export const untrack = _zenUntrack
54
+
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Zenith Reactivity Tracking System
3
+ *
4
+ * This module provides the core dependency tracking mechanism used by
5
+ * signals, effects, and memos. It uses a stack-based approach to track
6
+ * which reactive values are accessed during effect/memo execution.
7
+ *
8
+ * Key concepts:
9
+ * - Subscriber: A function that should be called when a dependency changes
10
+ * - Tracking context: The currently executing effect/memo that should collect dependencies
11
+ * - Dependency: A reactive value that an effect/memo depends on
12
+ */
13
+
14
+ /**
15
+ * A subscriber is a function that gets called when a reactive value changes
16
+ */
17
+ export type Subscriber = () => void
18
+
19
+ /**
20
+ * Tracking context - represents an effect or memo that is collecting dependencies
21
+ */
22
+ export interface TrackingContext {
23
+ /** The function to call when dependencies change */
24
+ execute: Subscriber
25
+ /** Set of dependency subscriber sets this context is registered with */
26
+ dependencies: Set<Set<Subscriber>>
27
+ }
28
+
29
+ /**
30
+ * Stack of currently executing tracking contexts
31
+ * When an effect runs, it pushes itself onto this stack.
32
+ * When a signal is read, it registers the top of the stack as a subscriber.
33
+ */
34
+ const trackingStack: TrackingContext[] = []
35
+
36
+ /**
37
+ * Flag to disable tracking (used by zenUntrack)
38
+ */
39
+ let trackingDisabled = false
40
+
41
+ /**
42
+ * Batch depth counter - when > 0, effect execution is deferred
43
+ */
44
+ let batchDepth = 0
45
+
46
+ /**
47
+ * Queue of effects to run after batch completes
48
+ */
49
+ const pendingEffects: Set<Subscriber> = new Set()
50
+
51
+ /**
52
+ * Get the current tracking context (if any)
53
+ */
54
+ export function getCurrentContext(): TrackingContext | undefined {
55
+ if (trackingDisabled) return undefined
56
+ return trackingStack[trackingStack.length - 1]
57
+ }
58
+
59
+ /**
60
+ * Push a new tracking context onto the stack
61
+ */
62
+ export function pushContext(context: TrackingContext): void {
63
+ trackingStack.push(context)
64
+ }
65
+
66
+ /**
67
+ * Pop the current tracking context from the stack
68
+ */
69
+ export function popContext(): TrackingContext | undefined {
70
+ return trackingStack.pop()
71
+ }
72
+
73
+ /**
74
+ * Track a dependency - called when a reactive value is read
75
+ *
76
+ * @param subscribers - The subscriber set of the reactive value being read
77
+ */
78
+ export function trackDependency(subscribers: Set<Subscriber>): void {
79
+ const context = getCurrentContext()
80
+
81
+ if (context) {
82
+ // Register this effect as a subscriber to the signal
83
+ subscribers.add(context.execute)
84
+ // Track that this effect depends on this signal
85
+ context.dependencies.add(subscribers)
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Notify subscribers that a reactive value has changed
91
+ *
92
+ * @param subscribers - The subscriber set to notify
93
+ */
94
+ export function notifySubscribers(subscribers: Set<Subscriber>): void {
95
+ // Copy subscribers to avoid issues if the set is modified during iteration
96
+ const toNotify = [...subscribers]
97
+
98
+ for (const subscriber of toNotify) {
99
+ if (batchDepth > 0) {
100
+ // Batching - defer effect execution
101
+ pendingEffects.add(subscriber)
102
+ } else {
103
+ // Execute immediately
104
+ subscriber()
105
+ }
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Clean up a tracking context - remove it from all dependency sets
111
+ *
112
+ * @param context - The context to clean up
113
+ */
114
+ export function cleanupContext(context: TrackingContext): void {
115
+ for (const deps of context.dependencies) {
116
+ deps.delete(context.execute)
117
+ }
118
+ context.dependencies.clear()
119
+ }
120
+
121
+ /**
122
+ * Run a function without tracking dependencies
123
+ *
124
+ * @param fn - The function to run
125
+ * @returns The return value of the function
126
+ */
127
+ export function runUntracked<T>(fn: () => T): T {
128
+ const wasDisabled = trackingDisabled
129
+ trackingDisabled = true
130
+ try {
131
+ return fn()
132
+ } finally {
133
+ trackingDisabled = wasDisabled
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Start a batch - defer effect execution until batch ends
139
+ */
140
+ export function startBatch(): void {
141
+ batchDepth++
142
+ }
143
+
144
+ /**
145
+ * End a batch - run all pending effects
146
+ */
147
+ export function endBatch(): void {
148
+ batchDepth--
149
+
150
+ if (batchDepth === 0 && pendingEffects.size > 0) {
151
+ // Run all pending effects
152
+ const effects = [...pendingEffects]
153
+ pendingEffects.clear()
154
+
155
+ for (const effect of effects) {
156
+ effect()
157
+ }
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Check if currently inside a batch
163
+ */
164
+ export function isBatching(): boolean {
165
+ return batchDepth > 0
166
+ }
167
+
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Zenith Batch - Deferred Effect Execution
3
+ *
4
+ * Batching allows you to make multiple reactive updates without
5
+ * triggering effects until all updates are complete. This improves
6
+ * performance by preventing redundant effect executions.
7
+ *
8
+ * Features:
9
+ * - Groups multiple mutations
10
+ * - Defers effect execution until batch completes
11
+ * - Supports nested batches
12
+ * - Automatically flushes on completion
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * const firstName = zenSignal('John')
17
+ * const lastName = zenSignal('Doe')
18
+ *
19
+ * zenEffect(() => {
20
+ * console.log(`${firstName()} ${lastName()}`)
21
+ * })
22
+ * // Logs: "John Doe"
23
+ *
24
+ * // Without batch - effect runs twice
25
+ * firstName('Jane') // Logs: "Jane Doe"
26
+ * lastName('Smith') // Logs: "Jane Smith"
27
+ *
28
+ * // With batch - effect runs once
29
+ * zenBatch(() => {
30
+ * firstName('Bob')
31
+ * lastName('Brown')
32
+ * })
33
+ * // Logs: "Bob Brown" (only once)
34
+ * ```
35
+ */
36
+
37
+ import { startBatch, endBatch } from './tracking'
38
+
39
+ /**
40
+ * Execute a function with batched updates
41
+ *
42
+ * All reactive updates inside the batch will be collected,
43
+ * and effects will only run once after the batch completes.
44
+ *
45
+ * @param fn - The function to execute
46
+ * @returns The return value of the function
47
+ */
48
+ export function zenBatch<T>(fn: () => T): T {
49
+ startBatch()
50
+
51
+ try {
52
+ return fn()
53
+ } finally {
54
+ endBatch()
55
+ }
56
+ }
57
+
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Zenith Effect - Auto-Tracked Side Effect
3
+ *
4
+ * Effects are functions that automatically track their reactive dependencies
5
+ * and re-run when those dependencies change. They are the bridge between
6
+ * reactive state and side effects (DOM updates, logging, API calls, etc.)
7
+ *
8
+ * Features:
9
+ * - Automatic dependency tracking (no dependency arrays)
10
+ * - Runs immediately on creation
11
+ * - Re-runs when dependencies change
12
+ * - Supports cleanup functions
13
+ * - Can be manually disposed
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * const count = zenSignal(0)
18
+ *
19
+ * // Effect runs immediately, then re-runs when count changes
20
+ * const dispose = zenEffect(() => {
21
+ * console.log('Count:', count())
22
+ *
23
+ * // Optional cleanup - runs before next execution or on dispose
24
+ * return () => {
25
+ * console.log('Cleanup')
26
+ * }
27
+ * })
28
+ *
29
+ * count(1) // Logs: "Cleanup", then "Count: 1"
30
+ *
31
+ * dispose() // Cleanup and stop watching
32
+ * ```
33
+ */
34
+
35
+ import {
36
+ pushContext,
37
+ popContext,
38
+ cleanupContext,
39
+ type TrackingContext
40
+ } from './tracking'
41
+
42
+ /**
43
+ * Effect function type - can optionally return a cleanup function
44
+ */
45
+ export type EffectFn = () => void | (() => void)
46
+
47
+ /**
48
+ * Dispose function - call to stop the effect
49
+ */
50
+ export type DisposeFn = () => void
51
+
52
+ /**
53
+ * Effect state
54
+ */
55
+ interface EffectState {
56
+ /** The effect function */
57
+ fn: EffectFn
58
+ /** Current cleanup function (if any) */
59
+ cleanup: (() => void) | null
60
+ /** Tracking context for dependency collection */
61
+ context: TrackingContext
62
+ /** Whether the effect has been disposed */
63
+ disposed: boolean
64
+ }
65
+
66
+ /**
67
+ * Create an auto-tracked side effect
68
+ *
69
+ * @param fn - The effect function to run
70
+ * @returns A dispose function to stop the effect
71
+ */
72
+ export function zenEffect(fn: EffectFn): DisposeFn {
73
+ const state: EffectState = {
74
+ fn,
75
+ cleanup: null,
76
+ context: {
77
+ execute: () => runEffect(state),
78
+ dependencies: new Set()
79
+ },
80
+ disposed: false
81
+ }
82
+
83
+ // Run the effect immediately
84
+ runEffect(state)
85
+
86
+ // Return dispose function
87
+ return () => disposeEffect(state)
88
+ }
89
+
90
+ /**
91
+ * Run an effect, tracking dependencies
92
+ */
93
+ function runEffect(state: EffectState): void {
94
+ if (state.disposed) return
95
+
96
+ // Run cleanup from previous execution
97
+ if (state.cleanup) {
98
+ state.cleanup()
99
+ state.cleanup = null
100
+ }
101
+
102
+ // Clean up old dependencies
103
+ cleanupContext(state.context)
104
+
105
+ // Push this effect onto the tracking stack
106
+ pushContext(state.context)
107
+
108
+ try {
109
+ // Run the effect function
110
+ const result = state.fn()
111
+
112
+ // Store cleanup if returned
113
+ if (typeof result === 'function') {
114
+ state.cleanup = result
115
+ }
116
+ } finally {
117
+ // Pop from tracking stack
118
+ popContext()
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Dispose an effect - run cleanup and stop watching
124
+ */
125
+ function disposeEffect(state: EffectState): void {
126
+ if (state.disposed) return
127
+
128
+ state.disposed = true
129
+
130
+ // Run cleanup
131
+ if (state.cleanup) {
132
+ state.cleanup()
133
+ state.cleanup = null
134
+ }
135
+
136
+ // Remove from all dependency sets
137
+ cleanupContext(state.context)
138
+ }
139
+