@zenithbuild/runtime 0.7.3 → 0.7.4

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 (47) hide show
  1. package/HYDRATION_CONTRACT.md +1 -1
  2. package/LICENSE +21 -0
  3. package/README.md +16 -18
  4. package/RUNTIME_CONTRACT.md +24 -12
  5. package/dist/cleanup.js +6 -3
  6. package/dist/diagnostics.d.ts +7 -0
  7. package/dist/diagnostics.js +7 -0
  8. package/dist/effect-runtime.d.ts +2 -0
  9. package/dist/effect-runtime.js +139 -0
  10. package/dist/effect-scheduler.d.ts +4 -0
  11. package/dist/effect-scheduler.js +88 -0
  12. package/dist/effect-utils.d.ts +19 -0
  13. package/dist/effect-utils.js +160 -0
  14. package/dist/env.d.ts +12 -0
  15. package/dist/env.js +18 -2
  16. package/dist/events.d.ts +1 -8
  17. package/dist/events.js +108 -46
  18. package/dist/expressions.d.ts +14 -0
  19. package/dist/expressions.js +295 -0
  20. package/dist/fragment-patch.d.ts +12 -0
  21. package/dist/fragment-patch.js +118 -0
  22. package/dist/hydrate.d.ts +3 -117
  23. package/dist/hydrate.js +235 -1817
  24. package/dist/index.d.ts +2 -2
  25. package/dist/index.js +2 -2
  26. package/dist/markup.d.ts +8 -0
  27. package/dist/markup.js +127 -0
  28. package/dist/mount-runtime.d.ts +1 -0
  29. package/dist/mount-runtime.js +39 -0
  30. package/dist/payload.d.ts +21 -0
  31. package/dist/payload.js +386 -0
  32. package/dist/reactivity-core.d.ts +3 -0
  33. package/dist/reactivity-core.js +22 -0
  34. package/dist/render.d.ts +3 -0
  35. package/dist/render.js +340 -0
  36. package/dist/scanner.d.ts +1 -0
  37. package/dist/scanner.js +61 -0
  38. package/dist/side-effect-scope.d.ts +16 -0
  39. package/dist/side-effect-scope.js +99 -0
  40. package/dist/signal.js +1 -1
  41. package/dist/state.js +1 -1
  42. package/dist/template-parser.d.ts +1 -0
  43. package/dist/template-parser.js +268 -0
  44. package/dist/template.js +10 -11
  45. package/dist/zeneffect.d.ts +12 -14
  46. package/dist/zeneffect.js +25 -519
  47. package/package.json +5 -3
@@ -1,6 +1,6 @@
1
1
  # Zenith Runtime V0 Hydration Contract
2
2
 
3
- Canonical public docs: `../zenith-docs/documentation/contracts/hydration-contract.md`
3
+ Canonical public docs: `../../docs/documentation/contracts/hydration-contract.md`
4
4
 
5
5
 
6
6
  Status: FROZEN (V0)
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zenith Team
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,30 +1,28 @@
1
- # @zenith/runtime
1
+ # @zenithbuild/runtime
2
2
 
3
3
  > **⚠️ Internal API:** This package is an internal implementation detail of the Zenith framework. It is not intended for public use and its API may break without warning. Please use `@zenithbuild/core` instead.
4
4
 
5
-
6
- The core runtime library for the Zenith framework.
5
+ Internal runtime package for Zenith hydration, bindings, and reactive primitives consumed by compiled output.
7
6
 
8
7
  ## Canonical Docs
9
8
 
10
- - Runtime contract: `../zenith-docs/documentation/contracts/runtime-contract.md`
11
- - Hydration contract: `../zenith-docs/documentation/contracts/hydration-contract.md`
12
- - Reactive binding model: `../zenith-docs/documentation/reference/reactive-binding-model.md`
9
+ - Runtime contract: `../../docs/documentation/contracts/runtime-contract.md`
10
+ - Hydration contract: `../../docs/documentation/contracts/hydration-contract.md`
11
+ - Reactive binding model: `../../docs/documentation/reference/reactive-binding-model.md`
13
12
 
14
13
  ## Overview
15
- This package provides the reactivity system, hydration logic, and Virtual DOM primitives used by Zenith applications. It is designed to be lightweight, fast, and tree-shakeable.
14
+ This package provides the minimal runtime surfaces needed after compile:
15
+
16
+ - signal/state/effect/mount primitives
17
+ - DOM binding and hydration helpers
18
+ - template/runtime bridges consumed by compiler and bundler output
19
+
20
+ It does not define a public virtual-DOM framework API.
16
21
 
17
22
  ## Features
18
- - **Fine-Grained Reactivity**: Signals, Effects, Derived State (Memos).
19
- - **Hydration**: Efficient client-side hydration of server-rendered HTML.
20
- - **VDOM primitives**: `h` and `fragment` for lightweight view rendering.
21
- - **Lifecycle Hooks**: `onMount`, `onUnmount`.
23
+ - **Fine-Grained Reactivity**: signal/state/effect primitives used by emitted code.
24
+ - **Hydration**: deterministic client-side hydration for server-rendered HTML.
25
+ - **Lifecycle Cleanup**: explicit mount/effect cleanup semantics.
22
26
 
23
27
  ## Usage
24
- This package is typically installed automatically by the Zenith CLI.
25
- ```typescript
26
- import { signal, effect } from "@zenith/runtime";
27
-
28
- const count = signal(0);
29
- effect(() => console.log(count()));
30
- ```
28
+ This package is installed as an internal framework dependency. App code should normally use the public Zenith surface instead of importing `@zenithbuild/runtime` directly.
@@ -1,6 +1,6 @@
1
1
  # RUNTIME_CONTRACT.md — Sealed Runtime Interface
2
2
 
3
- Canonical public docs: `../zenith-docs/documentation/contracts/runtime-contract.md`
3
+ Canonical public docs: `../../docs/documentation/contracts/runtime-contract.md`
4
4
 
5
5
 
6
6
  > **This document is a legal boundary.**
@@ -117,15 +117,16 @@ effect(() => {
117
117
  Internals:
118
118
  - On execution, sets itself as the "current tracking context"
119
119
  - Any signal read during execution adds this effect to its subscriber set
120
- - When a dependency signal changes, the effect re-runs
120
+ - When a dependency signal changes, the effect is scheduled to re-run
121
121
 
122
- ### Constraints
122
+ ### Scheduling & Execution Constraints
123
123
 
124
- - No batching
125
- - No scheduler / microtask queue
126
- - No async effects
124
+ - By default, effects are scheduled on the microtask queue (`flush: 'post'`) to prevent render thrashing
125
+ - Synchronous execution is supported via `flush: 'sync'`
126
+ - Advanced debouncing, throttling, and RAF deferral schedulers are internally implemented for complex interactions
127
127
  - No suspense / lazy loading
128
- - No lifecycle hooks beyond `cleanup()`
128
+ - `mount()` exists as the sole component bootstrap hook, deferring initialization until the DOM attachment scope is complete
129
+ - Beyond `mount()` and `cleanup()`, the runtime offers no lifecycle hooks
129
130
 
130
131
  ---
131
132
 
@@ -166,13 +167,24 @@ Cleanup is deterministic — calling it twice is a no-op.
166
167
  Total exports (exhaustive):
167
168
 
168
169
  ```js
169
- export { signal } // Create reactive signal
170
- export { effect } // Create reactive effect
171
- export { mount } // Mount page module into container
172
- export { cleanup } // Tear down all bindings
170
+ export { signal } // Create reactive signal
171
+ export { state } // Create deep reactive state proxy
172
+ export { zeneffect } // Canonical reactive effect subscription
173
+ export { zenMount } // Canonical component bootstrap lifecycle hook
174
+ export { zenWindow } // Canonical SSR-safe global window access
175
+ export { zenDocument } // Canonical SSR-safe global document access
176
+ export { hydrate } // Mount page module into container
177
+ export { cleanup } // Deterministic teardown of effects/listeners
173
178
  ```
174
179
 
175
- Four functions. No more.
180
+ ### Optional Secondary Aliases
181
+ For developer convenience, the runtime also exports optional, standard-named aliases. These exist purely as synonyms mapped to the canonical primitives:
182
+ ```js
183
+ export { effect } // Alias for zeneffect
184
+ export { mount } // Alias for zenMount
185
+ export { window } // Alias for zenWindow
186
+ export { document } // Alias for zenDocument
187
+ ```
176
188
 
177
189
  ---
178
190
 
package/dist/cleanup.js CHANGED
@@ -10,7 +10,7 @@
10
10
  // cleanup() removes everything.
11
11
  // Calling cleanup() twice is a no-op.
12
12
  // ---------------------------------------------------------------------------
13
- import { resetGlobalSideEffects } from './zeneffect.js';
13
+ import { resetGlobalSideEffects } from './side-effect-scope.js';
14
14
  /** @type {function[]} */
15
15
  const _disposers = [];
16
16
  /** @type {{ element: Element, event: string, handler: function }[]} */
@@ -46,8 +46,9 @@ export function _registerListener(element, event, handler) {
46
46
  */
47
47
  export function cleanup() {
48
48
  // Global zenMount/zenEffect registrations can be created outside hydrate's
49
- // disposer table (e.g. page-level component bootstraps). Even when cleanup
50
- // has already run once, we still need to reset that scope deterministically.
49
+ // disposer table (e.g. page-level component bootstraps). cleanup() is the
50
+ // full runtime teardown boundary, so that scope must be reset on every pass,
51
+ // including the first real cleanup.
51
52
  if (_cleaned) {
52
53
  resetGlobalSideEffects();
53
54
  return;
@@ -63,6 +64,8 @@ export function cleanup() {
63
64
  element.removeEventListener(event, handler);
64
65
  }
65
66
  _listeners.length = 0;
67
+ // 3. Dispose any top-level reactive work registered outside hydrate.
68
+ resetGlobalSideEffects();
66
69
  _cleaned = true;
67
70
  }
68
71
  /**
@@ -2,3 +2,10 @@ export function isZenithRuntimeError(error: any): boolean;
2
2
  export function createZenithRuntimeError(details: any, cause: any): Error;
3
3
  export function throwZenithRuntimeError(details: any, cause: any): void;
4
4
  export function rethrowZenithRuntimeError(error: any, fallback?: {}): void;
5
+ export const DOCS_LINKS: Readonly<{
6
+ eventBinding: "/docs/documentation/contracts/runtime-contract.md#event-bindings";
7
+ expressionScope: "/docs/documentation/reference/reactive-binding-model.md#expression-resolution";
8
+ markerTable: "/docs/documentation/reference/markers.md";
9
+ componentBootstrap: "/docs/documentation/contracts/runtime-contract.md#component-bootstrap";
10
+ refs: "/docs/documentation/reference/reactive-binding-model.md#refs-and-mount";
11
+ }>;
@@ -405,3 +405,10 @@ export function rethrowZenithRuntimeError(error, fallback = {}) {
405
405
  _reportRuntimeError(wrapped);
406
406
  throw wrapped;
407
407
  }
408
+ export const DOCS_LINKS = Object.freeze({
409
+ eventBinding: '/docs/documentation/contracts/runtime-contract.md#event-bindings',
410
+ expressionScope: '/docs/documentation/reference/reactive-binding-model.md#expression-resolution',
411
+ markerTable: '/docs/documentation/reference/markers.md',
412
+ componentBootstrap: '/docs/documentation/contracts/runtime-contract.md#component-bootstrap',
413
+ refs: '/docs/documentation/reference/reactive-binding-model.md#refs-and-mount'
414
+ });
@@ -0,0 +1,2 @@
1
+ export declare function createAutoTrackedEffect(effect: any, options: any, scope: any): () => void;
2
+ export declare function createExplicitDependencyEffect(effect: any, dependencies: any, scope: any): () => void;
@@ -0,0 +1,139 @@
1
+ // @ts-nocheck
2
+ import { runWithDependencyCollector } from './reactivity-core.js';
3
+ import { registerScopeDisposer, queueWhenScopeReady } from './side-effect-scope.js';
4
+ import { drainCleanupStack, applyCleanupResult, createEffectContext } from './effect-utils.js';
5
+ import { createScheduler } from './effect-scheduler.js';
6
+ let _effectIdCounter = 0;
7
+ export function createAutoTrackedEffect(effect, options, scope) {
8
+ let disposed = false;
9
+ const activeSubscriptions = new Map();
10
+ const runCleanups = [];
11
+ _effectIdCounter += 1;
12
+ const effectId = _effectIdCounter;
13
+ function registerCleanup(cleanup) {
14
+ if (typeof cleanup !== 'function') {
15
+ throw new Error('[Zenith Runtime] cleanup(fn) requires a function');
16
+ }
17
+ runCleanups.push(cleanup);
18
+ }
19
+ function runEffectNow() {
20
+ if (disposed || !scope || scope.disposed) {
21
+ return;
22
+ }
23
+ drainCleanupStack(runCleanups);
24
+ const nextDependenciesById = new Map();
25
+ runWithDependencyCollector((source) => {
26
+ if (!source || typeof source.subscribe !== 'function') {
27
+ return;
28
+ }
29
+ const reactiveId = Number.isInteger(source.__zenith_id) ? source.__zenith_id : 0;
30
+ if (!nextDependenciesById.has(reactiveId)) {
31
+ nextDependenciesById.set(reactiveId, source);
32
+ }
33
+ }, () => {
34
+ const result = effect(createEffectContext(registerCleanup));
35
+ applyCleanupResult(result, registerCleanup);
36
+ });
37
+ const nextDependencies = Array.from(nextDependenciesById.values()).sort((left, right) => {
38
+ const leftId = Number.isInteger(left.__zenith_id) ? left.__zenith_id : 0;
39
+ const rightId = Number.isInteger(right.__zenith_id) ? right.__zenith_id : 0;
40
+ return leftId - rightId;
41
+ });
42
+ const nextSet = new Set(nextDependencies);
43
+ for (const [dependency, unsubscribe] of activeSubscriptions.entries()) {
44
+ if (nextSet.has(dependency)) {
45
+ continue;
46
+ }
47
+ if (typeof unsubscribe === 'function') {
48
+ unsubscribe();
49
+ }
50
+ activeSubscriptions.delete(dependency);
51
+ }
52
+ for (let i = 0; i < nextDependencies.length; i++) {
53
+ const dependency = nextDependencies[i];
54
+ if (activeSubscriptions.has(dependency)) {
55
+ continue;
56
+ }
57
+ const unsubscribe = dependency.subscribe(() => {
58
+ scheduler.schedule();
59
+ });
60
+ activeSubscriptions.set(dependency, typeof unsubscribe === 'function' ? unsubscribe : () => { });
61
+ }
62
+ void effectId;
63
+ }
64
+ const scheduler = createScheduler(runEffectNow, options);
65
+ function disposeEffect() {
66
+ if (disposed) {
67
+ return;
68
+ }
69
+ disposed = true;
70
+ scheduler.cancel();
71
+ for (const unsubscribe of activeSubscriptions.values()) {
72
+ if (typeof unsubscribe === 'function') {
73
+ unsubscribe();
74
+ }
75
+ }
76
+ activeSubscriptions.clear();
77
+ drainCleanupStack(runCleanups);
78
+ }
79
+ registerScopeDisposer(scope, disposeEffect);
80
+ queueWhenScopeReady(scope, () => scheduler.schedule());
81
+ return disposeEffect;
82
+ }
83
+ export function createExplicitDependencyEffect(effect, dependencies, scope) {
84
+ if (!Array.isArray(dependencies)) {
85
+ throw new Error('[Zenith Runtime] zeneffect(deps, fn) requires an array of dependencies');
86
+ }
87
+ if (dependencies.length === 0) {
88
+ throw new Error('[Zenith Runtime] zeneffect(deps, fn) requires at least one dependency');
89
+ }
90
+ if (typeof effect !== 'function') {
91
+ throw new Error('[Zenith Runtime] zeneffect(deps, fn) requires a function');
92
+ }
93
+ let disposed = false;
94
+ const runCleanups = [];
95
+ function registerCleanup(cleanup) {
96
+ if (typeof cleanup !== 'function') {
97
+ throw new Error('[Zenith Runtime] cleanup(fn) requires a function');
98
+ }
99
+ runCleanups.push(cleanup);
100
+ }
101
+ function runEffectNow() {
102
+ if (disposed || !scope || scope.disposed) {
103
+ return;
104
+ }
105
+ drainCleanupStack(runCleanups);
106
+ const result = effect(createEffectContext(registerCleanup));
107
+ applyCleanupResult(result, registerCleanup);
108
+ }
109
+ const unsubscribers = dependencies.map((dep, index) => {
110
+ if (!dep || typeof dep.subscribe !== 'function') {
111
+ throw new Error(`[Zenith Runtime] zeneffect dependency at index ${index} must expose subscribe(fn)`);
112
+ }
113
+ return dep.subscribe(() => {
114
+ if (scope?.mountReady === true) {
115
+ runEffectNow();
116
+ return;
117
+ }
118
+ queueWhenScopeReady(scope, runEffectNow);
119
+ });
120
+ });
121
+ if (scope?.mountReady === true) {
122
+ runEffectNow();
123
+ }
124
+ else {
125
+ queueWhenScopeReady(scope, runEffectNow);
126
+ }
127
+ const dispose = () => {
128
+ if (disposed) {
129
+ return;
130
+ }
131
+ disposed = true;
132
+ for (let i = 0; i < unsubscribers.length; i++) {
133
+ unsubscribers[i]();
134
+ }
135
+ drainCleanupStack(runCleanups);
136
+ };
137
+ registerScopeDisposer(scope, dispose);
138
+ return dispose;
139
+ }
@@ -0,0 +1,4 @@
1
+ export declare function createScheduler(runNow: any, options: any): {
2
+ schedule: () => void;
3
+ cancel: () => void;
4
+ };
@@ -0,0 +1,88 @@
1
+ // @ts-nocheck
2
+ export function createScheduler(runNow, options) {
3
+ let microtaskQueued = false;
4
+ let debounceTimer = null;
5
+ let throttleTimer = null;
6
+ let rafHandle = null;
7
+ let lastRunAt = 0;
8
+ function clearScheduledWork() {
9
+ if (debounceTimer !== null) {
10
+ clearTimeout(debounceTimer);
11
+ debounceTimer = null;
12
+ }
13
+ if (throttleTimer !== null) {
14
+ clearTimeout(throttleTimer);
15
+ throttleTimer = null;
16
+ }
17
+ if (rafHandle !== null) {
18
+ if (typeof cancelAnimationFrame === 'function') {
19
+ cancelAnimationFrame(rafHandle);
20
+ }
21
+ else {
22
+ clearTimeout(rafHandle);
23
+ }
24
+ rafHandle = null;
25
+ }
26
+ microtaskQueued = false;
27
+ }
28
+ function invokeNow() {
29
+ microtaskQueued = false;
30
+ debounceTimer = null;
31
+ throttleTimer = null;
32
+ rafHandle = null;
33
+ lastRunAt = Date.now();
34
+ runNow();
35
+ }
36
+ function schedule() {
37
+ if (options.debounceMs > 0) {
38
+ if (debounceTimer !== null) {
39
+ clearTimeout(debounceTimer);
40
+ }
41
+ debounceTimer = setTimeout(invokeNow, options.debounceMs);
42
+ return;
43
+ }
44
+ if (options.throttleMs > 0) {
45
+ const now = Date.now();
46
+ const elapsed = now - lastRunAt;
47
+ if (lastRunAt === 0 || elapsed >= options.throttleMs) {
48
+ invokeNow();
49
+ return;
50
+ }
51
+ if (throttleTimer !== null) {
52
+ return;
53
+ }
54
+ throttleTimer = setTimeout(invokeNow, options.throttleMs - elapsed);
55
+ return;
56
+ }
57
+ if (options.raf) {
58
+ if (rafHandle !== null) {
59
+ if (typeof cancelAnimationFrame === 'function') {
60
+ cancelAnimationFrame(rafHandle);
61
+ }
62
+ else {
63
+ clearTimeout(rafHandle);
64
+ }
65
+ }
66
+ if (typeof requestAnimationFrame === 'function') {
67
+ rafHandle = requestAnimationFrame(invokeNow);
68
+ }
69
+ else {
70
+ rafHandle = setTimeout(invokeNow, 16);
71
+ }
72
+ return;
73
+ }
74
+ if (options.flush === 'sync') {
75
+ invokeNow();
76
+ return;
77
+ }
78
+ if (microtaskQueued) {
79
+ return;
80
+ }
81
+ microtaskQueued = true;
82
+ queueMicrotask(invokeNow);
83
+ }
84
+ return {
85
+ schedule,
86
+ cancel: clearScheduledWork
87
+ };
88
+ }
@@ -0,0 +1,19 @@
1
+ export declare function normalizeDelay(value: any, fieldName: any): number;
2
+ export declare function normalizeEffectOptions(options: any): {
3
+ debounceMs: number;
4
+ throttleMs: number;
5
+ raf: boolean;
6
+ flush: string;
7
+ };
8
+ export declare function drainCleanupStack(cleanups: any): void;
9
+ export declare function applyCleanupResult(result: any, registerCleanup: any): void;
10
+ export declare function createMountContext(registerCleanup: any): {
11
+ cleanup: any;
12
+ };
13
+ export declare function createEffectContext(registerCleanup: any): {
14
+ cleanup: any;
15
+ timeout(callback: any, delayMs?: number): NodeJS.Timeout;
16
+ raf(callback: any): number | NodeJS.Timeout;
17
+ debounce(callback: any, delayMs: any): (...args: any[]) => void;
18
+ throttle(callback: any, delayMs: any): (...args: any[]) => void;
19
+ };
@@ -0,0 +1,160 @@
1
+ // @ts-nocheck
2
+ const DEFAULT_EFFECT_OPTIONS = {
3
+ debounceMs: 0,
4
+ throttleMs: 0,
5
+ raf: false,
6
+ flush: 'post'
7
+ };
8
+ export function normalizeDelay(value, fieldName) {
9
+ if (value === undefined || value === null) {
10
+ return 0;
11
+ }
12
+ if (!Number.isFinite(value) || value < 0) {
13
+ throw new Error(`[Zenith Runtime] zenEffect options.${fieldName} must be a non-negative number`);
14
+ }
15
+ return Math.floor(value);
16
+ }
17
+ export function normalizeEffectOptions(options) {
18
+ if (options === undefined || options === null) {
19
+ return DEFAULT_EFFECT_OPTIONS;
20
+ }
21
+ if (!options || typeof options !== 'object' || Array.isArray(options)) {
22
+ throw new Error('[Zenith Runtime] zenEffect(effect, options) requires options object when provided');
23
+ }
24
+ const normalized = {
25
+ debounceMs: normalizeDelay(options.debounceMs, 'debounceMs'),
26
+ throttleMs: normalizeDelay(options.throttleMs, 'throttleMs'),
27
+ raf: options.raf === true,
28
+ flush: options.flush === 'sync' ? 'sync' : 'post'
29
+ };
30
+ if (options.flush !== undefined && options.flush !== 'sync' && options.flush !== 'post') {
31
+ throw new Error('[Zenith Runtime] zenEffect options.flush must be "post" or "sync"');
32
+ }
33
+ const schedulingModes = (normalized.debounceMs > 0 ? 1 : 0) +
34
+ (normalized.throttleMs > 0 ? 1 : 0) +
35
+ (normalized.raf ? 1 : 0);
36
+ if (schedulingModes > 1) {
37
+ throw new Error('[Zenith Runtime] zenEffect options may use only one scheduler: debounceMs, throttleMs, or raf');
38
+ }
39
+ return normalized;
40
+ }
41
+ export function drainCleanupStack(cleanups) {
42
+ for (let i = cleanups.length - 1; i >= 0; i--) {
43
+ const cleanup = cleanups[i];
44
+ if (typeof cleanup !== 'function') {
45
+ continue;
46
+ }
47
+ try {
48
+ cleanup();
49
+ }
50
+ catch {
51
+ }
52
+ }
53
+ cleanups.length = 0;
54
+ }
55
+ export function applyCleanupResult(result, registerCleanup) {
56
+ if (typeof result === 'function') {
57
+ registerCleanup(result);
58
+ return;
59
+ }
60
+ if (result && typeof result === 'object' && typeof result.cleanup === 'function') {
61
+ registerCleanup(result.cleanup);
62
+ }
63
+ }
64
+ function requireFunction(callback, label) {
65
+ if (typeof callback !== 'function') {
66
+ throw new Error(`[Zenith Runtime] ${label} requires callback function`);
67
+ }
68
+ }
69
+ export function createMountContext(registerCleanup) {
70
+ return {
71
+ cleanup: registerCleanup
72
+ };
73
+ }
74
+ export function createEffectContext(registerCleanup) {
75
+ return {
76
+ cleanup: registerCleanup,
77
+ timeout(callback, delayMs = 0) {
78
+ requireFunction(callback, 'zenEffect context.timeout(callback, delayMs)');
79
+ const timeoutId = setTimeout(callback, normalizeDelay(delayMs, 'timeout'));
80
+ registerCleanup(() => clearTimeout(timeoutId));
81
+ return timeoutId;
82
+ },
83
+ raf(callback) {
84
+ requireFunction(callback, 'zenEffect context.raf(callback)');
85
+ if (typeof requestAnimationFrame === 'function') {
86
+ const frameId = requestAnimationFrame(callback);
87
+ registerCleanup(() => cancelAnimationFrame(frameId));
88
+ return frameId;
89
+ }
90
+ const timeoutId = setTimeout(callback, 16);
91
+ registerCleanup(() => clearTimeout(timeoutId));
92
+ return timeoutId;
93
+ },
94
+ debounce(callback, delayMs) {
95
+ requireFunction(callback, 'zenEffect context.debounce(callback, delayMs)');
96
+ const waitMs = normalizeDelay(delayMs, 'debounce');
97
+ let timeoutId = null;
98
+ const wrapped = (...args) => {
99
+ if (timeoutId !== null) {
100
+ clearTimeout(timeoutId);
101
+ }
102
+ timeoutId = setTimeout(() => {
103
+ timeoutId = null;
104
+ callback(...args);
105
+ }, waitMs);
106
+ };
107
+ registerCleanup(() => {
108
+ if (timeoutId !== null) {
109
+ clearTimeout(timeoutId);
110
+ timeoutId = null;
111
+ }
112
+ });
113
+ return wrapped;
114
+ },
115
+ throttle(callback, delayMs) {
116
+ requireFunction(callback, 'zenEffect context.throttle(callback, delayMs)');
117
+ const waitMs = normalizeDelay(delayMs, 'throttle');
118
+ let timeoutId = null;
119
+ let lastRun = 0;
120
+ let pendingArgs = null;
121
+ const invoke = (args) => {
122
+ lastRun = Date.now();
123
+ callback(...args);
124
+ };
125
+ const wrapped = (...args) => {
126
+ const now = Date.now();
127
+ const elapsed = now - lastRun;
128
+ if (lastRun === 0 || elapsed >= waitMs) {
129
+ if (timeoutId !== null) {
130
+ clearTimeout(timeoutId);
131
+ timeoutId = null;
132
+ }
133
+ pendingArgs = null;
134
+ invoke(args);
135
+ return;
136
+ }
137
+ pendingArgs = args;
138
+ if (timeoutId !== null) {
139
+ return;
140
+ }
141
+ timeoutId = setTimeout(() => {
142
+ timeoutId = null;
143
+ if (pendingArgs) {
144
+ const next = pendingArgs;
145
+ pendingArgs = null;
146
+ invoke(next);
147
+ }
148
+ }, waitMs - elapsed);
149
+ };
150
+ registerCleanup(() => {
151
+ if (timeoutId !== null) {
152
+ clearTimeout(timeoutId);
153
+ timeoutId = null;
154
+ }
155
+ pendingArgs = null;
156
+ });
157
+ return wrapped;
158
+ }
159
+ };
160
+ }
package/dist/env.d.ts CHANGED
@@ -1,2 +1,14 @@
1
+ /** Canonical public access to window */
1
2
  export declare function zenWindow(): Window | null;
3
+ /** Canonical public access to document */
2
4
  export declare function zenDocument(): Document | null;
5
+ /**
6
+ * @alias zenWindow
7
+ * @description Optional secondary alias for the canonical zenWindow primitive.
8
+ */
9
+ export declare function window(): Window | null;
10
+ /**
11
+ * @alias zenDocument
12
+ * @description Optional secondary alias for the canonical zenDocument primitive.
13
+ */
14
+ export declare function document(): Document | null;
package/dist/env.js CHANGED
@@ -4,9 +4,25 @@
4
4
  // SSR-safe access to window and document. Returns null when not in browser.
5
5
  // Use zenWindow() / zenDocument() instead of direct window/document access.
6
6
  // ---------------------------------------------------------------------------
7
+ /** Canonical public access to window */
7
8
  export function zenWindow() {
8
- return typeof window === 'undefined' ? null : window;
9
+ return typeof globalThis.window === 'undefined' ? null : globalThis.window;
9
10
  }
11
+ /** Canonical public access to document */
10
12
  export function zenDocument() {
11
- return typeof document === 'undefined' ? null : document;
13
+ return typeof globalThis.document === 'undefined' ? null : globalThis.document;
14
+ }
15
+ /**
16
+ * @alias zenWindow
17
+ * @description Optional secondary alias for the canonical zenWindow primitive.
18
+ */
19
+ export function window() {
20
+ return zenWindow();
21
+ }
22
+ /**
23
+ * @alias zenDocument
24
+ * @description Optional secondary alias for the canonical zenDocument primitive.
25
+ */
26
+ export function document() {
27
+ return zenDocument();
12
28
  }
package/dist/events.d.ts CHANGED
@@ -1,8 +1 @@
1
- /**
2
- * Bind an event listener to a DOM element.
3
- *
4
- * @param {Element} element - The DOM element
5
- * @param {string} eventName - The event name (e.g. "click")
6
- * @param {() => any} exprFn - Pre-bound expression function that must resolve to a handler
7
- */
8
- export function bindEvent(element: Element, eventName: string, exprFn: () => any): void;
1
+ export function bindEventMarkers(context: any): void;