@zenithbuild/runtime 0.7.2 → 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.
- package/HYDRATION_CONTRACT.md +1 -1
- package/LICENSE +21 -0
- package/README.md +16 -18
- package/RUNTIME_CONTRACT.md +24 -12
- package/dist/cleanup.js +6 -3
- package/dist/diagnostics.d.ts +7 -0
- package/dist/diagnostics.js +7 -0
- package/dist/effect-runtime.d.ts +2 -0
- package/dist/effect-runtime.js +139 -0
- package/dist/effect-scheduler.d.ts +4 -0
- package/dist/effect-scheduler.js +88 -0
- package/dist/effect-utils.d.ts +19 -0
- package/dist/effect-utils.js +160 -0
- package/dist/env.d.ts +12 -0
- package/dist/env.js +18 -2
- package/dist/events.d.ts +1 -8
- package/dist/events.js +108 -46
- package/dist/expressions.d.ts +14 -0
- package/dist/expressions.js +295 -0
- package/dist/fragment-patch.d.ts +12 -0
- package/dist/fragment-patch.js +118 -0
- package/dist/hydrate.d.ts +3 -117
- package/dist/hydrate.js +235 -1817
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/markup.d.ts +8 -0
- package/dist/markup.js +127 -0
- package/dist/mount-runtime.d.ts +1 -0
- package/dist/mount-runtime.js +39 -0
- package/dist/payload.d.ts +21 -0
- package/dist/payload.js +386 -0
- package/dist/reactivity-core.d.ts +3 -0
- package/dist/reactivity-core.js +22 -0
- package/dist/render.d.ts +3 -0
- package/dist/render.js +340 -0
- package/dist/scanner.d.ts +1 -0
- package/dist/scanner.js +61 -0
- package/dist/side-effect-scope.d.ts +16 -0
- package/dist/side-effect-scope.js +99 -0
- package/dist/signal.js +1 -1
- package/dist/state.js +1 -1
- package/dist/template-parser.d.ts +1 -0
- package/dist/template-parser.js +268 -0
- package/dist/template.js +10 -11
- package/dist/zeneffect.d.ts +12 -14
- package/dist/zeneffect.js +25 -519
- package/package.json +5 -3
package/HYDRATION_CONTRACT.md
CHANGED
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
|
-
# @
|
|
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:
|
|
11
|
-
- Hydration contract:
|
|
12
|
-
- Reactive binding model:
|
|
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
|
|
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**:
|
|
19
|
-
- **Hydration**:
|
|
20
|
-
- **
|
|
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
|
|
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.
|
package/RUNTIME_CONTRACT.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# RUNTIME_CONTRACT.md — Sealed Runtime Interface
|
|
2
2
|
|
|
3
|
-
Canonical public docs:
|
|
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-
|
|
120
|
+
- When a dependency signal changes, the effect is scheduled to re-run
|
|
121
121
|
|
|
122
|
-
### Constraints
|
|
122
|
+
### Scheduling & Execution Constraints
|
|
123
123
|
|
|
124
|
-
-
|
|
125
|
-
-
|
|
126
|
-
-
|
|
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
|
-
-
|
|
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 }
|
|
170
|
-
export {
|
|
171
|
-
export {
|
|
172
|
-
export {
|
|
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
|
-
|
|
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 './
|
|
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).
|
|
50
|
-
//
|
|
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
|
/**
|
package/dist/diagnostics.d.ts
CHANGED
|
@@ -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
|
+
}>;
|
package/dist/diagnostics.js
CHANGED
|
@@ -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,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,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;
|