@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.
- package/.eslintignore +15 -0
- package/.gitattributes +2 -0
- package/.github/ISSUE_TEMPLATE/compiler-errors-for-invalid-state-declarations.md +25 -0
- package/.github/ISSUE_TEMPLATE/new_ticket.yaml +34 -0
- package/.github/pull_request_template.md +15 -0
- package/.github/workflows/discord-changelog.yml +141 -0
- package/.github/workflows/discord-notify.yml +242 -0
- package/.github/workflows/discord-version.yml +195 -0
- package/.prettierignore +13 -0
- package/.prettierrc +21 -0
- package/.zen.d.ts +15 -0
- package/LICENSE +21 -0
- package/README.md +55 -0
- package/app/components/Button.zen +46 -0
- package/app/components/Link.zen +11 -0
- package/app/favicon.ico +0 -0
- package/app/layouts/Main.zen +59 -0
- package/app/pages/about.zen +23 -0
- package/app/pages/blog/[id].zen +53 -0
- package/app/pages/blog/index.zen +32 -0
- package/app/pages/dynamic-dx.zen +712 -0
- package/app/pages/dynamic-primitives.zen +453 -0
- package/app/pages/index.zen +154 -0
- package/app/pages/navigation-demo.zen +229 -0
- package/app/pages/posts/[...slug].zen +61 -0
- package/app/pages/primitives-demo.zen +273 -0
- package/assets/logos/0E3B5DDD-605C-4839-BB2E-DFCA8ADC9604.PNG +0 -0
- package/assets/logos/760971E5-79A1-44F9-90B9-925DF30F4278.PNG +0 -0
- package/assets/logos/8A06ED80-9ED2-4689-BCBD-13B2E95EE8E4.JPG +0 -0
- package/assets/logos/C691FF58-ED13-4E8D-B6A3-02E835849340.PNG +0 -0
- package/assets/logos/C691FF58-ED13-4E8D-B6A3-02E835849340.svg +601 -0
- package/assets/logos/README.md +54 -0
- package/assets/logos/zen.icns +0 -0
- package/bun.lock +39 -0
- package/compiler/README.md +380 -0
- package/compiler/errors/compilerError.ts +24 -0
- package/compiler/finalize/finalizeOutput.ts +163 -0
- package/compiler/finalize/generateFinalBundle.ts +82 -0
- package/compiler/index.ts +44 -0
- package/compiler/ir/types.ts +83 -0
- package/compiler/legacy/binding.ts +254 -0
- package/compiler/legacy/bindings.ts +338 -0
- package/compiler/legacy/component-process.ts +1208 -0
- package/compiler/legacy/component.ts +301 -0
- package/compiler/legacy/event.ts +50 -0
- package/compiler/legacy/expression.ts +1149 -0
- package/compiler/legacy/mutation.ts +280 -0
- package/compiler/legacy/parse.ts +299 -0
- package/compiler/legacy/split.ts +608 -0
- package/compiler/legacy/types.ts +32 -0
- package/compiler/output/types.ts +34 -0
- package/compiler/parse/detectMapExpressions.ts +102 -0
- package/compiler/parse/parseScript.ts +22 -0
- package/compiler/parse/parseTemplate.ts +425 -0
- package/compiler/parse/parseZenFile.ts +66 -0
- package/compiler/parse/trackLoopContext.ts +82 -0
- package/compiler/runtime/dataExposure.ts +291 -0
- package/compiler/runtime/generateDOM.ts +144 -0
- package/compiler/runtime/generateHydrationBundle.ts +383 -0
- package/compiler/runtime/hydration.ts +309 -0
- package/compiler/runtime/navigation.ts +432 -0
- package/compiler/runtime/thinRuntime.ts +160 -0
- package/compiler/runtime/transformIR.ts +256 -0
- package/compiler/runtime/wrapExpression.ts +84 -0
- package/compiler/runtime/wrapExpressionWithLoop.ts +77 -0
- package/compiler/spa-build.ts +1000 -0
- package/compiler/test/validate-test.ts +104 -0
- package/compiler/transform/generateBindings.ts +47 -0
- package/compiler/transform/generateHTML.ts +28 -0
- package/compiler/transform/transformNode.ts +126 -0
- package/compiler/transform/transformTemplate.ts +38 -0
- package/compiler/validate/validateExpressions.ts +168 -0
- package/core/index.ts +135 -0
- package/core/lifecycle/index.ts +49 -0
- package/core/lifecycle/zen-mount.ts +182 -0
- package/core/lifecycle/zen-unmount.ts +88 -0
- package/core/reactivity/index.ts +54 -0
- package/core/reactivity/tracking.ts +167 -0
- package/core/reactivity/zen-batch.ts +57 -0
- package/core/reactivity/zen-effect.ts +139 -0
- package/core/reactivity/zen-memo.ts +146 -0
- package/core/reactivity/zen-ref.ts +52 -0
- package/core/reactivity/zen-signal.ts +121 -0
- package/core/reactivity/zen-state.ts +180 -0
- package/core/reactivity/zen-untrack.ts +44 -0
- package/docs/COMMENTS.md +111 -0
- package/docs/COMMITS.md +36 -0
- package/docs/CONTRIBUTING.md +116 -0
- package/docs/STYLEGUIDE.md +62 -0
- package/package.json +44 -0
- package/router/index.ts +76 -0
- package/router/manifest.ts +314 -0
- package/router/navigation/ZenLink.zen +231 -0
- package/router/navigation/index.ts +78 -0
- package/router/navigation/zen-link.ts +584 -0
- package/router/runtime.ts +458 -0
- package/router/types.ts +168 -0
- package/runtime/build.ts +17 -0
- package/runtime/serve.ts +93 -0
- package/scripts/webhook-proxy.ts +213 -0
- 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
|
+
|