@zenithbuild/runtime 0.5.0-beta.2.6 → 0.6.2

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.
@@ -1,5 +1,8 @@
1
1
  # Zenith Runtime V0 Hydration Contract
2
2
 
3
+ Canonical public docs: `../zenith-docs/documentation/contracts/hydration-contract.md`
4
+
5
+
3
6
  Status: FROZEN (V0)
4
7
 
5
8
  This contract locks hydration and reactivity boundaries for Zenith V0.
package/README.md CHANGED
@@ -5,6 +5,12 @@
5
5
 
6
6
  The core runtime library for the Zenith framework.
7
7
 
8
+ ## Canonical Docs
9
+
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`
13
+
8
14
  ## Overview
9
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.
10
16
 
@@ -1,5 +1,8 @@
1
1
  # RUNTIME_CONTRACT.md — Sealed Runtime Interface
2
2
 
3
+ Canonical public docs: `../zenith-docs/documentation/contracts/runtime-contract.md`
4
+
5
+
3
6
  > **This document is a legal boundary.**
4
7
  > The runtime is a consumer of bundler output.
5
8
  > It does not reinterpret, normalize, or extend.
package/dist/env.js ADDED
@@ -0,0 +1,22 @@
1
+ // ---------------------------------------------------------------------------
2
+ // env.js — Zenith Runtime canonical environment accessors
3
+ // ---------------------------------------------------------------------------
4
+ // SSR-safe access to window and document. Returns null when not in browser.
5
+ // Use zenWindow() / zenDocument() instead of direct window/document access.
6
+ // ---------------------------------------------------------------------------
7
+
8
+ /**
9
+ * SSR-safe window accessor.
10
+ * @returns {Window | null}
11
+ */
12
+ export function zenWindow() {
13
+ return typeof window === 'undefined' ? null : window;
14
+ }
15
+
16
+ /**
17
+ * SSR-safe document accessor.
18
+ * @returns {Document | null}
19
+ */
20
+ export function zenDocument() {
21
+ return typeof document === 'undefined' ? null : document;
22
+ }
package/dist/hydrate.js CHANGED
@@ -13,6 +13,7 @@ import { signal } from './signal.js';
13
13
  import { state } from './state.js';
14
14
  import {
15
15
  zeneffect,
16
+ zenEffect,
16
17
  zenMount,
17
18
  createSideEffectScope,
18
19
  activateSideEffectScope,
@@ -32,8 +33,6 @@ const BOOLEAN_ATTRIBUTES = new Set([
32
33
  const STRICT_MEMBER_CHAIN_LITERAL_RE = /^(?:true|false|null|undefined|[A-Za-z_$][A-Za-z0-9_$]*(\.[A-Za-z_$][A-Za-z0-9_$]*)*)$/;
33
34
  const UNSAFE_MEMBER_KEYS = new Set(['__proto__', 'prototype', 'constructor']);
34
35
 
35
- const COMPILED_LITERAL_CACHE = new Map();
36
-
37
36
  /**
38
37
  * Hydrate a pre-rendered DOM tree using explicit payload tables.
39
38
  *
@@ -43,6 +42,7 @@ const COMPILED_LITERAL_CACHE = new Map();
43
42
  * expressions: Array<{ marker_index: number, signal_index?: number|null, state_index?: number|null, component_instance?: string|null, component_binding?: string|null, literal?: string|null }>,
44
43
  * markers: Array<{ index: number, kind: 'text' | 'attr' | 'event', selector: string, attr?: string }>,
45
44
  * events: Array<{ index: number, event: string, selector: string }>,
45
+ * refs?: Array<{ index: number, state_index: number, selector: string }>,
46
46
  * state_values: Array<*>,
47
47
  * state_keys?: Array<string>,
48
48
  * signals: Array<{ id: number, kind: 'signal', state_index: number }>,
@@ -55,7 +55,22 @@ export function hydrate(payload) {
55
55
  try {
56
56
  const normalized = _validatePayload(payload);
57
57
  _deepFreezePayload(payload);
58
- const { root, expressions, markers, events, stateValues, stateKeys, signals, components, route, params, ssrData, props } = normalized;
58
+ const {
59
+ root,
60
+ expressions,
61
+ markers,
62
+ events,
63
+ refs,
64
+ stateValues,
65
+ stateKeys,
66
+ signals,
67
+ components,
68
+ route,
69
+ params,
70
+ ssrData,
71
+ props,
72
+ exprFns
73
+ } = normalized;
59
74
 
60
75
  const componentBindings = Object.create(null);
61
76
 
@@ -72,6 +87,23 @@ export function hydrate(payload) {
72
87
  signalMap.set(i, candidate);
73
88
  }
74
89
 
90
+ const hydratedRefs = [];
91
+ for (let i = 0; i < refs.length; i++) {
92
+ const refBinding = refs[i];
93
+ const targetRef = stateValues[refBinding.state_index];
94
+ const nodes = _resolveNodes(root, refBinding.selector, refBinding.index, 'ref');
95
+ targetRef.current = nodes[0] || null;
96
+ hydratedRefs.push(targetRef);
97
+ }
98
+
99
+ if (hydratedRefs.length > 0) {
100
+ _registerDisposer(() => {
101
+ for (let i = 0; i < hydratedRefs.length; i++) {
102
+ hydratedRefs[i].current = null;
103
+ }
104
+ });
105
+ }
106
+
75
107
  for (let i = 0; i < components.length; i++) {
76
108
  const component = components[i];
77
109
  const resolvedProps = Object.freeze(_resolveComponentProps(component.props || [], signalMap, {
@@ -149,7 +181,8 @@ export function hydrate(payload) {
149
181
  params,
150
182
  ssrData,
151
183
  marker.kind,
152
- props
184
+ props,
185
+ exprFns
153
186
  );
154
187
  _applyMarkerValue(nodes, marker, value);
155
188
  }
@@ -176,7 +209,8 @@ export function hydrate(payload) {
176
209
  params,
177
210
  ssrData,
178
211
  marker.kind,
179
- props
212
+ props,
213
+ exprFns
180
214
  );
181
215
  _applyMarkerValue(nodes, marker, value);
182
216
  }
@@ -184,13 +218,17 @@ export function hydrate(payload) {
184
218
  const dependentMarkersBySignal = new Map();
185
219
  for (let i = 0; i < expressions.length; i++) {
186
220
  const expression = expressions[i];
187
- if (!Number.isInteger(expression.signal_index)) {
221
+ const signalIndices = _resolveExpressionSignalIndices(expression);
222
+ if (signalIndices.length === 0) {
188
223
  continue;
189
224
  }
190
- if (!dependentMarkersBySignal.has(expression.signal_index)) {
191
- dependentMarkersBySignal.set(expression.signal_index, []);
225
+ for (let j = 0; j < signalIndices.length; j++) {
226
+ const signalIndex = signalIndices[j];
227
+ if (!dependentMarkersBySignal.has(signalIndex)) {
228
+ dependentMarkersBySignal.set(signalIndex, []);
229
+ }
230
+ dependentMarkersBySignal.get(signalIndex).push(expression.marker_index);
192
231
  }
193
- dependentMarkersBySignal.get(expression.signal_index).push(expression.marker_index);
194
232
  }
195
233
 
196
234
  for (const [signalId, markerIndexes] of dependentMarkersBySignal.entries()) {
@@ -246,6 +284,7 @@ export function hydrate(payload) {
246
284
  }
247
285
 
248
286
  const eventIndices = new Set();
287
+ const escDispatchEntries = [];
249
288
  for (let i = 0; i < events.length; i++) {
250
289
  const eventBinding = events[i];
251
290
  if (eventIndices.has(eventBinding.index)) {
@@ -262,7 +301,9 @@ export function hydrate(payload) {
262
301
  componentBindings,
263
302
  params,
264
303
  ssrData,
265
- 'event'
304
+ 'event',
305
+ props || {},
306
+ exprFns
266
307
  );
267
308
  if (typeof handler !== 'function') {
268
309
  throwZenithRuntimeError({
@@ -291,11 +332,67 @@ export function hydrate(payload) {
291
332
  });
292
333
  }
293
334
  };
335
+ if (eventBinding.event === 'esc') {
336
+ escDispatchEntries.push({
337
+ node,
338
+ handler: wrappedHandler
339
+ });
340
+ continue;
341
+ }
294
342
  node.addEventListener(eventBinding.event, wrappedHandler);
295
343
  _registerListener(node, eventBinding.event, wrappedHandler);
296
344
  }
297
345
  }
298
346
 
347
+ if (escDispatchEntries.length > 0) {
348
+ const doc = root && root.ownerDocument ? root.ownerDocument : (typeof document !== 'undefined' ? document : null);
349
+ if (doc && typeof doc.addEventListener === 'function') {
350
+ const escDispatchListener = function zenithEscDispatch(event) {
351
+ if (!event || event.key !== 'Escape') {
352
+ return;
353
+ }
354
+
355
+ const activeElement = doc.activeElement || null;
356
+ let targetEntry = null;
357
+
358
+ if (activeElement && activeElement !== doc.body && activeElement !== doc.documentElement) {
359
+ for (let i = escDispatchEntries.length - 1; i >= 0; i--) {
360
+ const entry = escDispatchEntries[i];
361
+ if (!entry || !entry.node) {
362
+ continue;
363
+ }
364
+ if (!entry.node.isConnected) {
365
+ continue;
366
+ }
367
+ if (typeof entry.node.contains === 'function' && entry.node.contains(activeElement)) {
368
+ targetEntry = entry;
369
+ break;
370
+ }
371
+ }
372
+ }
373
+
374
+ if (!targetEntry && (activeElement === null || activeElement === doc.body || activeElement === doc.documentElement)) {
375
+ for (let i = escDispatchEntries.length - 1; i >= 0; i--) {
376
+ const entry = escDispatchEntries[i];
377
+ if (!entry || !entry.node || !entry.node.isConnected) {
378
+ continue;
379
+ }
380
+ targetEntry = entry;
381
+ break;
382
+ }
383
+ }
384
+
385
+ if (!targetEntry) {
386
+ return;
387
+ }
388
+
389
+ return targetEntry.handler.call(targetEntry.node, event);
390
+ };
391
+ doc.addEventListener('keydown', escDispatchListener);
392
+ _registerListener(doc, 'keydown', escDispatchListener);
393
+ }
394
+ }
395
+
299
396
  return cleanup;
300
397
  } catch (error) {
301
398
  rethrowZenithRuntimeError(error, {
@@ -336,6 +433,8 @@ function _validatePayload(payload) {
336
433
  throw new Error('[Zenith Runtime] hydrate(payload) requires events[]');
337
434
  }
338
435
 
436
+ const refs = Array.isArray(payload.refs) ? payload.refs : [];
437
+
339
438
  const stateValues = payload.state_values;
340
439
  if (!Array.isArray(stateValues)) {
341
440
  throw new Error('[Zenith Runtime] hydrate(payload) requires state_values[]');
@@ -365,6 +464,7 @@ function _validatePayload(payload) {
365
464
  const ssrData = payload.ssr_data && typeof payload.ssr_data === 'object'
366
465
  ? payload.ssr_data
367
466
  : {};
467
+ const exprFns = Array.isArray(payload.expr_fns) ? payload.expr_fns : [];
368
468
 
369
469
  if (markers.length !== expressions.length) {
370
470
  throw new Error(
@@ -385,6 +485,23 @@ function _validatePayload(payload) {
385
485
  `[Zenith Runtime] expression table out of order at position ${i}: marker_index=${expression.marker_index}`
386
486
  );
387
487
  }
488
+ if (expression.fn_index !== undefined && expression.fn_index !== null) {
489
+ if (!Number.isInteger(expression.fn_index) || expression.fn_index < 0) {
490
+ throw new Error(`[Zenith Runtime] expression at position ${i} has invalid fn_index`);
491
+ }
492
+ }
493
+ if (expression.signal_indices !== undefined) {
494
+ if (!Array.isArray(expression.signal_indices)) {
495
+ throw new Error(`[Zenith Runtime] expression at position ${i} must provide signal_indices[]`);
496
+ }
497
+ for (let j = 0; j < expression.signal_indices.length; j++) {
498
+ if (!Number.isInteger(expression.signal_indices[j]) || expression.signal_indices[j] < 0) {
499
+ throw new Error(
500
+ `[Zenith Runtime] expression at position ${i} has invalid signal_indices[${j}]`
501
+ );
502
+ }
503
+ }
504
+ }
388
505
  }
389
506
 
390
507
  for (let i = 0; i < markers.length; i++) {
@@ -425,6 +542,34 @@ function _validatePayload(payload) {
425
542
  }
426
543
  }
427
544
 
545
+ for (let i = 0; i < refs.length; i++) {
546
+ const refBinding = refs[i];
547
+ if (!refBinding || typeof refBinding !== 'object' || Array.isArray(refBinding)) {
548
+ throw new Error(`[Zenith Runtime] ref binding at position ${i} must be an object`);
549
+ }
550
+ if (!Number.isInteger(refBinding.index) || refBinding.index < 0) {
551
+ throw new Error(`[Zenith Runtime] ref binding at position ${i} requires non-negative index`);
552
+ }
553
+ if (
554
+ !Number.isInteger(refBinding.state_index) ||
555
+ refBinding.state_index < 0 ||
556
+ refBinding.state_index >= stateValues.length
557
+ ) {
558
+ throw new Error(
559
+ `[Zenith Runtime] ref binding at position ${i} has out-of-bounds state_index`
560
+ );
561
+ }
562
+ if (typeof refBinding.selector !== 'string' || refBinding.selector.length === 0) {
563
+ throw new Error(`[Zenith Runtime] ref binding at position ${i} requires selector`);
564
+ }
565
+ const candidate = stateValues[refBinding.state_index];
566
+ if (!candidate || typeof candidate !== 'object' || !Object.prototype.hasOwnProperty.call(candidate, 'current')) {
567
+ throw new Error(
568
+ `[Zenith Runtime] ref binding at position ${i} must resolve to a ref-like object`
569
+ );
570
+ }
571
+ }
572
+
428
573
  for (let i = 0; i < signals.length; i++) {
429
574
  const signalDescriptor = signals[i];
430
575
  if (!signalDescriptor || typeof signalDescriptor !== 'object' || Array.isArray(signalDescriptor)) {
@@ -478,6 +623,7 @@ function _validatePayload(payload) {
478
623
  for (let i = 0; i < expressions.length; i++) Object.freeze(expressions[i]);
479
624
  for (let i = 0; i < markers.length; i++) Object.freeze(markers[i]);
480
625
  for (let i = 0; i < events.length; i++) Object.freeze(events[i]);
626
+ for (let i = 0; i < refs.length; i++) Object.freeze(refs[i]);
481
627
  for (let i = 0; i < signals.length; i++) Object.freeze(signals[i]);
482
628
  for (let i = 0; i < components.length; i++) {
483
629
  const c = components[i];
@@ -501,6 +647,7 @@ function _validatePayload(payload) {
501
647
  Object.freeze(expressions);
502
648
  Object.freeze(markers);
503
649
  Object.freeze(events);
650
+ Object.freeze(refs);
504
651
  Object.freeze(signals);
505
652
  Object.freeze(components);
506
653
 
@@ -509,6 +656,7 @@ function _validatePayload(payload) {
509
656
  expressions,
510
657
  markers,
511
658
  events,
659
+ refs,
512
660
  stateValues,
513
661
  stateKeys,
514
662
  signals,
@@ -516,7 +664,8 @@ function _validatePayload(payload) {
516
664
  route,
517
665
  params: Object.freeze(params),
518
666
  ssrData: Object.freeze(ssrData),
519
- props: Object.freeze(props)
667
+ props: Object.freeze(props),
668
+ exprFns: Object.freeze(exprFns)
520
669
  };
521
670
 
522
671
  return Object.freeze(validatedPayload);
@@ -580,7 +729,40 @@ function _resolveNodes(root, selector, index, kind) {
580
729
  return nodes;
581
730
  }
582
731
 
583
- function _evaluateExpression(binding, stateValues, stateKeys, signalMap, componentBindings, params, ssrData, mode, props) {
732
+ function _resolveExpressionSignalIndices(binding) {
733
+ if (!binding || typeof binding !== 'object') {
734
+ return [];
735
+ }
736
+ if (Array.isArray(binding.signal_indices) && binding.signal_indices.length > 0) {
737
+ return [...new Set(binding.signal_indices.filter((value) => Number.isInteger(value) && value >= 0))];
738
+ }
739
+ if (Number.isInteger(binding.signal_index) && binding.signal_index >= 0) {
740
+ return [binding.signal_index];
741
+ }
742
+ return [];
743
+ }
744
+
745
+ function _evaluateExpression(binding, stateValues, stateKeys, signalMap, componentBindings, params, ssrData, mode, props, exprFns) {
746
+ if (binding.fn_index != null && binding.fn_index !== undefined) {
747
+ const fns = Array.isArray(exprFns) ? exprFns : [];
748
+ const fn = fns[binding.fn_index];
749
+ if (typeof fn === 'function') {
750
+ return fn({
751
+ signalMap,
752
+ params,
753
+ ssrData,
754
+ props: props || {},
755
+ componentBindings,
756
+ zenhtml: _zenhtml,
757
+ fragment(html) {
758
+ return {
759
+ __zenith_fragment: true,
760
+ html: html === null || html === undefined || html === false ? '' : String(html)
761
+ };
762
+ }
763
+ });
764
+ }
765
+ }
584
766
  if (binding.signal_index !== null && binding.signal_index !== undefined) {
585
767
  const signalValue = signalMap.get(binding.signal_index);
586
768
  if (!signalValue || typeof signalValue.get !== 'function') {
@@ -651,17 +833,9 @@ function _evaluateExpression(binding, stateValues, stateKeys, signalMap, compone
651
833
  return props || {};
652
834
  }
653
835
 
654
- const evaluated = _evaluateLiteralExpression(
655
- trimmedLiteral,
656
- stateValues,
657
- stateKeys,
658
- params,
659
- ssrData,
660
- mode,
661
- props
662
- );
663
- if (evaluated !== UNRESOLVED_LITERAL) {
664
- return evaluated;
836
+ const primitiveValue = _resolvePrimitiveLiteral(trimmedLiteral);
837
+ if (primitiveValue !== UNRESOLVED_LITERAL) {
838
+ return primitiveValue;
665
839
  }
666
840
  if (_isLikelyExpressionLiteral(trimmedLiteral)) {
667
841
  throwZenithRuntimeError({
@@ -784,33 +958,39 @@ function _resolveStrictMemberChainLiteral(
784
958
  return cursor;
785
959
  }
786
960
 
787
- function _evaluateLiteralExpression(expression, stateValues, stateKeys, params, ssrData, mode, props) {
788
- const scope = _buildLiteralScope(stateValues, stateKeys, params, ssrData, mode, props);
789
- const scopeKeys = Object.keys(scope);
790
- const scopeValues = scopeKeys.map((key) => scope[key]);
791
- const rewritten = _rewriteMarkupLiterals(expression);
792
- const cacheKey = `${scopeKeys.join('|')}::${rewritten}`;
961
+ function _resolvePrimitiveLiteral(literal) {
962
+ if (typeof literal !== 'string') {
963
+ return UNRESOLVED_LITERAL;
964
+ }
965
+ if (literal === 'true') return true;
966
+ if (literal === 'false') return false;
967
+ if (literal === 'null') return null;
968
+ if (literal === 'undefined') return undefined;
793
969
 
794
- let evaluator = COMPILED_LITERAL_CACHE.get(cacheKey);
795
- if (!evaluator) {
970
+ if (/^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?$/.test(literal)) {
971
+ return Number(literal);
972
+ }
973
+
974
+ if (literal.length >= 2 && literal.startsWith('"') && literal.endsWith('"')) {
796
975
  try {
797
- evaluator = Function(
798
- ...scopeKeys,
799
- `return (${rewritten});`
800
- );
801
- } catch (err) {
802
- if (isZenithRuntimeError(err)) throw err;
976
+ return JSON.parse(literal);
977
+ } catch {
803
978
  return UNRESOLVED_LITERAL;
804
979
  }
805
- COMPILED_LITERAL_CACHE.set(cacheKey, evaluator);
806
980
  }
807
981
 
808
- try {
809
- return evaluator(...scopeValues);
810
- } catch (err) {
811
- if (isZenithRuntimeError(err)) throw err;
812
- return UNRESOLVED_LITERAL;
982
+ if (literal.length >= 2 && literal.startsWith('\'') && literal.endsWith('\'')) {
983
+ return literal
984
+ .slice(1, -1)
985
+ .replace(/\\\\/g, '\\')
986
+ .replace(/\\'/g, '\'');
813
987
  }
988
+
989
+ if (literal.length >= 2 && literal.startsWith('`') && literal.endsWith('`')) {
990
+ return literal.slice(1, -1);
991
+ }
992
+
993
+ return UNRESOLVED_LITERAL;
814
994
  }
815
995
 
816
996
  function _buildLiteralScope(stateValues, stateKeys, params, ssrData, mode, props) {
package/dist/index.js CHANGED
@@ -2,3 +2,5 @@ export { signal } from './signal.js';
2
2
  export { state } from './state.js';
3
3
  export { zeneffect } from './zeneffect.js';
4
4
  export { hydrate } from './hydrate.js';
5
+ export { zenWindow, zenDocument } from './env.js';
6
+ export { zenOn, zenResize, collectRefs } from './platform.js';
@@ -0,0 +1,102 @@
1
+ // ---------------------------------------------------------------------------
2
+ // platform.js — Zenith Runtime canonical DOM/platform helpers
3
+ // ---------------------------------------------------------------------------
4
+ // zenOn: SSR-safe event subscription with disposer
5
+ // zenResize: window resize handler with rAF throttle
6
+ // collectRefs: deterministic null-filtered ref collection
7
+ // ---------------------------------------------------------------------------
8
+
9
+ import { zenWindow } from './env.js';
10
+
11
+ /**
12
+ * SSR-safe event subscription. Returns disposer.
13
+ * @param {EventTarget | null} target
14
+ * @param {string} eventName
15
+ * @param {EventListener} handler
16
+ * @param {AddEventListenerOptions | boolean} [options]
17
+ * @returns {() => void}
18
+ */
19
+ export function zenOn(target, eventName, handler, options) {
20
+ if (!target || typeof target.addEventListener !== 'function') {
21
+ return () => {};
22
+ }
23
+ target.addEventListener(eventName, handler, options);
24
+ return () => {
25
+ target.removeEventListener(eventName, handler, options);
26
+ };
27
+ }
28
+
29
+ /**
30
+ * Window resize handler with requestAnimationFrame throttle.
31
+ * Returns disposer.
32
+ * @param {(size: { w: number; h: number }) => void} handler
33
+ * @returns {() => void}
34
+ */
35
+ export function zenResize(handler) {
36
+ const win = zenWindow();
37
+ if (!win || typeof win.addEventListener !== 'function') {
38
+ return () => {};
39
+ }
40
+ const hasRaf =
41
+ typeof win.requestAnimationFrame === 'function'
42
+ && typeof win.cancelAnimationFrame === 'function';
43
+ let scheduledId = null;
44
+ let lastW = Number.NaN;
45
+ let lastH = Number.NaN;
46
+
47
+ const schedule = (callback) => {
48
+ if (hasRaf) {
49
+ return win.requestAnimationFrame(callback);
50
+ }
51
+ return win.setTimeout(callback, 0);
52
+ };
53
+
54
+ const cancel = (id) => {
55
+ if (hasRaf) {
56
+ win.cancelAnimationFrame(id);
57
+ return;
58
+ }
59
+ win.clearTimeout(id);
60
+ };
61
+
62
+ function onResize() {
63
+ if (scheduledId !== null) return;
64
+ scheduledId = schedule(() => {
65
+ scheduledId = null;
66
+ const w = win.innerWidth;
67
+ const h = win.innerHeight;
68
+ if (w !== lastW || h !== lastH) {
69
+ lastW = w;
70
+ lastH = h;
71
+ handler({ w, h });
72
+ }
73
+ });
74
+ }
75
+
76
+ win.addEventListener('resize', onResize);
77
+ onResize();
78
+
79
+ return () => {
80
+ if (scheduledId !== null) {
81
+ cancel(scheduledId);
82
+ scheduledId = null;
83
+ }
84
+ win.removeEventListener('resize', onResize);
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Deterministic null-filtered collection of ref.current values.
90
+ * @param {...{ current?: Element | null }} refs
91
+ * @returns {Element[]}
92
+ */
93
+ export function collectRefs(...refs) {
94
+ const out = [];
95
+ for (let i = 0; i < refs.length; i++) {
96
+ const node = refs[i] && refs[i].current;
97
+ if (node && typeof node === 'object' && typeof node.nodeType === 'number') {
98
+ out.push(node);
99
+ }
100
+ }
101
+ return out;
102
+ }
package/dist/signal.js CHANGED
@@ -15,6 +15,7 @@
15
15
  // - No scheduler
16
16
  // - No async queue
17
17
  // ---------------------------------------------------------------------------
18
+ import { _nextReactiveId, _trackDependency } from './zeneffect.js';
18
19
 
19
20
  /**
20
21
  * Create a deterministic signal with explicit subscription semantics.
@@ -25,9 +26,12 @@
25
26
  export function signal(initialValue) {
26
27
  let value = initialValue;
27
28
  const subscribers = new Set();
29
+ const reactiveId = _nextReactiveId();
28
30
 
29
31
  return {
32
+ __zenith_id: reactiveId,
30
33
  get() {
34
+ _trackDependency(this);
31
35
  return value;
32
36
  },
33
37
  set(nextValue) {
package/dist/state.js CHANGED
@@ -37,9 +37,12 @@ function cloneSnapshot(value) {
37
37
  export function state(initialValue) {
38
38
  let current = Object.freeze(cloneSnapshot(initialValue));
39
39
  const subscribers = new Set();
40
+ const reactiveId = _nextReactiveId();
40
41
 
41
42
  return {
43
+ __zenith_id: reactiveId,
42
44
  get() {
45
+ _trackDependency(this);
43
46
  return current;
44
47
  },
45
48
  set(nextPatch) {
package/dist/template.js CHANGED
@@ -22,6 +22,8 @@ function buildRuntimeModuleSource() {
22
22
  const segments = [
23
23
  stripImports(readRuntimeSourceFile('zeneffect.js')),
24
24
  stripImports(readRuntimeSourceFile('ref.js')),
25
+ stripImports(readRuntimeSourceFile('env.js')),
26
+ stripImports(readRuntimeSourceFile('platform.js')),
25
27
  stripImports(readRuntimeSourceFile('signal.js')),
26
28
  stripImports(readRuntimeSourceFile('state.js')),
27
29
  stripImports(readRuntimeSourceFile('diagnostics.js')),
@@ -119,18 +121,101 @@ const RUNTIME_DEV_CLIENT_SOURCE = `(() => {
119
121
  });
120
122
  }
121
123
 
122
- function swapStylesheet(nextHref) {
124
+ let cssSwapEpoch = 0;
125
+
126
+ function withCacheBuster(nextHref) {
127
+ const separator = nextHref.includes('?') ? '&' : '?';
128
+ return nextHref + separator + '__zenith_dev=' + Date.now();
129
+ }
130
+
131
+ function isSameOriginStylesheet(href) {
132
+ if (typeof href !== 'string' || href.length === 0) {
133
+ return false;
134
+ }
135
+ if (href.startsWith('http://') || href.startsWith('https://')) {
136
+ try {
137
+ return new URL(href, window.location.href).origin === window.location.origin;
138
+ } catch {
139
+ return false;
140
+ }
141
+ }
142
+ return true;
143
+ }
144
+
145
+ function findPrimaryStylesheet() {
146
+ const links = Array.from(document.querySelectorAll('link[rel="stylesheet"]'))
147
+ .filter(function (link) {
148
+ return isSameOriginStylesheet(link.getAttribute('href') || '');
149
+ });
150
+ if (links.length === 0) {
151
+ return null;
152
+ }
153
+ const marked = links.find(function (link) {
154
+ return link.getAttribute('data-zenith-dev-primary') === 'true';
155
+ });
156
+ if (marked) {
157
+ return marked;
158
+ }
159
+ links[0].setAttribute('data-zenith-dev-primary', 'true');
160
+ return links[0];
161
+ }
162
+
163
+ function scheduleCssRetry(previousHref, attempt) {
164
+ if (attempt >= 3) {
165
+ window.location.reload();
166
+ return;
167
+ }
168
+ const delayMs = (attempt + 1) * 100;
169
+ setTimeout(function () {
170
+ fetchDevState().then(function (statePayload) {
171
+ const href = statePayload && typeof statePayload.cssHref === 'string' && statePayload.cssHref.length > 0
172
+ ? statePayload.cssHref
173
+ : previousHref;
174
+ swapStylesheet(href, attempt + 1);
175
+ });
176
+ }, delayMs);
177
+ }
178
+
179
+ function swapStylesheet(nextHref, attempt) {
180
+ const tries = Number.isInteger(attempt) ? attempt : 0;
123
181
  if (typeof nextHref !== 'string' || nextHref.length === 0) {
124
182
  window.location.reload();
125
183
  return;
126
184
  }
127
- const links = Array.from(document.querySelectorAll('link[rel="stylesheet"]'));
128
- if (links.length === 0) {
185
+ const activeLink = findPrimaryStylesheet();
186
+ if (!activeLink) {
129
187
  window.location.reload();
130
188
  return;
131
189
  }
132
- const separator = nextHref.includes('?') ? '&' : '?';
133
- links[0].setAttribute('href', nextHref + separator + '__zenith_dev=' + Date.now());
190
+
191
+ const swapId = ++cssSwapEpoch;
192
+ const nextLink = activeLink.cloneNode(true);
193
+ nextLink.setAttribute('href', withCacheBuster(nextHref));
194
+ nextLink.removeAttribute('data-zenith-dev-primary');
195
+ nextLink.setAttribute('data-zenith-dev-pending', 'true');
196
+ activeLink.removeAttribute('data-zenith-dev-primary');
197
+
198
+ nextLink.addEventListener('load', function () {
199
+ if (swapId !== cssSwapEpoch) {
200
+ try { nextLink.remove(); } catch { }
201
+ return;
202
+ }
203
+ nextLink.removeAttribute('data-zenith-dev-pending');
204
+ nextLink.setAttribute('data-zenith-dev-primary', 'true');
205
+ try { activeLink.remove(); } catch { }
206
+ }, { once: true });
207
+
208
+ nextLink.addEventListener('error', function () {
209
+ if (swapId !== cssSwapEpoch) {
210
+ try { nextLink.remove(); } catch { }
211
+ return;
212
+ }
213
+ try { nextLink.remove(); } catch { }
214
+ activeLink.setAttribute('data-zenith-dev-primary', 'true');
215
+ scheduleCssRetry(nextHref, tries);
216
+ }, { once: true });
217
+
218
+ activeLink.insertAdjacentElement('afterend', nextLink);
134
219
  }
135
220
 
136
221
  const state = ensureDevState();
@@ -341,8 +426,10 @@ const RUNTIME_DEV_CLIENT_SOURCE = `(() => {
341
426
  appendLog('[css_update] ' + (payload.href || ''));
342
427
  emitDebug('css_update', payload);
343
428
  fetchDevState().then(function (statePayload) {
344
- if (statePayload && typeof statePayload.cssHref === 'string' && statePayload.cssHref.length > 0) {
429
+ if (statePayload) {
345
430
  updateInfo({ ...payload, ...statePayload });
431
+ }
432
+ if (statePayload && typeof statePayload.cssHref === 'string' && statePayload.cssHref.length > 0) {
346
433
  swapStylesheet(statePayload.cssHref);
347
434
  return;
348
435
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenithbuild/runtime",
3
- "version": "0.5.0-beta.2.6",
3
+ "version": "0.6.2",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "exports": {