@zenithbuild/runtime 0.6.5 → 0.6.7

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