@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
package/dist/zeneffect.js CHANGED
@@ -1,499 +1,26 @@
1
1
  // @ts-nocheck
2
- // ---------------------------------------------------------------------------
3
- // zeneffect.ts Zenith Runtime V0
4
- // ---------------------------------------------------------------------------
5
- // Explicit dependency effect primitive.
6
- //
7
- // API:
8
- // const dispose = zeneffect([countSignal], () => { ... });
9
- //
10
- // Constraints:
11
- // - Dependencies are explicit
12
- // - No implicit tracking context
13
- // - No scheduler
14
- // - No async queue
15
- // ---------------------------------------------------------------------------
16
- const DEFAULT_EFFECT_OPTIONS = {
17
- debounceMs: 0,
18
- throttleMs: 0,
19
- raf: false,
20
- flush: 'post'
21
- };
22
- let _activeDependencyCollector = null;
23
- let _reactiveIdCounter = 0;
24
- let _scopeIdCounter = 0;
25
- let _effectIdCounter = 0;
26
- export function _nextReactiveId() {
27
- _reactiveIdCounter += 1;
28
- return _reactiveIdCounter;
29
- }
30
- export function _trackDependency(source) {
31
- if (typeof _activeDependencyCollector === 'function') {
32
- _activeDependencyCollector(source);
33
- }
34
- }
35
- function createInternalScope(label, mountReady) {
36
- _scopeIdCounter += 1;
37
- return {
38
- __zenith_scope: true,
39
- id: _scopeIdCounter,
40
- label,
41
- mountReady: mountReady === true,
42
- disposed: false,
43
- pendingMounts: [],
44
- disposers: []
45
- };
46
- }
47
- let _globalScope = createInternalScope('global', true);
48
- function isScope(value) {
49
- return !!value && typeof value === 'object' && value.__zenith_scope === true;
50
- }
51
- function resolveScope(scopeOverride) {
52
- if (isScope(scopeOverride)) {
53
- return scopeOverride;
54
- }
55
- return _globalScope;
56
- }
57
- export function resetGlobalSideEffects() {
58
- disposeSideEffectScope(_globalScope);
59
- _globalScope = createInternalScope('global', true);
60
- }
61
- export function createSideEffectScope(label = 'anonymous') {
62
- return createInternalScope(label, false);
63
- }
64
- export function activateSideEffectScope(scope) {
65
- if (!isScope(scope) || scope.disposed || scope.mountReady) {
66
- return;
67
- }
68
- scope.mountReady = true;
69
- const pending = scope.pendingMounts.slice();
70
- scope.pendingMounts.length = 0;
71
- for (let i = 0; i < pending.length; i++) {
72
- const callback = pending[i];
73
- if (typeof callback !== 'function')
74
- continue;
75
- try {
76
- callback();
77
- }
78
- catch {
79
- // failed effect mounts should not crash sibling nodes
80
- }
81
- }
82
- }
83
- function registerScopeDisposer(scope, disposer) {
84
- if (typeof disposer !== 'function') {
85
- return () => { };
86
- }
87
- if (!scope || scope.disposed) {
88
- disposer();
89
- return () => { };
90
- }
91
- scope.disposers.push(disposer);
92
- return function unregisterScopeDisposer() {
93
- if (!scope || scope.disposed) {
94
- return;
95
- }
96
- const index = scope.disposers.indexOf(disposer);
97
- if (index >= 0) {
98
- scope.disposers.splice(index, 1);
99
- }
100
- };
101
- }
102
- export function disposeSideEffectScope(scope) {
103
- if (!scope || scope.disposed) {
104
- return;
105
- }
106
- scope.disposed = true;
107
- const disposers = scope.disposers.slice();
108
- scope.disposers.length = 0;
109
- scope.pendingMounts.length = 0;
110
- for (let i = disposers.length - 1; i >= 0; i--) {
111
- const disposer = disposers[i];
112
- if (typeof disposer !== 'function') {
113
- continue;
114
- }
115
- try {
116
- disposer();
117
- }
118
- catch {
119
- // cleanup failures must never break teardown flow
120
- }
121
- }
122
- }
123
- function normalizeDelay(value, fieldName) {
124
- if (value === undefined || value === null) {
125
- return 0;
126
- }
127
- if (!Number.isFinite(value) || value < 0) {
128
- throw new Error(`[Zenith Runtime] zenEffect options.${fieldName} must be a non-negative number`);
129
- }
130
- return Math.floor(value);
131
- }
132
- function normalizeEffectOptions(options) {
133
- if (options === undefined || options === null) {
134
- return DEFAULT_EFFECT_OPTIONS;
135
- }
136
- if (!options || typeof options !== 'object' || Array.isArray(options)) {
137
- throw new Error('[Zenith Runtime] zenEffect(effect, options) requires options object when provided');
138
- }
139
- const normalized = {
140
- debounceMs: normalizeDelay(options.debounceMs, 'debounceMs'),
141
- throttleMs: normalizeDelay(options.throttleMs, 'throttleMs'),
142
- raf: options.raf === true,
143
- flush: options.flush === 'sync' ? 'sync' : 'post'
144
- };
145
- if (options.flush !== undefined && options.flush !== 'sync' && options.flush !== 'post') {
146
- throw new Error('[Zenith Runtime] zenEffect options.flush must be "post" or "sync"');
147
- }
148
- const schedulingModes = (normalized.debounceMs > 0 ? 1 : 0) +
149
- (normalized.throttleMs > 0 ? 1 : 0) +
150
- (normalized.raf ? 1 : 0);
151
- if (schedulingModes > 1) {
152
- throw new Error('[Zenith Runtime] zenEffect options may use only one scheduler: debounceMs, throttleMs, or raf');
153
- }
154
- return normalized;
155
- }
156
- function drainCleanupStack(cleanups) {
157
- for (let i = cleanups.length - 1; i >= 0; i--) {
158
- const cleanup = cleanups[i];
159
- if (typeof cleanup !== 'function') {
160
- continue;
161
- }
162
- try {
163
- cleanup();
164
- }
165
- catch {
166
- // swallow cleanup errors to preserve deterministic teardown
167
- }
168
- }
169
- cleanups.length = 0;
170
- }
171
- function applyCleanupResult(result, registerCleanup) {
172
- if (typeof result === 'function') {
173
- registerCleanup(result);
174
- return;
175
- }
176
- if (result && typeof result === 'object' && typeof result.cleanup === 'function') {
177
- registerCleanup(result.cleanup);
178
- }
179
- }
180
- function requireFunction(callback, label) {
181
- if (typeof callback !== 'function') {
182
- throw new Error(`[Zenith Runtime] ${label} requires callback function`);
183
- }
184
- }
185
- function createMountContext(registerCleanup) {
186
- return {
187
- cleanup: registerCleanup
188
- };
189
- }
190
- function createEffectContext(registerCleanup) {
191
- return {
192
- cleanup: registerCleanup,
193
- timeout(callback, delayMs = 0) {
194
- requireFunction(callback, 'zenEffect context.timeout(callback, delayMs)');
195
- const timeoutId = setTimeout(callback, normalizeDelay(delayMs, 'timeout'));
196
- registerCleanup(() => clearTimeout(timeoutId));
197
- return timeoutId;
198
- },
199
- raf(callback) {
200
- requireFunction(callback, 'zenEffect context.raf(callback)');
201
- if (typeof requestAnimationFrame === 'function') {
202
- const frameId = requestAnimationFrame(callback);
203
- registerCleanup(() => cancelAnimationFrame(frameId));
204
- return frameId;
205
- }
206
- const timeoutId = setTimeout(callback, 16);
207
- registerCleanup(() => clearTimeout(timeoutId));
208
- return timeoutId;
209
- },
210
- debounce(callback, delayMs) {
211
- requireFunction(callback, 'zenEffect context.debounce(callback, delayMs)');
212
- const waitMs = normalizeDelay(delayMs, 'debounce');
213
- let timeoutId = null;
214
- const wrapped = (...args) => {
215
- if (timeoutId !== null) {
216
- clearTimeout(timeoutId);
217
- }
218
- timeoutId = setTimeout(() => {
219
- timeoutId = null;
220
- callback(...args);
221
- }, waitMs);
222
- };
223
- registerCleanup(() => {
224
- if (timeoutId !== null) {
225
- clearTimeout(timeoutId);
226
- timeoutId = null;
227
- }
228
- });
229
- return wrapped;
230
- },
231
- throttle(callback, delayMs) {
232
- requireFunction(callback, 'zenEffect context.throttle(callback, delayMs)');
233
- const waitMs = normalizeDelay(delayMs, 'throttle');
234
- let timeoutId = null;
235
- let lastRun = 0;
236
- let pendingArgs = null;
237
- const invoke = (args) => {
238
- lastRun = Date.now();
239
- callback(...args);
240
- };
241
- const wrapped = (...args) => {
242
- const now = Date.now();
243
- const elapsed = now - lastRun;
244
- if (lastRun === 0 || elapsed >= waitMs) {
245
- if (timeoutId !== null) {
246
- clearTimeout(timeoutId);
247
- timeoutId = null;
248
- }
249
- pendingArgs = null;
250
- invoke(args);
251
- return;
252
- }
253
- pendingArgs = args;
254
- if (timeoutId !== null) {
255
- return;
256
- }
257
- timeoutId = setTimeout(() => {
258
- timeoutId = null;
259
- if (pendingArgs) {
260
- const next = pendingArgs;
261
- pendingArgs = null;
262
- invoke(next);
263
- }
264
- }, waitMs - elapsed);
265
- };
266
- registerCleanup(() => {
267
- if (timeoutId !== null) {
268
- clearTimeout(timeoutId);
269
- timeoutId = null;
270
- }
271
- pendingArgs = null;
272
- });
273
- return wrapped;
274
- }
275
- };
276
- }
277
- function createScheduler(runNow, options) {
278
- let microtaskQueued = false;
279
- let debounceTimer = null;
280
- let throttleTimer = null;
281
- let rafHandle = null;
282
- let lastRunAt = 0;
283
- function clearScheduledWork() {
284
- if (debounceTimer !== null) {
285
- clearTimeout(debounceTimer);
286
- debounceTimer = null;
287
- }
288
- if (throttleTimer !== null) {
289
- clearTimeout(throttleTimer);
290
- throttleTimer = null;
291
- }
292
- if (rafHandle !== null) {
293
- if (typeof cancelAnimationFrame === 'function') {
294
- cancelAnimationFrame(rafHandle);
295
- }
296
- else {
297
- clearTimeout(rafHandle);
298
- }
299
- rafHandle = null;
300
- }
301
- microtaskQueued = false;
302
- }
303
- function invokeNow() {
304
- microtaskQueued = false;
305
- debounceTimer = null;
306
- throttleTimer = null;
307
- rafHandle = null;
308
- lastRunAt = Date.now();
309
- runNow();
310
- }
311
- function schedule() {
312
- if (options.debounceMs > 0) {
313
- if (debounceTimer !== null) {
314
- clearTimeout(debounceTimer);
315
- }
316
- debounceTimer = setTimeout(invokeNow, options.debounceMs);
317
- return;
318
- }
319
- if (options.throttleMs > 0) {
320
- const now = Date.now();
321
- const elapsed = now - lastRunAt;
322
- if (lastRunAt === 0 || elapsed >= options.throttleMs) {
323
- invokeNow();
324
- return;
325
- }
326
- if (throttleTimer !== null) {
327
- return;
328
- }
329
- throttleTimer = setTimeout(invokeNow, options.throttleMs - elapsed);
330
- return;
331
- }
332
- if (options.raf) {
333
- if (rafHandle !== null) {
334
- if (typeof cancelAnimationFrame === 'function') {
335
- cancelAnimationFrame(rafHandle);
336
- }
337
- else {
338
- clearTimeout(rafHandle);
339
- }
340
- }
341
- if (typeof requestAnimationFrame === 'function') {
342
- rafHandle = requestAnimationFrame(invokeNow);
343
- }
344
- else {
345
- rafHandle = setTimeout(invokeNow, 16);
346
- }
347
- return;
348
- }
349
- if (options.flush === 'sync') {
350
- invokeNow();
351
- return;
352
- }
353
- if (microtaskQueued) {
354
- return;
355
- }
356
- microtaskQueued = true;
357
- queueMicrotask(invokeNow);
358
- }
359
- return {
360
- schedule,
361
- cancel: clearScheduledWork
362
- };
363
- }
364
- function queueWhenScopeReady(scope, callback) {
365
- if (!scope || scope.disposed) {
366
- return;
367
- }
368
- if (scope.mountReady) {
369
- callback();
370
- return;
371
- }
372
- scope.pendingMounts.push(callback);
373
- }
374
- function createAutoTrackedEffect(effect, options, scope) {
375
- let disposed = false;
376
- const activeSubscriptions = new Map();
377
- const runCleanups = [];
378
- _effectIdCounter += 1;
379
- const effectId = _effectIdCounter;
380
- function registerCleanup(cleanup) {
381
- if (typeof cleanup !== 'function') {
382
- throw new Error('[Zenith Runtime] cleanup(fn) requires a function');
383
- }
384
- runCleanups.push(cleanup);
385
- }
386
- function runEffectNow() {
387
- if (disposed || !scope || scope.disposed) {
388
- return;
389
- }
390
- drainCleanupStack(runCleanups);
391
- const nextDependenciesById = new Map();
392
- const previousCollector = _activeDependencyCollector;
393
- _activeDependencyCollector = (source) => {
394
- if (!source || typeof source.subscribe !== 'function') {
395
- return;
396
- }
397
- const reactiveId = Number.isInteger(source.__zenith_id) ? source.__zenith_id : 0;
398
- if (!nextDependenciesById.has(reactiveId)) {
399
- nextDependenciesById.set(reactiveId, source);
400
- }
401
- };
402
- try {
403
- const result = effect(createEffectContext(registerCleanup));
404
- applyCleanupResult(result, registerCleanup);
405
- }
406
- finally {
407
- _activeDependencyCollector = previousCollector;
408
- }
409
- const nextDependencies = Array.from(nextDependenciesById.values()).sort((left, right) => {
410
- const leftId = Number.isInteger(left.__zenith_id) ? left.__zenith_id : 0;
411
- const rightId = Number.isInteger(right.__zenith_id) ? right.__zenith_id : 0;
412
- return leftId - rightId;
413
- });
414
- const nextSet = new Set(nextDependencies);
415
- for (const [dependency, unsubscribe] of activeSubscriptions.entries()) {
416
- if (nextSet.has(dependency)) {
417
- continue;
418
- }
419
- if (typeof unsubscribe === 'function') {
420
- unsubscribe();
421
- }
422
- activeSubscriptions.delete(dependency);
423
- }
424
- for (let i = 0; i < nextDependencies.length; i++) {
425
- const dependency = nextDependencies[i];
426
- if (activeSubscriptions.has(dependency)) {
427
- continue;
428
- }
429
- const unsubscribe = dependency.subscribe(() => {
430
- scheduler.schedule();
431
- });
432
- activeSubscriptions.set(dependency, typeof unsubscribe === 'function' ? unsubscribe : () => { });
433
- }
434
- void effectId;
435
- }
436
- const scheduler = createScheduler(runEffectNow, options);
437
- function disposeEffect() {
438
- if (disposed) {
439
- return;
440
- }
441
- disposed = true;
442
- scheduler.cancel();
443
- for (const unsubscribe of activeSubscriptions.values()) {
444
- if (typeof unsubscribe === 'function') {
445
- unsubscribe();
446
- }
447
- }
448
- activeSubscriptions.clear();
449
- drainCleanupStack(runCleanups);
450
- }
451
- registerScopeDisposer(scope, disposeEffect);
452
- queueWhenScopeReady(scope, () => scheduler.schedule());
453
- return disposeEffect;
454
- }
455
- function createExplicitDependencyEffect(effect, dependencies, scope) {
456
- if (!Array.isArray(dependencies)) {
457
- throw new Error('[Zenith Runtime] zeneffect(deps, fn) requires an array of dependencies');
458
- }
459
- if (dependencies.length === 0) {
460
- throw new Error('[Zenith Runtime] zeneffect(deps, fn) requires at least one dependency');
461
- }
462
- if (typeof effect !== 'function') {
463
- throw new Error('[Zenith Runtime] zeneffect(deps, fn) requires a function');
464
- }
465
- const unsubscribers = dependencies.map((dep, index) => {
466
- if (!dep || typeof dep.subscribe !== 'function') {
467
- throw new Error(`[Zenith Runtime] zeneffect dependency at index ${index} must expose subscribe(fn)`);
468
- }
469
- return dep.subscribe(() => {
470
- effect();
471
- });
472
- });
473
- effect();
474
- return function dispose() {
475
- for (let i = 0; i < unsubscribers.length; i++) {
476
- unsubscribers[i]();
477
- }
478
- };
479
- }
2
+ import { createAutoTrackedEffect, createExplicitDependencyEffect } from './effect-runtime.js';
3
+ import { normalizeEffectOptions } from './effect-utils.js';
4
+ import { createMountEffect } from './mount-runtime.js';
5
+ import { resolveSideEffectScope } from './side-effect-scope.js';
6
+ export { _nextReactiveId, _trackDependency } from './reactivity-core.js';
7
+ export { resetGlobalSideEffects, createSideEffectScope, activateSideEffectScope, disposeSideEffectScope } from './side-effect-scope.js';
480
8
  export function zenEffect(effect, options = null, scopeOverride = null) {
481
9
  if (typeof effect !== 'function') {
482
10
  throw new Error('[Zenith Runtime] zenEffect(effect) requires a callback function');
483
11
  }
484
- const opts = normalizeEffectOptions(options);
485
- const scope = resolveScope(scopeOverride);
486
- return createAutoTrackedEffect(effect, opts, scope);
12
+ return createAutoTrackedEffect(effect, normalizeEffectOptions(options), resolveSideEffectScope(scopeOverride));
487
13
  }
488
14
  export function zeneffect(effectOrDependencies, optionsOrEffect, scopeOverride = null) {
15
+ const scope = resolveSideEffectScope(scopeOverride);
489
16
  if (Array.isArray(effectOrDependencies)) {
490
17
  if (typeof optionsOrEffect !== 'function') {
491
18
  throw new Error('[Zenith Runtime] zeneffect(deps, effect) requires an effect function');
492
19
  }
493
- return createExplicitDependencyEffect(optionsOrEffect, effectOrDependencies, resolveScope(scopeOverride));
20
+ return createExplicitDependencyEffect(optionsOrEffect, effectOrDependencies, scope);
494
21
  }
495
22
  if (typeof effectOrDependencies === 'function') {
496
- return createAutoTrackedEffect(effectOrDependencies, normalizeEffectOptions(optionsOrEffect), resolveScope(scopeOverride));
23
+ return createAutoTrackedEffect(effectOrDependencies, normalizeEffectOptions(optionsOrEffect), scope);
497
24
  }
498
25
  throw new Error('[Zenith Runtime] zeneffect() invalid arguments. Expected (effect) or (dependencies, effect)');
499
26
  }
@@ -501,40 +28,19 @@ export function zenMount(callback, scopeOverride = null) {
501
28
  if (typeof callback !== 'function') {
502
29
  throw new Error('[Zenith Runtime] zenMount(callback) requires a function');
503
30
  }
504
- const scope = resolveScope(scopeOverride);
505
- const cleanups = [];
506
- let executed = false;
507
- let registeredDisposer = null;
508
- function registerCleanup(fn) {
509
- if (typeof fn !== 'function') {
510
- throw new Error('[Zenith Runtime] cleanup(fn) requires a function');
511
- }
512
- cleanups.push(fn);
513
- }
514
- function runMount() {
515
- if (scope.disposed || executed) {
516
- return;
517
- }
518
- executed = true;
519
- try {
520
- const result = callback(createMountContext(registerCleanup));
521
- applyCleanupResult(result, registerCleanup);
522
- }
523
- catch (error) {
524
- // Unhandled mount errors shouldn't crash hydration, but we log them
525
- console.error('[Zenith Runtime] Unhandled error during zenMount:', error);
526
- }
527
- registeredDisposer = registerScopeDisposer(scope, () => {
528
- drainCleanupStack(cleanups);
529
- });
530
- }
531
- queueWhenScopeReady(scope, runMount);
532
- return function dispose() {
533
- if (registeredDisposer) {
534
- registeredDisposer();
535
- }
536
- else {
537
- drainCleanupStack(cleanups);
538
- }
539
- };
31
+ return createMountEffect(callback, resolveSideEffectScope(scopeOverride));
32
+ }
33
+ /**
34
+ * @alias zeneffect
35
+ * @description Optional secondary alias for the canonical zeneffect primitive.
36
+ */
37
+ export function effect(effectOrDependencies, optionsOrEffect, scopeOverride = null) {
38
+ return zeneffect(effectOrDependencies, optionsOrEffect, scopeOverride);
39
+ }
40
+ /**
41
+ * @alias zenMount
42
+ * @description Optional secondary alias for the canonical zenMount primitive.
43
+ */
44
+ export function mount(callback, scopeOverride = null) {
45
+ return zenMount(callback, scopeOverride);
540
46
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenithbuild/runtime",
3
- "version": "0.7.3",
3
+ "version": "0.7.4",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "exports": {
@@ -31,12 +31,14 @@
31
31
  "scripts": {
32
32
  "build": "rm -rf dist && tsc -p tsconfig.build.json",
33
33
  "typecheck": "tsc -p tsconfig.json --noEmit",
34
- "test": "npm run build && NODE_OPTIONS=--experimental-vm-modules jest --config jest.config.js",
34
+ "test": "npm run build && bun test --preload ./bun-test-setup.js",
35
35
  "prepublishOnly": "npm run build"
36
36
  },
37
37
  "devDependencies": {
38
- "@types/node": "latest",
38
+ "@happy-dom/global-registrator": "^20.8.9",
39
39
  "@jest/globals": "^30.2.0",
40
+ "@types/node": "latest",
41
+ "happy-dom": "^20.8.9",
40
42
  "jest": "^30.2.0",
41
43
  "jest-environment-jsdom": "^30.2.0",
42
44
  "typescript": "^5.7.3"