tova 0.3.0 → 0.3.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.
- package/bin/tova.js +1401 -111
- package/package.json +4 -7
- package/src/analyzer/analyzer.js +831 -709
- package/src/analyzer/client-analyzer.js +191 -0
- package/src/analyzer/server-analyzer.js +467 -0
- package/src/analyzer/types.js +20 -4
- package/src/codegen/base-codegen.js +467 -109
- package/src/codegen/client-codegen.js +92 -42
- package/src/codegen/codegen.js +65 -5
- package/src/codegen/server-codegen.js +290 -36
- package/src/diagnostics/error-codes.js +255 -0
- package/src/diagnostics/formatter.js +150 -28
- package/src/docs/generator.js +390 -0
- package/src/lexer/lexer.js +305 -63
- package/src/lexer/tokens.js +19 -0
- package/src/lsp/server.js +892 -30
- package/src/parser/ast.js +81 -368
- package/src/parser/client-ast.js +138 -0
- package/src/parser/client-parser.js +504 -0
- package/src/parser/parser.js +491 -1064
- package/src/parser/server-ast.js +240 -0
- package/src/parser/server-parser.js +602 -0
- package/src/runtime/array-proto.js +32 -0
- package/src/runtime/embedded.js +1 -1
- package/src/runtime/reactivity.js +191 -10
- package/src/stdlib/advanced-collections.js +81 -0
- package/src/stdlib/inline.js +549 -6
- package/src/version.js +1 -1
package/src/runtime/embedded.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Auto-generated by scripts/embed-runtime.js — do not edit
|
|
2
2
|
|
|
3
|
-
export const REACTIVITY_SOURCE = "// Fine-grained reactivity system for Tova (signals-based)\n\nconst __DEV__ = typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production';\n\n// ─── DevTools hooks (zero-cost when disabled) ────────────\nlet __devtools_hooks = null;\nexport function __enableDevTools(hooks) {\n __devtools_hooks = hooks;\n}\n\nlet currentEffect = null;\nconst effectStack = [];\n\n// ─── Ownership System ─────────────────────────────────────\nlet currentOwner = null;\nconst ownerStack = [];\n\n// ─── Batching ────────────────────────────────────────────\n// Default: synchronous flush after each setter (backward compatible).\n// Inside batch(): effects are deferred and flushed once when batch ends.\n// This means setA(1); setB(2) causes 2 runs by default, but\n// batch(() => { setA(1); setB(2); }) causes only 1 run.\n\nlet pendingEffects = new Set();\nlet batchDepth = 0;\nlet flushing = false;\n\nfunction flush() {\n if (flushing) return; // prevent re-entrant flush\n flushing = true;\n let iterations = 0;\n try {\n while (pendingEffects.size > 0) {\n if (++iterations > 100) {\n console.error('Tova: Possible infinite loop in reactive updates (>100 flush iterations). Aborting.');\n pendingEffects.clear();\n break;\n }\n const toRun = [...pendingEffects];\n pendingEffects.clear();\n for (const effect of toRun) {\n if (!effect._disposed) {\n effect();\n }\n }\n }\n } finally {\n flushing = false;\n }\n}\n\nexport function batch(fn) {\n batchDepth++;\n try {\n fn();\n } finally {\n batchDepth--;\n if (batchDepth === 0) {\n flush();\n }\n }\n}\n\n// ─── Ownership Root ──────────────────────────────────────\n\nexport function createRoot(fn) {\n const root = {\n _children: [],\n _disposed: false,\n _cleanups: [],\n _contexts: null,\n _owner: currentOwner,\n dispose() {\n if (root._disposed) return;\n root._disposed = true;\n // Dispose children in reverse order\n for (let i = root._children.length - 1; i >= 0; i--) {\n const child = root._children[i];\n if (typeof child.dispose === 'function') child.dispose();\n }\n root._children.length = 0;\n // Run cleanups in reverse order\n for (let i = root._cleanups.length - 1; i >= 0; i--) {\n try { root._cleanups[i](); } catch (e) { console.error('Tova: root cleanup error:', e); }\n }\n root._cleanups.length = 0;\n }\n };\n ownerStack.push(currentOwner);\n currentOwner = root;\n try {\n return fn(root.dispose.bind(root));\n } finally {\n currentOwner = ownerStack.pop();\n }\n}\n\n// ─── Dependency Cleanup ──────────────────────────────────\n\nfunction cleanupDeps(subscriber) {\n if (subscriber._deps) {\n for (const depSet of subscriber._deps) {\n depSet.delete(subscriber);\n }\n subscriber._deps.clear();\n }\n}\n\nfunction trackDep(subscriber, subscriberSet) {\n subscriberSet.add(subscriber);\n if (!subscriber._deps) subscriber._deps = new Set();\n subscriber._deps.add(subscriberSet);\n}\n\n// ─── Signals ─────────────────────────────────────────────\n\nexport function createSignal(initialValue, name) {\n let value = initialValue;\n const subscribers = new Set();\n let signalId = null;\n\n if (__devtools_hooks) {\n signalId = __devtools_hooks.onSignalCreate(\n () => value,\n (v) => setter(v),\n name,\n );\n }\n\n function getter() {\n if (currentEffect) {\n trackDep(currentEffect, subscribers);\n }\n return value;\n }\n\n function setter(newValue) {\n if (typeof newValue === 'function') {\n newValue = newValue(value);\n }\n if (value !== newValue) {\n const oldValue = value;\n value = newValue;\n if (__devtools_hooks && signalId != null) {\n __devtools_hooks.onSignalUpdate(signalId, oldValue, newValue);\n }\n for (const sub of [...subscribers]) {\n if (sub._isComputed) {\n sub(); // propagate dirty flags synchronously through computed graph\n } else {\n pendingEffects.add(sub);\n }\n }\n if (batchDepth === 0) {\n flush();\n }\n }\n }\n\n return [getter, setter];\n}\n\n// ─── Effects ─────────────────────────────────────────────\n\nfunction runCleanups(effect) {\n if (effect._cleanup) {\n try { effect._cleanup(); } catch (e) { console.error('Tova: cleanup error:', e); }\n effect._cleanup = null;\n }\n if (effect._cleanups && effect._cleanups.length > 0) {\n for (const cb of effect._cleanups) {\n try { cb(); } catch (e) { console.error('Tova: cleanup error:', e); }\n }\n effect._cleanups = [];\n }\n}\n\nexport function createEffect(fn) {\n function effect() {\n if (effect._running) return;\n if (effect._disposed) return;\n effect._running = true;\n\n // Run cleanups from previous execution\n runCleanups(effect);\n\n // Remove from all previous dependency subscriber sets\n cleanupDeps(effect);\n\n effectStack.push(effect);\n currentEffect = effect;\n const startTime = __devtools_hooks && typeof performance !== 'undefined' ? performance.now() : 0;\n try {\n const result = fn();\n // If effect returns a function, use as cleanup\n if (typeof result === 'function') {\n effect._cleanup = result;\n }\n } catch (e) {\n console.error('Tova: Error in effect:', e);\n if (currentErrorHandler) {\n currentErrorHandler(e);\n }\n } finally {\n if (__devtools_hooks) {\n const duration = typeof performance !== 'undefined' ? performance.now() - startTime : 0;\n __devtools_hooks.onEffectRun(effect, duration);\n }\n effectStack.pop();\n currentEffect = effectStack[effectStack.length - 1] || null;\n effect._running = false;\n }\n }\n\n effect._deps = new Set();\n effect._running = false;\n effect._disposed = false;\n effect._cleanup = null;\n effect._cleanups = [];\n effect._owner = currentOwner;\n\n if (__devtools_hooks) {\n __devtools_hooks.onEffectCreate(effect);\n }\n\n if (currentOwner && !currentOwner._disposed) {\n currentOwner._children.push(effect);\n }\n\n effect.dispose = function () {\n effect._disposed = true;\n runCleanups(effect);\n cleanupDeps(effect);\n pendingEffects.delete(effect);\n // Remove from owner's children\n if (effect._owner) {\n const idx = effect._owner._children.indexOf(effect);\n if (idx >= 0) effect._owner._children.splice(idx, 1);\n }\n };\n\n // Run immediately (synchronous first run)\n effect();\n return effect;\n}\n\n// ─── Computed (lazy/pull-based for glitch-free reads) ────\n\nexport function createComputed(fn) {\n let value;\n let dirty = true;\n const subscribers = new Set();\n\n // notify is called synchronously when a source signal changes.\n // It marks the computed dirty and propagates to downstream subscribers.\n function notify() {\n if (!dirty) {\n dirty = true;\n for (const sub of [...subscribers]) {\n if (sub._isComputed) {\n sub(); // cascade dirty flags synchronously\n } else {\n pendingEffects.add(sub);\n }\n }\n }\n }\n\n notify._deps = new Set();\n notify._disposed = false;\n notify._isComputed = true;\n notify._owner = currentOwner;\n\n if (currentOwner && !currentOwner._disposed) {\n currentOwner._children.push(notify);\n }\n\n notify.dispose = function () {\n notify._disposed = true;\n cleanupDeps(notify);\n if (notify._owner) {\n const idx = notify._owner._children.indexOf(notify);\n if (idx >= 0) notify._owner._children.splice(idx, 1);\n }\n };\n\n function recompute() {\n cleanupDeps(notify);\n\n effectStack.push(notify);\n currentEffect = notify;\n try {\n value = fn();\n dirty = false;\n } finally {\n effectStack.pop();\n currentEffect = effectStack[effectStack.length - 1] || null;\n }\n }\n\n // Initial computation\n recompute();\n\n function getter() {\n if (currentEffect) {\n trackDep(currentEffect, subscribers);\n }\n if (dirty) {\n recompute();\n }\n return value;\n }\n\n return getter;\n}\n\n// ─── Lifecycle Hooks ─────────────────────────────────────\n\nexport function onMount(fn) {\n const owner = currentOwner;\n queueMicrotask(() => {\n const result = fn();\n if (typeof result === 'function' && owner && !owner._disposed) {\n owner._cleanups.push(result);\n }\n });\n}\n\nexport function onUnmount(fn) {\n if (currentOwner && !currentOwner._disposed) {\n currentOwner._cleanups.push(fn);\n }\n}\n\nexport function onCleanup(fn) {\n if (currentEffect) {\n if (!currentEffect._cleanups) currentEffect._cleanups = [];\n currentEffect._cleanups.push(fn);\n }\n}\n\n// ─── Untrack ─────────────────────────────────────────────\n// Run a function without tracking any signal reads (opt out of reactivity)\n\nexport function untrack(fn) {\n const prev = currentEffect;\n currentEffect = null;\n try {\n return fn();\n } finally {\n currentEffect = prev;\n }\n}\n\n// ─── Watch ───────────────────────────────────────────────\n// Watch a reactive expression, calling callback with (newValue, oldValue)\n// Returns a dispose function to stop watching.\n\nexport function watch(getter, callback, options = {}) {\n let oldValue = undefined;\n let initialized = false;\n\n const effect = createEffect(() => {\n const newValue = getter();\n if (initialized) {\n untrack(() => callback(newValue, oldValue));\n } else if (options.immediate) {\n untrack(() => callback(newValue, undefined));\n }\n oldValue = newValue;\n initialized = true;\n });\n\n return effect.dispose ? effect.dispose.bind(effect) : () => {\n effect._disposed = true;\n runCleanups(effect);\n cleanupDeps(effect);\n pendingEffects.delete(effect);\n };\n}\n\n// ─── Refs ────────────────────────────────────────────────\n\nexport function createRef(initialValue) {\n return { current: initialValue !== undefined ? initialValue : null };\n}\n\n// ─── Error Boundaries ────────────────────────────────────\n\n// Stack-based error handler for correct nested boundary propagation\nconst errorHandlerStack = [];\nlet currentErrorHandler = null;\n\nfunction pushErrorHandler(handler) {\n errorHandlerStack.push(currentErrorHandler);\n currentErrorHandler = handler;\n}\n\nfunction popErrorHandler() {\n currentErrorHandler = errorHandlerStack.pop() || null;\n}\n\n// Component name tracking for stack traces\nconst componentNameStack = [];\n\nexport function pushComponentName(name) {\n componentNameStack.push(name);\n}\n\nexport function popComponentName() {\n componentNameStack.pop();\n}\n\nfunction buildComponentStack() {\n return [...componentNameStack].reverse();\n}\n\nexport function createErrorBoundary(options = {}) {\n const { onError, onReset } = options;\n const [error, setError] = createSignal(null);\n\n function run(fn) {\n pushErrorHandler((e) => {\n const stack = buildComponentStack();\n if (e && typeof e === 'object') e.__tovaComponentStack = stack;\n setError(e);\n if (onError) onError({ error: e, componentStack: stack });\n });\n try {\n return fn();\n } catch (e) {\n const stack = buildComponentStack();\n if (e && typeof e === 'object') e.__tovaComponentStack = stack;\n setError(e);\n if (onError) onError({ error: e, componentStack: stack });\n return null;\n } finally {\n popErrorHandler();\n }\n }\n\n function reset() {\n setError(null);\n if (onReset) onReset();\n }\n\n return { error, run, reset };\n}\n\nexport function ErrorBoundary({ fallback, children, onError, onReset, retry = 0 }) {\n const [error, setError] = createSignal(null);\n const [retryCount, setRetryCount] = createSignal(0);\n\n function handleError(e) {\n const stack = buildComponentStack();\n if (e && typeof e === 'object') e.__tovaComponentStack = stack;\n if (retryCount() < retry) {\n setRetryCount(c => c + 1);\n setError(null); // clear to re-trigger render\n return;\n }\n setError(e);\n if (onError) onError({ error: e, componentStack: stack });\n }\n\n // Return a reactive wrapper that switches between children and fallback\n const childContent = children && children.length === 1 ? children[0] : tova_fragment(children || []);\n\n const vnode = {\n __tova: true,\n tag: '__dynamic',\n props: {},\n children: [],\n _fallback: fallback,\n _componentName: 'ErrorBoundary',\n _errorHandler: handleError, // Active during __dynamic effect render cycle\n compute: () => {\n const err = error();\n if (err) {\n // Render fallback — if fallback itself throws, propagate to parent boundary\n try {\n return typeof fallback === 'function'\n ? fallback({\n error: err,\n reset: () => {\n setRetryCount(0);\n setError(null);\n if (onReset) onReset();\n },\n })\n : fallback;\n } catch (fallbackError) {\n // Fallback threw — propagate to parent error boundary\n if (currentErrorHandler) {\n currentErrorHandler(fallbackError);\n }\n return null;\n }\n }\n return childContent;\n },\n };\n\n return vnode;\n}\n\n// ─── Dynamic Component ──────────────────────────────────\n// Renders a component dynamically based on a reactive signal.\n// Usage: Dynamic({ component: mySignal, ...props })\n\nexport function Dynamic({ component, ...rest }) {\n return {\n __tova: true,\n tag: '__dynamic',\n props: {},\n children: [],\n compute: () => {\n const comp = typeof component === 'function' && !component.__tova ? component() : component;\n if (!comp) return null;\n if (typeof comp === 'function') {\n return comp(rest);\n }\n return comp;\n },\n };\n}\n\n// ─── Portal ─────────────────────────────────────────────\n// Renders children into a different DOM target.\n// Usage: Portal({ target: \"#modal-root\", children })\n\nexport function Portal({ target, children }) {\n return {\n __tova: true,\n tag: '__portal',\n props: { target },\n children: children || [],\n };\n}\n\n// ─── Lazy ───────────────────────────────────────────────\n// Async component loading with optional fallback.\n// Usage: const LazyComp = lazy(() => import('./HeavyComponent.js'))\n\nexport function lazy(loader) {\n let resolved = null;\n let loadError = null;\n let promise = null;\n\n return function LazyWrapper(props) {\n if (resolved) {\n return resolved(props);\n }\n\n if (!promise) {\n promise = loader()\n .then(mod => {\n resolved = mod.default || mod;\n })\n .catch(e => { loadError = e; });\n }\n\n const [tick, setTick] = createSignal(0);\n\n // Trigger re-render when promise settles\n promise.then(() => setTick(1)).catch(() => setTick(1));\n\n return {\n __tova: true,\n tag: '__dynamic',\n props: {},\n children: [],\n compute: () => {\n tick(); // Track for reactivity\n if (loadError) return tova_el('span', { className: 'tova-error' }, [String(loadError)]);\n if (resolved) return resolved(props);\n // Fallback while loading\n return props && props.fallback ? props.fallback : null;\n },\n };\n };\n}\n\n// ─── Context (Provide/Inject) ────────────────────────────\n// Tree-based: values are stored on the ownership tree, inject walks up.\n\nexport function createContext(defaultValue) {\n const id = Symbol('context');\n return { _id: id, _default: defaultValue };\n}\n\nexport function provide(context, value) {\n const owner = currentOwner;\n if (owner) {\n if (!owner._contexts) owner._contexts = new Map();\n owner._contexts.set(context._id, value);\n }\n}\n\nexport function inject(context) {\n let owner = currentOwner;\n while (owner) {\n if (owner._contexts && owner._contexts.has(context._id)) {\n return owner._contexts.get(context._id);\n }\n owner = owner._owner;\n }\n return context._default;\n}\n\n// ─── DOM Rendering ────────────────────────────────────────\n\n// Inject scoped CSS into the page (idempotent — only injects once per id)\nconst __tovaInjectedStyles = new Set();\nexport function tova_inject_css(id, css) {\n if (__tovaInjectedStyles.has(id)) return;\n __tovaInjectedStyles.add(id);\n const style = document.createElement('style');\n style.setAttribute('data-tova-style', id);\n style.textContent = css;\n document.head.appendChild(style);\n}\n\nexport function tova_el(tag, props = {}, children = []) {\n return { __tova: true, tag, props, children };\n}\n\nexport function tova_fragment(children) {\n return { __tova: true, tag: '__fragment', props: {}, children };\n}\n\n// Inject a key prop into a vnode for keyed reconciliation\nexport function tova_keyed(key, vnode) {\n if (vnode && vnode.__tova) {\n vnode.props = { ...vnode.props, key };\n }\n return vnode;\n}\n\n// Flatten nested arrays and vnodes into a flat list of vnodes\nfunction flattenVNodes(children) {\n const result = [];\n for (const child of children) {\n if (child === null || child === undefined) {\n continue;\n } else if (Array.isArray(child)) {\n result.push(...flattenVNodes(child));\n } else {\n result.push(child);\n }\n }\n return result;\n}\n\n// ─── Marker-based DOM helpers ─────────────────────────────\n// Instead of wrapping dynamic blocks/fragments in <span style=\"display:contents\">,\n// we use comment node markers. A marker's __tovaNodes tracks its content nodes.\n// Content nodes have __tovaOwner pointing to their owning marker.\n\n// Recursively dispose ownership roots attached to a DOM subtree\nfunction disposeNode(node) {\n if (!node) return;\n if (node.__tovaRoot) {\n node.__tovaRoot();\n node.__tovaRoot = null;\n }\n // If this is a marker, dispose and remove its content nodes\n if (node.__tovaNodes) {\n for (const cn of node.__tovaNodes) {\n disposeNode(cn);\n if (cn.parentNode) cn.parentNode.removeChild(cn);\n }\n node.__tovaNodes = [];\n }\n if (node.childNodes) {\n for (const child of Array.from(node.childNodes)) {\n disposeNode(child);\n }\n }\n}\n\n// Check if a node is transitively owned by a marker (walks __tovaOwner chain)\nfunction isOwnedBy(node, marker) {\n let owner = node.__tovaOwner;\n while (owner) {\n if (owner === marker) return true;\n owner = owner.__tovaOwner;\n }\n return false;\n}\n\n// Get logical children of a parent element (skips marker content nodes)\nfunction getLogicalChildren(parent) {\n const logical = [];\n for (let i = 0; i < parent.childNodes.length; i++) {\n const node = parent.childNodes[i];\n if (!node.__tovaOwner) {\n logical.push(node);\n }\n }\n return logical;\n}\n\n// Find the first DOM sibling after all of a marker's content\nfunction nextSiblingAfterMarker(marker) {\n if (!marker.__tovaNodes || marker.__tovaNodes.length === 0) {\n return marker.nextSibling;\n }\n let last = marker.__tovaNodes[marker.__tovaNodes.length - 1];\n // If last content is itself a marker, recurse to find physical end\n while (last && last.__tovaNodes && last.__tovaNodes.length > 0) {\n last = last.__tovaNodes[last.__tovaNodes.length - 1];\n }\n return last ? last.nextSibling : marker.nextSibling;\n}\n\n// Remove a logical node (marker + its content, or a regular node) from the DOM\nfunction removeLogicalNode(parent, node) {\n disposeNode(node);\n if (node.parentNode === parent) parent.removeChild(node);\n}\n\n// Insert rendered result (could be single node or DocumentFragment) before ref,\n// setting __tovaOwner on top-level inserted nodes. Returns array of inserted nodes.\nfunction insertRendered(parent, rendered, ref, owner) {\n if (rendered.nodeType === 11) {\n const nodes = Array.from(rendered.childNodes);\n for (const n of nodes) {\n if (!n.__tovaOwner) n.__tovaOwner = owner;\n }\n parent.insertBefore(rendered, ref);\n return nodes;\n }\n if (!rendered.__tovaOwner) rendered.__tovaOwner = owner;\n parent.insertBefore(rendered, ref);\n return [rendered];\n}\n\n// Clear a marker's content from the DOM and reset __tovaNodes\nfunction clearMarkerContent(marker) {\n for (const node of marker.__tovaNodes) {\n disposeNode(node);\n if (node.parentNode) node.parentNode.removeChild(node);\n }\n marker.__tovaNodes = [];\n}\n\n// ─── Render ───────────────────────────────────────────────\n\n// Create real DOM nodes from a vnode (with fine-grained reactive bindings).\n// Returns a single DOM node for elements/text, or a DocumentFragment for\n// markers (dynamic blocks, fragments) containing [marker, ...content].\nexport function render(vnode) {\n if (vnode === null || vnode === undefined) {\n return document.createTextNode('');\n }\n\n // Reactive dynamic block (JSXIf, JSXFor, reactive text, etc.)\n if (typeof vnode === 'function') {\n const marker = document.createComment('');\n marker.__tovaDynamic = true;\n marker.__tovaNodes = [];\n\n const frag = document.createDocumentFragment();\n frag.appendChild(marker);\n\n createEffect(() => {\n const val = vnode();\n const parent = marker.parentNode;\n const ref = nextSiblingAfterMarker(marker);\n\n // Array: keyed or positional reconciliation within marker range\n if (Array.isArray(val)) {\n const flat = flattenVNodes(val);\n const hasKeys = flat.some(c => getKey(c) != null);\n if (hasKeys) {\n patchKeyedInMarker(marker, flat);\n } else {\n patchPositionalInMarker(marker, flat);\n }\n return;\n }\n\n // Text: optimize single text node update in place\n if (val == null || typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean') {\n const text = val == null ? '' : String(val);\n if (marker.__tovaNodes.length === 1 && marker.__tovaNodes[0].nodeType === 3) {\n if (marker.__tovaNodes[0].textContent !== text) {\n marker.__tovaNodes[0].textContent = text;\n }\n return;\n }\n clearMarkerContent(marker);\n const textNode = document.createTextNode(text);\n textNode.__tovaOwner = marker;\n parent.insertBefore(textNode, ref);\n marker.__tovaNodes = [textNode];\n return;\n }\n\n // Vnode or other: clear and re-render\n clearMarkerContent(marker);\n if (val && val.__tova) {\n const rendered = render(val);\n marker.__tovaNodes = insertRendered(parent, rendered, ref, marker);\n } else {\n const textNode = document.createTextNode(String(val));\n textNode.__tovaOwner = marker;\n parent.insertBefore(textNode, ref);\n marker.__tovaNodes = [textNode];\n }\n });\n\n return frag;\n }\n\n if (typeof vnode === 'string' || typeof vnode === 'number' || typeof vnode === 'boolean') {\n return document.createTextNode(String(vnode));\n }\n\n if (Array.isArray(vnode)) {\n const fragment = document.createDocumentFragment();\n for (const child of vnode) {\n fragment.appendChild(render(child));\n }\n return fragment;\n }\n\n if (!vnode.__tova) {\n return document.createTextNode(String(vnode));\n }\n\n // Fragment — marker + children (no wrapper element)\n if (vnode.tag === '__fragment') {\n const marker = document.createComment('');\n marker.__tovaFragment = true;\n marker.__tovaNodes = [];\n marker.__vnode = vnode;\n\n const frag = document.createDocumentFragment();\n frag.appendChild(marker);\n\n for (const child of flattenVNodes(vnode.children)) {\n const rendered = render(child);\n const inserted = insertRendered(frag, rendered, null, marker);\n marker.__tovaNodes.push(...inserted);\n }\n\n return frag;\n }\n\n // Dynamic reactive node (ErrorBoundary, Dynamic component, etc.)\n if (vnode.tag === '__dynamic' && typeof vnode.compute === 'function') {\n const marker = document.createComment('');\n marker.__tovaDynamic = true;\n marker.__tovaNodes = [];\n\n const frag = document.createDocumentFragment();\n frag.appendChild(marker);\n\n let prevDispose = null;\n const errHandler = vnode._errorHandler || null;\n createEffect(() => {\n if (errHandler) pushErrorHandler(errHandler);\n try {\n const inner = vnode.compute();\n const parent = marker.parentNode;\n const ref = nextSiblingAfterMarker(marker);\n\n if (prevDispose) {\n prevDispose();\n prevDispose = null;\n }\n clearMarkerContent(marker);\n\n createRoot((dispose) => {\n prevDispose = dispose;\n const rendered = render(inner);\n marker.__tovaNodes = insertRendered(parent, rendered, ref, marker);\n });\n } catch (e) {\n if (errHandler) {\n errHandler(e);\n } else if (currentErrorHandler) {\n currentErrorHandler(e);\n } else {\n console.error('Uncaught error during render:', e);\n }\n } finally {\n if (errHandler) popErrorHandler();\n }\n });\n\n return frag;\n }\n\n // Portal — render children into a different DOM target\n if (vnode.tag === '__portal') {\n const placeholder = document.createComment('portal');\n const targetSelector = vnode.props.target;\n queueMicrotask(() => {\n const targetEl = typeof targetSelector === 'string'\n ? document.querySelector(targetSelector)\n : targetSelector;\n if (targetEl) {\n for (const child of flattenVNodes(vnode.children)) {\n targetEl.appendChild(render(child));\n }\n }\n });\n return placeholder;\n }\n\n // Element\n const el = document.createElement(vnode.tag);\n applyReactiveProps(el, vnode.props);\n\n // Set data-tova-component attribute for DevTools\n if (vnode._componentName) {\n el.setAttribute('data-tova-component', vnode._componentName);\n if (__devtools_hooks && __devtools_hooks.onComponentRender) {\n __devtools_hooks.onComponentRender(vnode._componentName, el, 0);\n }\n }\n\n // Render children\n for (const child of flattenVNodes(vnode.children)) {\n el.appendChild(render(child));\n }\n\n // Store vnode reference for patching\n el.__vnode = vnode;\n\n return el;\n}\n\n// Apply reactive props — function-valued props get their own effect\nfunction applyReactiveProps(el, props) {\n for (const [key, value] of Object.entries(props)) {\n if (key === 'ref') {\n if (typeof value === 'object' && value !== null && 'current' in value) {\n value.current = el;\n } else if (typeof value === 'function') {\n value(el);\n }\n } else if (key.startsWith('on')) {\n const eventName = key.slice(2).toLowerCase();\n el.addEventListener(eventName, value);\n if (!el.__handlers) el.__handlers = {};\n el.__handlers[eventName] = value;\n } else if (key === 'key') {\n // Skip\n } else if (typeof value === 'function' && !key.startsWith('on')) {\n // Reactive prop — create effect for fine-grained updates\n createEffect(() => {\n const val = value();\n applyPropValue(el, key, val);\n });\n } else {\n applyPropValue(el, key, value);\n }\n }\n}\n\nfunction applyPropValue(el, key, val) {\n if (key === 'className') {\n if (el.className !== val) el.className = val || '';\n } else if (key === 'innerHTML' || key === 'dangerouslySetInnerHTML') {\n const html = typeof val === 'object' && val !== null ? val.__html || '' : val || '';\n if (el.innerHTML !== html) el.innerHTML = html;\n } else if (key === 'value') {\n if (el !== document.activeElement && el.value !== val) {\n el.value = val;\n }\n } else if (key === 'checked') {\n el.checked = !!val;\n } else if (key === 'disabled' || key === 'readOnly' || key === 'hidden') {\n el[key] = !!val;\n } else if (key === 'style' && typeof val === 'object') {\n // Clear old properties not present in new style object\n for (let i = el.style.length - 1; i >= 0; i--) {\n const prop = el.style[i];\n const camel = prop.replace(/-([a-z])/g, (_, c) => c.toUpperCase());\n if (!(prop in val) && !(camel in val)) {\n el.style.removeProperty(prop);\n }\n }\n Object.assign(el.style, val);\n } else {\n const s = val == null ? '' : String(val);\n if (el.getAttribute(key) !== s) {\n el.setAttribute(key, s);\n }\n }\n}\n\n// Apply/update props on a DOM element (used by patcher for full-tree mode)\nfunction applyProps(el, newProps, oldProps) {\n // Remove old props that are no longer present\n for (const key of Object.keys(oldProps)) {\n if (!(key in newProps)) {\n if (key.startsWith('on')) {\n const eventName = key.slice(2).toLowerCase();\n if (el.__handlers && el.__handlers[eventName]) {\n el.removeEventListener(eventName, el.__handlers[eventName]);\n delete el.__handlers[eventName];\n }\n } else if (key === 'className') {\n el.className = '';\n } else if (key === 'style') {\n el.removeAttribute('style');\n } else {\n el.removeAttribute(key);\n }\n }\n }\n\n // Apply new props\n for (const [key, value] of Object.entries(newProps)) {\n if (key === 'className') {\n const val = typeof value === 'function' ? value() : value;\n if (el.className !== val) el.className = val;\n } else if (key === 'ref') {\n if (typeof value === 'object' && value !== null && 'current' in value) {\n value.current = el;\n } else if (typeof value === 'function') {\n value(el);\n }\n } else if (key.startsWith('on')) {\n const eventName = key.slice(2).toLowerCase();\n const oldHandler = el.__handlers && el.__handlers[eventName];\n if (oldHandler !== value) {\n if (oldHandler) el.removeEventListener(eventName, oldHandler);\n el.addEventListener(eventName, value);\n if (!el.__handlers) el.__handlers = {};\n el.__handlers[eventName] = value;\n }\n } else if (key === 'style' && typeof value === 'object') {\n Object.assign(el.style, value);\n } else if (key === 'key') {\n // Skip\n } else if (key === 'value') {\n const val = typeof value === 'function' ? value() : value;\n if (el !== document.activeElement && el.value !== val) {\n el.value = val;\n }\n } else if (key === 'checked') {\n el.checked = !!value;\n } else {\n const val = typeof value === 'function' ? value() : value;\n if (el.getAttribute(key) !== String(val)) {\n el.setAttribute(key, val);\n }\n }\n }\n}\n\n// ─── Keyed Reconciliation ────────────────────────────────\n\nfunction getKey(vnode) {\n if (vnode && vnode.__tova && vnode.props) return vnode.props.key;\n return undefined;\n}\n\nfunction getNodeKey(node) {\n if (node && node.__vnode && node.__vnode.props) return node.__vnode.props.key;\n return undefined;\n}\n\n// Keyed reconciliation within a marker's content range\nfunction patchKeyedInMarker(marker, newVNodes) {\n const parent = marker.parentNode;\n const oldNodes = [...marker.__tovaNodes];\n const oldKeyMap = new Map();\n\n for (const node of oldNodes) {\n const key = getNodeKey(node);\n if (key != null) oldKeyMap.set(key, node);\n }\n\n const newNodes = [];\n const usedOld = new Set();\n\n for (const newChild of newVNodes) {\n const key = getKey(newChild);\n\n if (key != null && oldKeyMap.has(key)) {\n const oldNode = oldKeyMap.get(key);\n usedOld.add(oldNode);\n\n if (oldNode.nodeType === 1 && newChild.__tova &&\n oldNode.tagName.toLowerCase() === newChild.tag.toLowerCase()) {\n const oldVNode = oldNode.__vnode || { props: {}, children: [] };\n applyProps(oldNode, newChild.props, oldVNode.props);\n patchChildrenOfElement(oldNode, flattenVNodes(newChild.children));\n oldNode.__vnode = newChild;\n newNodes.push(oldNode);\n } else {\n const node = render(newChild);\n // render may return Fragment — collect nodes\n if (node.nodeType === 11) {\n const nodes = Array.from(node.childNodes);\n for (const n of nodes) { if (!n.__tovaOwner) n.__tovaOwner = marker; }\n parent.insertBefore(node, nextSiblingAfterMarker(marker));\n newNodes.push(...nodes);\n } else {\n if (!node.__tovaOwner) node.__tovaOwner = marker;\n newNodes.push(node);\n }\n }\n } else {\n const node = render(newChild);\n if (node.nodeType === 11) {\n const nodes = Array.from(node.childNodes);\n for (const n of nodes) { if (!n.__tovaOwner) n.__tovaOwner = marker; }\n parent.insertBefore(node, nextSiblingAfterMarker(marker));\n newNodes.push(...nodes);\n } else {\n if (!node.__tovaOwner) node.__tovaOwner = marker;\n newNodes.push(node);\n }\n }\n }\n\n // Remove unused old nodes\n for (const node of oldNodes) {\n if (!usedOld.has(node)) {\n disposeNode(node);\n if (node.parentNode === parent) parent.removeChild(node);\n }\n }\n\n // Arrange in correct order after marker using cursor approach\n let cursor = marker.nextSibling;\n for (const node of newNodes) {\n if (node === cursor) {\n cursor = node.nextSibling;\n } else {\n parent.insertBefore(node, cursor);\n }\n }\n\n marker.__tovaNodes = newNodes;\n}\n\n// Positional reconciliation within a marker's content range\nfunction patchPositionalInMarker(marker, newChildren) {\n const parent = marker.parentNode;\n const oldNodes = [...marker.__tovaNodes];\n const oldCount = oldNodes.length;\n const newCount = newChildren.length;\n\n // Patch in place\n const patchCount = Math.min(oldCount, newCount);\n for (let i = 0; i < patchCount; i++) {\n patchSingle(parent, oldNodes[i], newChildren[i]);\n }\n\n // Append new children\n const ref = nextSiblingAfterMarker(marker);\n for (let i = oldCount; i < newCount; i++) {\n const rendered = render(newChildren[i]);\n const inserted = insertRendered(parent, rendered, ref, marker);\n oldNodes.push(...inserted);\n }\n\n // Remove excess children\n for (let i = newCount; i < oldCount; i++) {\n disposeNode(oldNodes[i]);\n if (oldNodes[i].parentNode === parent) parent.removeChild(oldNodes[i]);\n }\n\n marker.__tovaNodes = oldNodes.slice(0, newCount);\n}\n\n// Keyed reconciliation for children of an element (not marker-based)\nfunction patchKeyedChildren(parent, newVNodes) {\n const logical = getLogicalChildren(parent);\n const oldKeyMap = new Map();\n\n for (const node of logical) {\n const key = getNodeKey(node);\n if (key != null) oldKeyMap.set(key, node);\n }\n\n const newNodes = [];\n const usedOld = new Set();\n\n for (const newChild of newVNodes) {\n const key = getKey(newChild);\n\n if (key != null && oldKeyMap.has(key)) {\n const oldNode = oldKeyMap.get(key);\n usedOld.add(oldNode);\n\n if (oldNode.nodeType === 1 && newChild.__tova &&\n oldNode.tagName.toLowerCase() === newChild.tag.toLowerCase()) {\n const oldVNode = oldNode.__vnode || { props: {}, children: [] };\n applyProps(oldNode, newChild.props, oldVNode.props);\n patchChildrenOfElement(oldNode, flattenVNodes(newChild.children));\n oldNode.__vnode = newChild;\n newNodes.push(oldNode);\n } else {\n newNodes.push(render(newChild));\n }\n } else {\n newNodes.push(render(newChild));\n }\n }\n\n // Remove unused old logical nodes\n for (const node of logical) {\n if (!usedOld.has(node) && node.parentNode === parent) {\n removeLogicalNode(parent, node);\n }\n }\n\n // Arrange in correct order\n for (let i = 0; i < newNodes.length; i++) {\n const expected = newNodes[i];\n const logicalNow = getLogicalChildren(parent);\n const current = logicalNow[i];\n if (current !== expected) {\n parent.insertBefore(expected, current || null);\n }\n }\n}\n\n// Positional reconciliation for children of an element\nfunction patchPositionalChildren(parent, newChildren) {\n const logical = getLogicalChildren(parent);\n const oldCount = logical.length;\n const newCount = newChildren.length;\n\n for (let i = 0; i < Math.min(oldCount, newCount); i++) {\n patchSingle(parent, logical[i], newChildren[i]);\n }\n\n for (let i = oldCount; i < newCount; i++) {\n parent.appendChild(render(newChildren[i]));\n }\n\n // Remove excess logical children\n const currentLogical = getLogicalChildren(parent);\n while (currentLogical.length > newCount) {\n const node = currentLogical.pop();\n removeLogicalNode(parent, node);\n }\n}\n\n// Patch children of a regular element\nfunction patchChildrenOfElement(el, newChildren) {\n const hasKeys = newChildren.some(c => getKey(c) != null);\n if (hasKeys) {\n patchKeyedChildren(el, newChildren);\n } else {\n patchPositionalChildren(el, newChildren);\n }\n}\n\n// Patch a single logical node in place\nfunction patchSingle(parent, existing, newVNode) {\n if (!existing) {\n parent.appendChild(render(newVNode));\n return;\n }\n\n if (newVNode === null || newVNode === undefined) {\n removeLogicalNode(parent, existing);\n return;\n }\n\n // Function vnode — replace with new dynamic block\n if (typeof newVNode === 'function') {\n const rendered = render(newVNode);\n if (existing.__tovaNodes) {\n // Existing is a marker — clear its content and replace\n clearMarkerContent(existing);\n parent.replaceChild(rendered, existing);\n } else {\n disposeNode(existing);\n parent.replaceChild(rendered, existing);\n }\n return;\n }\n\n // Text\n if (typeof newVNode === 'string' || typeof newVNode === 'number' || typeof newVNode === 'boolean') {\n const text = String(newVNode);\n if (existing.nodeType === 3) {\n if (existing.textContent !== text) existing.textContent = text;\n } else {\n removeLogicalNode(parent, existing);\n parent.insertBefore(document.createTextNode(text), null);\n }\n return;\n }\n\n if (!newVNode.__tova) {\n const text = String(newVNode);\n if (existing.nodeType === 3) {\n if (existing.textContent !== text) existing.textContent = text;\n } else {\n removeLogicalNode(parent, existing);\n parent.insertBefore(document.createTextNode(text), null);\n }\n return;\n }\n\n // Fragment — patch marker content\n if (newVNode.tag === '__fragment') {\n if (existing.__tovaFragment) {\n // Patch children within the marker range\n const oldNodes = [...existing.__tovaNodes];\n const newChildren = flattenVNodes(newVNode.children);\n // Simple approach: clear and re-render fragment content\n clearMarkerContent(existing);\n const ref = nextSiblingAfterMarker(existing);\n for (const child of newChildren) {\n const rendered = render(child);\n const inserted = insertRendered(parent, rendered, ref, existing);\n existing.__tovaNodes.push(...inserted);\n }\n existing.__vnode = newVNode;\n return;\n }\n removeLogicalNode(parent, existing);\n parent.appendChild(render(newVNode));\n return;\n }\n\n // Element — patch in place\n if (existing.nodeType === 1 && newVNode.tag &&\n existing.tagName.toLowerCase() === newVNode.tag.toLowerCase()) {\n const oldVNode = existing.__vnode || { props: {}, children: [] };\n applyProps(existing, newVNode.props, oldVNode.props);\n patchChildrenOfElement(existing, flattenVNodes(newVNode.children));\n existing.__vnode = newVNode;\n return;\n }\n\n // Different type — full replace\n removeLogicalNode(parent, existing);\n parent.appendChild(render(newVNode));\n}\n\n// ─── Hydration (SSR) ─────────────────────────────────────\n// SSR renders flat HTML without markers. Hydration attaches reactivity\n// to existing DOM nodes and inserts markers for dynamic blocks.\n\n// Dev-mode hydration mismatch detection\nfunction checkHydrationMismatch(domNode, vnode) {\n if (!__DEV__) return;\n if (!domNode || !vnode || !vnode.__tova) return;\n\n const props = vnode.props || {};\n\n // Check className\n if (props.className !== undefined) {\n const expected = typeof props.className === 'function' ? props.className() : props.className;\n const actual = domNode.className || '';\n if (expected && actual !== expected) {\n console.warn(`Tova hydration mismatch: <${vnode.tag}> class expected \"${expected}\" but got \"${actual}\"`);\n }\n }\n\n // Check attributes\n for (const [key, value] of Object.entries(props)) {\n if (key === 'key' || key === 'ref' || key === 'className' || key.startsWith('on')) continue;\n if (typeof value === 'function') continue; // reactive props — skip static check\n\n if (domNode.getAttribute) {\n const attrName = key === 'className' ? 'class' : key;\n const actual = domNode.getAttribute(attrName);\n const expected = String(value);\n if (actual !== null && actual !== expected) {\n console.warn(`Tova hydration mismatch: <${vnode.tag}> attribute \"${key}\" expected \"${expected}\" but got \"${actual}\"`);\n }\n }\n }\n}\n\n// Check if a DOM node is an SSR marker comment (<!--tova-s:ID-->)\nfunction isSSRMarker(node) {\n return node && node.nodeType === 8 && typeof node.data === 'string' && node.data.startsWith('tova-s:');\n}\n\n// Find the closing SSR marker and collect content nodes between them\nfunction collectSSRMarkerContent(startMarker) {\n const id = startMarker.data.replace('tova-s:', '');\n const closingText = `/tova-s:${id}`;\n const content = [];\n let cursor = startMarker.nextSibling;\n while (cursor) {\n if (cursor.nodeType === 8 && cursor.data === closingText) {\n return { content, endMarker: cursor };\n }\n content.push(cursor);\n cursor = cursor.nextSibling;\n }\n return { content, endMarker: null };\n}\n\nfunction hydrateVNode(domNode, vnode) {\n if (!domNode) return null;\n if (vnode === null || vnode === undefined) return domNode;\n\n // Function vnode (reactive text, JSXIf, JSXFor)\n if (typeof vnode === 'function') {\n if (domNode.nodeType === 3) {\n // Dev-mode: warn if text content differs\n if (__DEV__) {\n const val = vnode();\n const expected = val == null ? '' : String(val);\n if (domNode.textContent !== expected) {\n console.warn(`Tova hydration mismatch: text expected \"${expected}\" but got \"${domNode.textContent}\"`);\n }\n }\n // Reactive text: attach effect to existing text node\n domNode.__tovaReactive = true;\n createEffect(() => {\n const val = vnode();\n const text = val == null ? '' : String(val);\n if (domNode.textContent !== text) domNode.textContent = text;\n });\n return domNode.nextSibling;\n }\n // Complex dynamic block: insert marker-based render, replace SSR node\n const parent = domNode.parentNode;\n const next = domNode.nextSibling;\n const rendered = render(vnode);\n parent.replaceChild(rendered, domNode);\n return next;\n }\n\n // Primitive text — already correct from SSR\n if (typeof vnode === 'string' || typeof vnode === 'number' || typeof vnode === 'boolean') {\n if (__DEV__ && domNode.nodeType === 3) {\n const expected = String(vnode);\n if (domNode.textContent !== expected) {\n console.warn(`Tova hydration mismatch: text expected \"${expected}\" but got \"${domNode.textContent}\"`);\n }\n }\n return domNode.nextSibling;\n }\n\n // Array\n if (Array.isArray(vnode)) {\n let cursor = domNode;\n for (const child of flattenVNodes(vnode)) {\n if (!cursor) break;\n cursor = hydrateVNode(cursor, child);\n }\n return cursor;\n }\n\n if (!vnode.__tova) return domNode.nextSibling;\n\n // Fragment — children rendered inline in SSR (no wrapper)\n if (vnode.tag === '__fragment') {\n const children = flattenVNodes(vnode.children);\n let cursor = domNode;\n for (const child of children) {\n if (!cursor) break;\n cursor = hydrateVNode(cursor, child);\n }\n return cursor;\n }\n\n // Dynamic node — SSR marker-aware hydration\n if (vnode.tag === '__dynamic' && typeof vnode.compute === 'function') {\n // Check if current domNode is an SSR marker (<!--tova-s:ID-->)\n if (isSSRMarker(domNode)) {\n const { content, endMarker } = collectSSRMarkerContent(domNode);\n const parent = domNode.parentNode;\n\n // Remove SSR markers and content, replace with reactive marker\n const afterEnd = endMarker ? endMarker.nextSibling : null;\n for (const node of content) {\n if (node.parentNode === parent) parent.removeChild(node);\n }\n if (endMarker && endMarker.parentNode === parent) parent.removeChild(endMarker);\n\n const rendered = render(vnode);\n parent.replaceChild(rendered, domNode);\n return afterEnd;\n }\n\n // No SSR marker — fall back to standard behavior\n const parent = domNode.parentNode;\n const next = domNode.nextSibling;\n const rendered = render(vnode);\n parent.replaceChild(rendered, domNode);\n return next;\n }\n\n // Element — attach event handlers, reactive props, refs\n if (domNode.nodeType === 1 && domNode.tagName.toLowerCase() === vnode.tag.toLowerCase()) {\n if (__DEV__) checkHydrationMismatch(domNode, vnode);\n hydrateProps(domNode, vnode.props);\n domNode.__vnode = vnode;\n\n const children = flattenVNodes(vnode.children || []);\n let cursor = domNode.firstChild;\n for (const child of children) {\n if (!cursor) break;\n cursor = hydrateVNode(cursor, child);\n }\n return domNode.nextSibling;\n }\n\n // Tag mismatch — fall back to full render\n if (__DEV__) {\n const expectedTag = vnode.tag || '(unknown)';\n const actualTag = domNode.tagName ? domNode.tagName.toLowerCase() : `nodeType:${domNode.nodeType}`;\n console.warn(`Tova hydration mismatch: expected <${expectedTag}> but got <${actualTag}>, falling back to full render`);\n }\n const parent = domNode.parentNode;\n const next = domNode.nextSibling;\n const rendered = render(vnode);\n parent.replaceChild(rendered, domNode);\n return next;\n}\n\nfunction hydrateProps(el, props) {\n for (const [key, value] of Object.entries(props)) {\n if (key === 'ref') {\n if (typeof value === 'object' && value !== null && 'current' in value) {\n value.current = el;\n } else if (typeof value === 'function') {\n value(el);\n }\n } else if (key.startsWith('on')) {\n const eventName = key.slice(2).toLowerCase();\n el.addEventListener(eventName, value);\n if (!el.__handlers) el.__handlers = {};\n el.__handlers[eventName] = value;\n } else if (key === 'key') {\n // Skip\n } else if (typeof value === 'function' && !key.startsWith('on')) {\n createEffect(() => {\n const val = value();\n applyPropValue(el, key, val);\n });\n }\n }\n}\n\nexport function hydrate(component, container) {\n if (!container) {\n console.error('Tova: Hydration target not found');\n return;\n }\n\n const startTime = typeof performance !== 'undefined' ? performance.now() : 0;\n\n const result = createRoot(() => {\n const vnode = typeof component === 'function' ? component() : component;\n if (container.firstChild) {\n hydrateVNode(container.firstChild, vnode);\n }\n });\n\n // Dispatch hydration completion event\n const duration = typeof performance !== 'undefined' ? performance.now() - startTime : 0;\n if (typeof CustomEvent !== 'undefined' && typeof container.dispatchEvent === 'function') {\n container.dispatchEvent(new CustomEvent('tova:hydrated', { detail: { duration }, bubbles: true }));\n }\n\n if (__devtools_hooks && __devtools_hooks.onHydrate) {\n __devtools_hooks.onHydrate({ duration });\n }\n\n return result;\n}\n\nexport function mount(component, container) {\n if (!container) {\n console.error('Tova: Mount target not found');\n return;\n }\n\n const result = createRoot((dispose) => {\n const vnode = typeof component === 'function' ? component() : component;\n container.innerHTML = '';\n container.appendChild(render(vnode));\n return dispose;\n });\n\n if (__devtools_hooks && __devtools_hooks.onMount) {\n __devtools_hooks.onMount();\n }\n\n return result;\n}\n\n// ─── Progressive Hydration ──────────────────────────────────\n// Hydrate a component only when it becomes visible in the viewport.\n\nexport function hydrateWhenVisible(component, domNode, options = {}) {\n if (typeof IntersectionObserver === 'undefined') {\n // Fallback: hydrate immediately\n return hydrate(component, domNode);\n }\n\n const { rootMargin = '200px' } = options;\n let hydrated = false;\n\n const observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (entry.isIntersecting && !hydrated) {\n hydrated = true;\n observer.disconnect();\n hydrate(component, domNode);\n }\n }\n },\n { rootMargin },\n );\n\n observer.observe(domNode);\n\n return () => {\n observer.disconnect();\n };\n}\n";
|
|
3
|
+
export const REACTIVITY_SOURCE = "// Fine-grained reactivity system for Tova (signals-based)\n\nconst __DEV__ = typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production';\n\n// ─── DevTools hooks (zero-cost when disabled) ────────────\nlet __devtools_hooks = null;\nexport function __enableDevTools(hooks) {\n __devtools_hooks = hooks;\n}\n\nlet currentEffect = null;\nconst effectStack = [];\n\n// ─── Ownership System ─────────────────────────────────────\nlet currentOwner = null;\nconst ownerStack = [];\n\n// ─── Batching ────────────────────────────────────────────\n// Default: synchronous flush after each setter (backward compatible).\n// Inside batch(): effects are deferred and flushed once when batch ends.\n// This means setA(1); setB(2) causes 2 runs by default, but\n// batch(() => { setA(1); setB(2); }) causes only 1 run.\n\nlet pendingEffects = new Set();\nlet batchDepth = 0;\nlet flushing = false;\n\nfunction flush() {\n if (flushing) return; // prevent re-entrant flush\n flushing = true;\n let iterations = 0;\n try {\n while (pendingEffects.size > 0) {\n if (++iterations > 100) {\n console.error('Tova: Possible infinite loop in reactive updates (>100 flush iterations). Aborting.');\n pendingEffects.clear();\n break;\n }\n const toRun = [...pendingEffects];\n pendingEffects.clear();\n for (const effect of toRun) {\n if (!effect._disposed) {\n effect();\n }\n }\n }\n } finally {\n flushing = false;\n }\n}\n\nexport function batch(fn) {\n batchDepth++;\n try {\n fn();\n } finally {\n batchDepth--;\n if (batchDepth === 0) {\n flush();\n }\n }\n}\n\n// ─── Ownership Root ──────────────────────────────────────\n\nexport function createRoot(fn) {\n const root = {\n _children: [],\n _disposed: false,\n _cleanups: [],\n _contexts: null,\n _owner: currentOwner,\n dispose() {\n if (root._disposed) return;\n root._disposed = true;\n // Dispose children in reverse order\n for (let i = root._children.length - 1; i >= 0; i--) {\n const child = root._children[i];\n if (typeof child.dispose === 'function') child.dispose();\n }\n root._children.length = 0;\n // Run cleanups in reverse order\n for (let i = root._cleanups.length - 1; i >= 0; i--) {\n try { root._cleanups[i](); } catch (e) { console.error('Tova: root cleanup error:', e); }\n }\n root._cleanups.length = 0;\n }\n };\n ownerStack.push(currentOwner);\n currentOwner = root;\n try {\n return fn(root.dispose.bind(root));\n } finally {\n currentOwner = ownerStack.pop();\n }\n}\n\n// ─── Dependency Cleanup ──────────────────────────────────\n\nfunction cleanupDeps(subscriber) {\n if (subscriber._deps) {\n for (const depSet of subscriber._deps) {\n depSet.delete(subscriber);\n }\n subscriber._deps.clear();\n }\n}\n\nfunction trackDep(subscriber, subscriberSet) {\n subscriberSet.add(subscriber);\n if (!subscriber._deps) subscriber._deps = new Set();\n subscriber._deps.add(subscriberSet);\n}\n\n// ─── Signals ─────────────────────────────────────────────\n\nexport function createSignal(initialValue, name) {\n let value = initialValue;\n const subscribers = new Set();\n let signalId = null;\n\n if (__devtools_hooks) {\n signalId = __devtools_hooks.onSignalCreate(\n () => value,\n (v) => setter(v),\n name,\n );\n }\n\n function getter() {\n if (currentEffect) {\n trackDep(currentEffect, subscribers);\n }\n return value;\n }\n\n function setter(newValue) {\n if (typeof newValue === 'function') {\n newValue = newValue(value);\n }\n if (value !== newValue) {\n const oldValue = value;\n value = newValue;\n if (__devtools_hooks && signalId != null) {\n __devtools_hooks.onSignalUpdate(signalId, oldValue, newValue);\n }\n for (const sub of [...subscribers]) {\n if (sub._isComputed) {\n sub(); // propagate dirty flags synchronously through computed graph\n } else {\n pendingEffects.add(sub);\n }\n }\n if (batchDepth === 0) {\n flush();\n }\n }\n }\n\n return [getter, setter];\n}\n\n// ─── Effects ─────────────────────────────────────────────\n\nfunction runCleanups(effect) {\n if (effect._cleanup) {\n try { effect._cleanup(); } catch (e) { console.error('Tova: cleanup error:', e); }\n effect._cleanup = null;\n }\n if (effect._cleanups && effect._cleanups.length > 0) {\n for (const cb of effect._cleanups) {\n try { cb(); } catch (e) { console.error('Tova: cleanup error:', e); }\n }\n effect._cleanups = [];\n }\n}\n\nexport function createEffect(fn) {\n function effect() {\n if (effect._running) return;\n if (effect._disposed) return;\n effect._running = true;\n\n // Run cleanups from previous execution\n runCleanups(effect);\n\n // Remove from all previous dependency subscriber sets\n cleanupDeps(effect);\n\n effectStack.push(effect);\n currentEffect = effect;\n const startTime = __devtools_hooks && typeof performance !== 'undefined' ? performance.now() : 0;\n try {\n const result = fn();\n // If effect returns a function, use as cleanup\n if (typeof result === 'function') {\n effect._cleanup = result;\n }\n } catch (e) {\n console.error('Tova: Error in effect:', e);\n if (currentErrorHandler) {\n currentErrorHandler(e);\n }\n } finally {\n if (__devtools_hooks) {\n const duration = typeof performance !== 'undefined' ? performance.now() - startTime : 0;\n __devtools_hooks.onEffectRun(effect, duration);\n }\n effectStack.pop();\n currentEffect = effectStack[effectStack.length - 1] || null;\n effect._running = false;\n }\n }\n\n effect._deps = new Set();\n effect._running = false;\n effect._disposed = false;\n effect._cleanup = null;\n effect._cleanups = [];\n effect._owner = currentOwner;\n\n if (__devtools_hooks) {\n __devtools_hooks.onEffectCreate(effect);\n }\n\n if (currentOwner && !currentOwner._disposed) {\n currentOwner._children.push(effect);\n }\n\n effect.dispose = function () {\n effect._disposed = true;\n runCleanups(effect);\n cleanupDeps(effect);\n pendingEffects.delete(effect);\n // Remove from owner's children\n if (effect._owner) {\n const idx = effect._owner._children.indexOf(effect);\n if (idx >= 0) effect._owner._children.splice(idx, 1);\n }\n };\n\n // Run immediately (synchronous first run)\n effect();\n return effect;\n}\n\n// ─── Computed (lazy/pull-based for glitch-free reads) ────\n\nexport function createComputed(fn) {\n let value;\n let dirty = true;\n const subscribers = new Set();\n\n // notify is called synchronously when a source signal changes.\n // It marks the computed dirty and propagates to downstream subscribers.\n function notify() {\n if (!dirty) {\n dirty = true;\n for (const sub of [...subscribers]) {\n if (sub._isComputed) {\n sub(); // cascade dirty flags synchronously\n } else {\n pendingEffects.add(sub);\n }\n }\n }\n }\n\n notify._deps = new Set();\n notify._disposed = false;\n notify._isComputed = true;\n notify._owner = currentOwner;\n\n if (currentOwner && !currentOwner._disposed) {\n currentOwner._children.push(notify);\n }\n\n notify.dispose = function () {\n notify._disposed = true;\n cleanupDeps(notify);\n if (notify._owner) {\n const idx = notify._owner._children.indexOf(notify);\n if (idx >= 0) notify._owner._children.splice(idx, 1);\n }\n };\n\n function recompute() {\n cleanupDeps(notify);\n\n effectStack.push(notify);\n currentEffect = notify;\n try {\n value = fn();\n dirty = false;\n } finally {\n effectStack.pop();\n currentEffect = effectStack[effectStack.length - 1] || null;\n }\n }\n\n // Initial computation\n recompute();\n\n function getter() {\n if (currentEffect) {\n trackDep(currentEffect, subscribers);\n }\n if (dirty) {\n recompute();\n }\n return value;\n }\n\n return getter;\n}\n\n// ─── Lifecycle Hooks ─────────────────────────────────────\n\nexport function onMount(fn) {\n const owner = currentOwner;\n queueMicrotask(() => {\n const result = fn();\n if (typeof result === 'function' && owner && !owner._disposed) {\n owner._cleanups.push(result);\n }\n });\n}\n\nexport function onUnmount(fn) {\n if (currentOwner && !currentOwner._disposed) {\n currentOwner._cleanups.push(fn);\n }\n}\n\nexport function onCleanup(fn) {\n if (currentEffect) {\n if (!currentEffect._cleanups) currentEffect._cleanups = [];\n currentEffect._cleanups.push(fn);\n }\n}\n\n// ─── Untrack ─────────────────────────────────────────────\n// Run a function without tracking any signal reads (opt out of reactivity)\n\nexport function untrack(fn) {\n const prev = currentEffect;\n currentEffect = null;\n try {\n return fn();\n } finally {\n currentEffect = prev;\n }\n}\n\n// ─── Watch ───────────────────────────────────────────────\n// Watch a reactive expression, calling callback with (newValue, oldValue)\n// Returns a dispose function to stop watching.\n\nexport function watch(getter, callback, options = {}) {\n let oldValue = undefined;\n let initialized = false;\n\n const effect = createEffect(() => {\n const newValue = getter();\n if (initialized) {\n untrack(() => callback(newValue, oldValue));\n } else if (options.immediate) {\n untrack(() => callback(newValue, undefined));\n }\n oldValue = newValue;\n initialized = true;\n });\n\n return effect.dispose ? effect.dispose.bind(effect) : () => {\n effect._disposed = true;\n runCleanups(effect);\n cleanupDeps(effect);\n pendingEffects.delete(effect);\n };\n}\n\n// ─── Refs ────────────────────────────────────────────────\n\nexport function createRef(initialValue) {\n return { current: initialValue !== undefined ? initialValue : null };\n}\n\n// ─── Error Boundaries ────────────────────────────────────\n\n// Stack-based error handler for correct nested boundary propagation\nconst errorHandlerStack = [];\nlet currentErrorHandler = null;\n\nfunction pushErrorHandler(handler) {\n errorHandlerStack.push(currentErrorHandler);\n currentErrorHandler = handler;\n}\n\nfunction popErrorHandler() {\n currentErrorHandler = errorHandlerStack.pop() || null;\n}\n\n// Component name tracking for stack traces\nconst componentNameStack = [];\n\nexport function pushComponentName(name) {\n componentNameStack.push(name);\n}\n\nexport function popComponentName() {\n componentNameStack.pop();\n}\n\nfunction buildComponentStack() {\n return [...componentNameStack].reverse();\n}\n\nexport function createErrorBoundary(options = {}) {\n const { onError, onReset } = options;\n const [error, setError] = createSignal(null);\n\n function run(fn) {\n pushErrorHandler((e) => {\n const stack = buildComponentStack();\n if (e && typeof e === 'object') e.__tovaComponentStack = stack;\n setError(e);\n if (onError) onError({ error: e, componentStack: stack });\n });\n try {\n return fn();\n } catch (e) {\n const stack = buildComponentStack();\n if (e && typeof e === 'object') e.__tovaComponentStack = stack;\n setError(e);\n if (onError) onError({ error: e, componentStack: stack });\n return null;\n } finally {\n popErrorHandler();\n }\n }\n\n function reset() {\n setError(null);\n if (onReset) onReset();\n }\n\n return { error, run, reset };\n}\n\nlet __errorBoundaryIdCounter = 0;\n\nexport function ErrorBoundary({ fallback, children, onError, onReset, onErrorCleared, retry = 0 }) {\n const [error, setError] = createSignal(null);\n const [retryCount, setRetryCount] = createSignal(0);\n const boundaryId = ++__errorBoundaryIdCounter;\n let lastErrorId = 0;\n\n function handleError(e) {\n const stack = buildComponentStack();\n const errorId = `EB${boundaryId}-${++lastErrorId}`;\n\n if (e && typeof e === 'object') {\n e.__tovaComponentStack = stack;\n e.__tovaErrorId = errorId;\n }\n\n if (retryCount() < retry) {\n setRetryCount(c => c + 1);\n setError(null); // clear to re-trigger render\n return;\n }\n setError(e);\n if (onError) onError({ error: e, componentStack: stack, errorId, retryCount: retryCount() });\n }\n\n function resetBoundary() {\n setRetryCount(0);\n setError(null);\n if (onReset) onReset();\n }\n\n // Return a reactive wrapper that switches between children and fallback\n const childContent = children && children.length === 1 ? children[0] : tova_fragment(children || []);\n\n const vnode = {\n __tova: true,\n tag: '__dynamic',\n props: {},\n children: [],\n _fallback: fallback,\n _componentName: 'ErrorBoundary',\n _errorHandler: handleError, // Active during __dynamic effect render cycle\n compute: () => {\n const err = error();\n if (err) {\n // Render fallback — if fallback itself throws, propagate to parent boundary\n try {\n const errorId = err && typeof err === 'object' ? err.__tovaErrorId : null;\n return typeof fallback === 'function'\n ? fallback({\n error: err,\n errorId,\n retryCount: retryCount(),\n componentStack: err && typeof err === 'object' ? err.__tovaComponentStack : [],\n reset: resetBoundary,\n })\n : fallback;\n } catch (fallbackError) {\n // Fallback threw — propagate to parent error boundary\n if (currentErrorHandler) {\n currentErrorHandler(fallbackError);\n }\n return null;\n }\n }\n // Children rendered successfully — fire onErrorCleared if we recovered from an error\n if (onErrorCleared && lastErrorId > 0 && retryCount() === 0) {\n queueMicrotask(() => onErrorCleared());\n }\n return childContent;\n },\n };\n\n return vnode;\n}\n\n// Built-in ErrorInfo component — renders a formatted error display\n// Usage: <ErrorBoundary fallback={fn(props) ErrorInfo(props)} />\nexport function ErrorInfo({ error, errorId, componentStack, reset, retryCount }) {\n const message = error instanceof Error ? error.message : String(error);\n const stackTrace = error instanceof Error && error.stack ? error.stack : '';\n const compStack = (componentStack || []).join(' > ');\n\n const children = [\n tova_el('h3', { style: { margin: '0 0 8px 0', color: '#e53e3e' } }, ['Something went wrong']),\n tova_el('p', { style: { margin: '4px 0', fontFamily: 'monospace', fontSize: '14px' } }, [message]),\n ];\n\n if (compStack) {\n children.push(\n tova_el('p', { style: { margin: '4px 0', fontSize: '12px', color: '#718096' } }, [\n 'Component: ', compStack\n ])\n );\n }\n\n if (errorId) {\n children.push(\n tova_el('p', { style: { margin: '4px 0', fontSize: '11px', color: '#a0aec0' } }, [\n 'Error ID: ', errorId\n ])\n );\n }\n\n if (stackTrace) {\n children.push(\n tova_el('details', { style: { marginTop: '8px', fontSize: '12px' } }, [\n tova_el('summary', { style: { cursor: 'pointer', color: '#4a5568' } }, ['Stack trace']),\n tova_el('pre', { style: { margin: '4px 0', padding: '8px', background: '#1a202c', color: '#e2e8f0', borderRadius: '4px', overflow: 'auto', fontSize: '11px', maxHeight: '200px' } }, [stackTrace]),\n ])\n );\n }\n\n if (reset) {\n children.push(\n tova_el('button', {\n style: { marginTop: '8px', padding: '6px 16px', background: '#3182ce', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '13px' },\n onClick: reset,\n }, [retryCount > 0 ? 'Retry again' : 'Try again'])\n );\n }\n\n return tova_el('div', {\n style: { padding: '16px', border: '1px solid #fed7d7', borderRadius: '8px', background: '#fff5f5', color: '#2d3748', fontFamily: 'system-ui, -apple-system, sans-serif' },\n role: 'alert',\n }, children);\n}\n\n// ─── Dynamic Component ──────────────────────────────────\n// Renders a component dynamically based on a reactive signal.\n// Usage: Dynamic({ component: mySignal, ...props })\n\nexport function Dynamic({ component, ...rest }) {\n return {\n __tova: true,\n tag: '__dynamic',\n props: {},\n children: [],\n compute: () => {\n const comp = typeof component === 'function' && !component.__tova ? component() : component;\n if (!comp) return null;\n if (typeof comp === 'function') {\n return comp(rest);\n }\n return comp;\n },\n };\n}\n\n// ─── Portal ─────────────────────────────────────────────\n// Renders children into a different DOM target.\n// Usage: Portal({ target: \"#modal-root\", children })\n\nexport function Portal({ target, children }) {\n return {\n __tova: true,\n tag: '__portal',\n props: { target },\n children: children || [],\n };\n}\n\n// ─── Lazy ───────────────────────────────────────────────\n// Async component loading with optional fallback.\n// Usage: const LazyComp = lazy(() => import('./HeavyComponent.js'))\n\nexport function lazy(loader) {\n let resolved = null;\n let loadError = null;\n let promise = null;\n\n return function LazyWrapper(props) {\n if (resolved) {\n return resolved(props);\n }\n\n if (!promise) {\n promise = loader()\n .then(mod => {\n resolved = mod.default || mod;\n })\n .catch(e => { loadError = e; });\n }\n\n const [tick, setTick] = createSignal(0);\n\n // Trigger re-render when promise settles\n promise.then(() => setTick(1)).catch(() => setTick(1));\n\n return {\n __tova: true,\n tag: '__dynamic',\n props: {},\n children: [],\n compute: () => {\n tick(); // Track for reactivity\n if (loadError) return tova_el('span', { className: 'tova-error' }, [String(loadError)]);\n if (resolved) return resolved(props);\n // Fallback while loading\n return props && props.fallback ? props.fallback : null;\n },\n };\n };\n}\n\n// ─── Context (Provide/Inject) ────────────────────────────\n// Tree-based: values are stored on the ownership tree, inject walks up.\n\nexport function createContext(defaultValue) {\n const id = Symbol('context');\n return { _id: id, _default: defaultValue };\n}\n\nexport function provide(context, value) {\n const owner = currentOwner;\n if (owner) {\n if (!owner._contexts) owner._contexts = new Map();\n owner._contexts.set(context._id, value);\n }\n}\n\nexport function inject(context) {\n let owner = currentOwner;\n while (owner) {\n if (owner._contexts && owner._contexts.has(context._id)) {\n return owner._contexts.get(context._id);\n }\n owner = owner._owner;\n }\n return context._default;\n}\n\n// ─── DOM Rendering ────────────────────────────────────────\n\n// Inject scoped CSS into the page (idempotent — only injects once per id)\nconst __tovaInjectedStyles = new Set();\nexport function tova_inject_css(id, css) {\n if (__tovaInjectedStyles.has(id)) return;\n __tovaInjectedStyles.add(id);\n const style = document.createElement('style');\n style.setAttribute('data-tova-style', id);\n style.textContent = css;\n document.head.appendChild(style);\n}\n\nexport function tova_el(tag, props = {}, children = []) {\n return { __tova: true, tag, props, children };\n}\n\nexport function tova_fragment(children) {\n return { __tova: true, tag: '__fragment', props: {}, children };\n}\n\n// ─── Transitions ──────────────────────────────────────────\n// CSS transition directives for mount/unmount animations.\n// Usage: tova_transition(vnode, \"fade\", { duration: 300 })\n\nconst TRANSITION_DEFAULTS = {\n fade: { duration: 200, easing: 'ease' },\n slide: { duration: 300, easing: 'ease-out', axis: 'y' },\n scale: { duration: 200, easing: 'ease' },\n fly: { duration: 300, easing: 'ease-out', x: 0, y: -20 },\n};\n\nfunction getTransitionCSS(name, config, phase) {\n const opts = { ...TRANSITION_DEFAULTS[name], ...config };\n const dur = opts.duration + 'ms';\n const ease = opts.easing;\n\n switch (name) {\n case 'fade':\n if (phase === 'enter-from' || phase === 'leave-to') {\n return { opacity: '0', transition: `opacity ${dur} ${ease}` };\n }\n return { opacity: '1', transition: `opacity ${dur} ${ease}` };\n\n case 'slide': {\n const axis = opts.axis || 'y';\n const prop = axis === 'x' ? 'translateX' : 'translateY';\n const dist = (opts.distance || 20) + 'px';\n if (phase === 'enter-from' || phase === 'leave-to') {\n return { transform: `${prop}(${dist})`, opacity: '0', transition: `transform ${dur} ${ease}, opacity ${dur} ${ease}` };\n }\n return { transform: `${prop}(0)`, opacity: '1', transition: `transform ${dur} ${ease}, opacity ${dur} ${ease}` };\n }\n\n case 'scale':\n if (phase === 'enter-from' || phase === 'leave-to') {\n return { transform: 'scale(0)', opacity: '0', transition: `transform ${dur} ${ease}, opacity ${dur} ${ease}` };\n }\n return { transform: 'scale(1)', opacity: '1', transition: `transform ${dur} ${ease}, opacity ${dur} ${ease}` };\n\n case 'fly': {\n const x = (opts.x || 0) + 'px';\n const y = (opts.y || -20) + 'px';\n if (phase === 'enter-from' || phase === 'leave-to') {\n return { transform: `translate(${x}, ${y})`, opacity: '0', transition: `transform ${dur} ${ease}, opacity ${dur} ${ease}` };\n }\n return { transform: 'translate(0, 0)', opacity: '1', transition: `transform ${dur} ${ease}, opacity ${dur} ${ease}` };\n }\n\n default:\n return {};\n }\n}\n\nexport function tova_transition(vnode, name, config = {}) {\n if (!vnode || !vnode.__tova) return vnode;\n vnode._transition = { name, config };\n return vnode;\n}\n\n// Apply enter transition to a DOM element after render\nfunction applyEnterTransition(el, trans) {\n if (!trans) return;\n const fromStyles = getTransitionCSS(trans.name, trans.config, 'enter-from');\n const toStyles = getTransitionCSS(trans.name, trans.config, 'enter-to');\n\n // Set initial state\n Object.assign(el.style, fromStyles);\n\n // Force reflow, then apply target state\n requestAnimationFrame(() => {\n requestAnimationFrame(() => {\n Object.assign(el.style, toStyles);\n });\n });\n}\n\n// Apply leave transition and return a Promise that resolves when done\nfunction applyLeaveTransition(el, trans) {\n if (!trans) return Promise.resolve();\n const duration = (trans.config && trans.config.duration) || TRANSITION_DEFAULTS[trans.name]?.duration || 200;\n const toStyles = getTransitionCSS(trans.name, trans.config, 'leave-to');\n Object.assign(el.style, toStyles);\n\n return new Promise(resolve => {\n const handler = () => {\n el.removeEventListener('transitionend', handler);\n resolve();\n };\n el.addEventListener('transitionend', handler);\n // Fallback timeout in case transitionend doesn't fire\n setTimeout(resolve, duration + 50);\n });\n}\n\n// Inject a key prop into a vnode for keyed reconciliation\nexport function tova_keyed(key, vnode) {\n if (vnode && vnode.__tova) {\n vnode.props = { ...vnode.props, key };\n }\n return vnode;\n}\n\n// Flatten nested arrays and vnodes into a flat list of vnodes\nfunction flattenVNodes(children) {\n const result = [];\n for (const child of children) {\n if (child === null || child === undefined) {\n continue;\n } else if (Array.isArray(child)) {\n result.push(...flattenVNodes(child));\n } else {\n result.push(child);\n }\n }\n return result;\n}\n\n// ─── Marker-based DOM helpers ─────────────────────────────\n// Instead of wrapping dynamic blocks/fragments in <span style=\"display:contents\">,\n// we use comment node markers. A marker's __tovaNodes tracks its content nodes.\n// Content nodes have __tovaOwner pointing to their owning marker.\n\n// Recursively dispose ownership roots attached to a DOM subtree\nfunction disposeNode(node) {\n if (!node) return;\n if (node.__tovaRoot) {\n node.__tovaRoot();\n node.__tovaRoot = null;\n }\n // If this is a marker, dispose and remove its content nodes\n if (node.__tovaNodes) {\n for (const cn of node.__tovaNodes) {\n disposeNode(cn);\n if (cn.parentNode) cn.parentNode.removeChild(cn);\n }\n node.__tovaNodes = [];\n }\n if (node.childNodes) {\n for (const child of Array.from(node.childNodes)) {\n disposeNode(child);\n }\n }\n}\n\n// Check if a node is transitively owned by a marker (walks __tovaOwner chain)\nfunction isOwnedBy(node, marker) {\n let owner = node.__tovaOwner;\n while (owner) {\n if (owner === marker) return true;\n owner = owner.__tovaOwner;\n }\n return false;\n}\n\n// Get logical children of a parent element (skips marker content nodes)\nfunction getLogicalChildren(parent) {\n const logical = [];\n for (let i = 0; i < parent.childNodes.length; i++) {\n const node = parent.childNodes[i];\n if (!node.__tovaOwner) {\n logical.push(node);\n }\n }\n return logical;\n}\n\n// Find the first DOM sibling after all of a marker's content\nfunction nextSiblingAfterMarker(marker) {\n if (!marker.__tovaNodes || marker.__tovaNodes.length === 0) {\n return marker.nextSibling;\n }\n let last = marker.__tovaNodes[marker.__tovaNodes.length - 1];\n // If last content is itself a marker, recurse to find physical end\n while (last && last.__tovaNodes && last.__tovaNodes.length > 0) {\n last = last.__tovaNodes[last.__tovaNodes.length - 1];\n }\n return last ? last.nextSibling : marker.nextSibling;\n}\n\n// Remove a logical node (marker + its content, or a regular node) from the DOM\nfunction removeLogicalNode(parent, node) {\n disposeNode(node);\n if (node.parentNode === parent) parent.removeChild(node);\n}\n\n// Insert rendered result (could be single node or DocumentFragment) before ref,\n// setting __tovaOwner on top-level inserted nodes. Returns array of inserted nodes.\nfunction insertRendered(parent, rendered, ref, owner) {\n if (rendered.nodeType === 11) {\n const nodes = Array.from(rendered.childNodes);\n for (const n of nodes) {\n if (!n.__tovaOwner) n.__tovaOwner = owner;\n }\n parent.insertBefore(rendered, ref);\n return nodes;\n }\n if (!rendered.__tovaOwner) rendered.__tovaOwner = owner;\n parent.insertBefore(rendered, ref);\n return [rendered];\n}\n\n// Clear a marker's content from the DOM and reset __tovaNodes\nfunction clearMarkerContent(marker) {\n for (const node of marker.__tovaNodes) {\n // If element has a leave transition, animate out before removing\n if (node.__tovaTransition && node.nodeType === 1) {\n const el = node;\n applyLeaveTransition(el, el.__tovaTransition).then(() => {\n disposeNode(el);\n if (el.parentNode) el.parentNode.removeChild(el);\n });\n } else {\n disposeNode(node);\n if (node.parentNode) node.parentNode.removeChild(node);\n }\n }\n marker.__tovaNodes = [];\n}\n\n// ─── Render ───────────────────────────────────────────────\n\n// Create real DOM nodes from a vnode (with fine-grained reactive bindings).\n// Returns a single DOM node for elements/text, or a DocumentFragment for\n// markers (dynamic blocks, fragments) containing [marker, ...content].\nexport function render(vnode) {\n if (vnode === null || vnode === undefined) {\n return document.createTextNode('');\n }\n\n // Reactive dynamic block (JSXIf, JSXFor, reactive text, etc.)\n if (typeof vnode === 'function') {\n const marker = document.createComment('');\n marker.__tovaDynamic = true;\n marker.__tovaNodes = [];\n\n const frag = document.createDocumentFragment();\n frag.appendChild(marker);\n\n createEffect(() => {\n const val = vnode();\n const parent = marker.parentNode;\n const ref = nextSiblingAfterMarker(marker);\n\n // Array: keyed or positional reconciliation within marker range\n if (Array.isArray(val)) {\n const flat = flattenVNodes(val);\n const hasKeys = flat.some(c => getKey(c) != null);\n if (hasKeys) {\n patchKeyedInMarker(marker, flat);\n } else {\n patchPositionalInMarker(marker, flat);\n }\n return;\n }\n\n // Text: optimize single text node update in place\n if (val == null || typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean') {\n const text = val == null ? '' : String(val);\n if (marker.__tovaNodes.length === 1 && marker.__tovaNodes[0].nodeType === 3) {\n if (marker.__tovaNodes[0].textContent !== text) {\n marker.__tovaNodes[0].textContent = text;\n }\n return;\n }\n clearMarkerContent(marker);\n const textNode = document.createTextNode(text);\n textNode.__tovaOwner = marker;\n parent.insertBefore(textNode, ref);\n marker.__tovaNodes = [textNode];\n return;\n }\n\n // Vnode or other: clear and re-render\n clearMarkerContent(marker);\n if (val && val.__tova) {\n const rendered = render(val);\n marker.__tovaNodes = insertRendered(parent, rendered, ref, marker);\n } else {\n const textNode = document.createTextNode(String(val));\n textNode.__tovaOwner = marker;\n parent.insertBefore(textNode, ref);\n marker.__tovaNodes = [textNode];\n }\n });\n\n return frag;\n }\n\n if (typeof vnode === 'string' || typeof vnode === 'number' || typeof vnode === 'boolean') {\n return document.createTextNode(String(vnode));\n }\n\n if (Array.isArray(vnode)) {\n const fragment = document.createDocumentFragment();\n for (const child of vnode) {\n fragment.appendChild(render(child));\n }\n return fragment;\n }\n\n if (!vnode.__tova) {\n return document.createTextNode(String(vnode));\n }\n\n // Fragment — marker + children (no wrapper element)\n if (vnode.tag === '__fragment') {\n const marker = document.createComment('');\n marker.__tovaFragment = true;\n marker.__tovaNodes = [];\n marker.__vnode = vnode;\n\n const frag = document.createDocumentFragment();\n frag.appendChild(marker);\n\n for (const child of flattenVNodes(vnode.children)) {\n const rendered = render(child);\n const inserted = insertRendered(frag, rendered, null, marker);\n marker.__tovaNodes.push(...inserted);\n }\n\n return frag;\n }\n\n // Dynamic reactive node (ErrorBoundary, Dynamic component, etc.)\n if (vnode.tag === '__dynamic' && typeof vnode.compute === 'function') {\n const marker = document.createComment('');\n marker.__tovaDynamic = true;\n marker.__tovaNodes = [];\n\n const frag = document.createDocumentFragment();\n frag.appendChild(marker);\n\n let prevDispose = null;\n const errHandler = vnode._errorHandler || null;\n createEffect(() => {\n if (errHandler) pushErrorHandler(errHandler);\n try {\n const inner = vnode.compute();\n const parent = marker.parentNode;\n const ref = nextSiblingAfterMarker(marker);\n\n if (prevDispose) {\n prevDispose();\n prevDispose = null;\n }\n clearMarkerContent(marker);\n\n createRoot((dispose) => {\n prevDispose = dispose;\n const rendered = render(inner);\n marker.__tovaNodes = insertRendered(parent, rendered, ref, marker);\n });\n } catch (e) {\n if (errHandler) {\n errHandler(e);\n } else if (currentErrorHandler) {\n currentErrorHandler(e);\n } else {\n console.error('Uncaught error during render:', e);\n }\n } finally {\n if (errHandler) popErrorHandler();\n }\n });\n\n return frag;\n }\n\n // Portal — render children into a different DOM target\n if (vnode.tag === '__portal') {\n const placeholder = document.createComment('portal');\n const targetSelector = vnode.props.target;\n queueMicrotask(() => {\n const targetEl = typeof targetSelector === 'string'\n ? document.querySelector(targetSelector)\n : targetSelector;\n if (targetEl) {\n for (const child of flattenVNodes(vnode.children)) {\n targetEl.appendChild(render(child));\n }\n }\n });\n return placeholder;\n }\n\n // Element\n const el = document.createElement(vnode.tag);\n applyReactiveProps(el, vnode.props);\n\n // Set data-tova-component attribute for DevTools\n if (vnode._componentName) {\n el.setAttribute('data-tova-component', vnode._componentName);\n if (__devtools_hooks && __devtools_hooks.onComponentRender) {\n __devtools_hooks.onComponentRender(vnode._componentName, el, 0);\n }\n }\n\n // Render children\n for (const child of flattenVNodes(vnode.children)) {\n el.appendChild(render(child));\n }\n\n // Store vnode reference for patching\n el.__vnode = vnode;\n\n // Apply enter transition if present\n if (vnode._transition) {\n el.__tovaTransition = vnode._transition;\n applyEnterTransition(el, vnode._transition);\n }\n\n return el;\n}\n\n// Apply reactive props — function-valued props get their own effect\nfunction applyReactiveProps(el, props) {\n for (const [key, value] of Object.entries(props)) {\n if (key === 'ref') {\n if (typeof value === 'object' && value !== null && 'current' in value) {\n value.current = el;\n } else if (typeof value === 'function') {\n value(el);\n }\n } else if (key.startsWith('on')) {\n const eventName = key.slice(2).toLowerCase();\n el.addEventListener(eventName, value);\n if (!el.__handlers) el.__handlers = {};\n el.__handlers[eventName] = value;\n } else if (key === 'key') {\n // Skip\n } else if (typeof value === 'function' && !key.startsWith('on')) {\n // Reactive prop — create effect for fine-grained updates\n createEffect(() => {\n const val = value();\n applyPropValue(el, key, val);\n });\n } else {\n applyPropValue(el, key, value);\n }\n }\n}\n\nfunction applyPropValue(el, key, val) {\n if (key === 'className') {\n if (el.className !== val) el.className = val || '';\n } else if (key === 'innerHTML' || key === 'dangerouslySetInnerHTML') {\n const html = typeof val === 'object' && val !== null ? val.__html || '' : val || '';\n if (el.innerHTML !== html) el.innerHTML = html;\n } else if (key === 'value') {\n if (el !== document.activeElement && el.value !== val) {\n el.value = val;\n }\n } else if (key === 'checked') {\n el.checked = !!val;\n } else if (key === 'disabled' || key === 'readOnly' || key === 'hidden') {\n el[key] = !!val;\n } else if (key === 'style' && typeof val === 'object') {\n // Clear old properties not present in new style object\n for (let i = el.style.length - 1; i >= 0; i--) {\n const prop = el.style[i];\n const camel = prop.replace(/-([a-z])/g, (_, c) => c.toUpperCase());\n if (!(prop in val) && !(camel in val)) {\n el.style.removeProperty(prop);\n }\n }\n Object.assign(el.style, val);\n } else {\n const s = val == null ? '' : String(val);\n if (el.getAttribute(key) !== s) {\n el.setAttribute(key, s);\n }\n }\n}\n\n// Apply/update props on a DOM element (used by patcher for full-tree mode)\nfunction applyProps(el, newProps, oldProps) {\n // Remove old props that are no longer present\n for (const key of Object.keys(oldProps)) {\n if (!(key in newProps)) {\n if (key.startsWith('on')) {\n const eventName = key.slice(2).toLowerCase();\n if (el.__handlers && el.__handlers[eventName]) {\n el.removeEventListener(eventName, el.__handlers[eventName]);\n delete el.__handlers[eventName];\n }\n } else if (key === 'className') {\n el.className = '';\n } else if (key === 'style') {\n el.removeAttribute('style');\n } else {\n el.removeAttribute(key);\n }\n }\n }\n\n // Apply new props\n for (const [key, value] of Object.entries(newProps)) {\n if (key === 'className') {\n const val = typeof value === 'function' ? value() : value;\n if (el.className !== val) el.className = val;\n } else if (key === 'ref') {\n if (typeof value === 'object' && value !== null && 'current' in value) {\n value.current = el;\n } else if (typeof value === 'function') {\n value(el);\n }\n } else if (key.startsWith('on')) {\n const eventName = key.slice(2).toLowerCase();\n const oldHandler = el.__handlers && el.__handlers[eventName];\n if (oldHandler !== value) {\n if (oldHandler) el.removeEventListener(eventName, oldHandler);\n el.addEventListener(eventName, value);\n if (!el.__handlers) el.__handlers = {};\n el.__handlers[eventName] = value;\n }\n } else if (key === 'style' && typeof value === 'object') {\n Object.assign(el.style, value);\n } else if (key === 'key') {\n // Skip\n } else if (key === 'value') {\n const val = typeof value === 'function' ? value() : value;\n if (el !== document.activeElement && el.value !== val) {\n el.value = val;\n }\n } else if (key === 'checked') {\n el.checked = !!value;\n } else {\n const val = typeof value === 'function' ? value() : value;\n if (el.getAttribute(key) !== String(val)) {\n el.setAttribute(key, val);\n }\n }\n }\n}\n\n// ─── Keyed Reconciliation ────────────────────────────────\n\nfunction getKey(vnode) {\n if (vnode && vnode.__tova && vnode.props) return vnode.props.key;\n return undefined;\n}\n\nfunction getNodeKey(node) {\n if (node && node.__vnode && node.__vnode.props) return node.__vnode.props.key;\n return undefined;\n}\n\n// Keyed reconciliation within a marker's content range\nfunction patchKeyedInMarker(marker, newVNodes) {\n const parent = marker.parentNode;\n const oldNodes = [...marker.__tovaNodes];\n const oldKeyMap = new Map();\n\n for (const node of oldNodes) {\n const key = getNodeKey(node);\n if (key != null) oldKeyMap.set(key, node);\n }\n\n const newNodes = [];\n const usedOld = new Set();\n\n for (const newChild of newVNodes) {\n const key = getKey(newChild);\n\n if (key != null && oldKeyMap.has(key)) {\n const oldNode = oldKeyMap.get(key);\n usedOld.add(oldNode);\n\n if (oldNode.nodeType === 1 && newChild.__tova &&\n oldNode.tagName.toLowerCase() === newChild.tag.toLowerCase()) {\n const oldVNode = oldNode.__vnode || { props: {}, children: [] };\n applyProps(oldNode, newChild.props, oldVNode.props);\n patchChildrenOfElement(oldNode, flattenVNodes(newChild.children));\n oldNode.__vnode = newChild;\n newNodes.push(oldNode);\n } else {\n const node = render(newChild);\n // render may return Fragment — collect nodes\n if (node.nodeType === 11) {\n const nodes = Array.from(node.childNodes);\n for (const n of nodes) { if (!n.__tovaOwner) n.__tovaOwner = marker; }\n parent.insertBefore(node, nextSiblingAfterMarker(marker));\n newNodes.push(...nodes);\n } else {\n if (!node.__tovaOwner) node.__tovaOwner = marker;\n newNodes.push(node);\n }\n }\n } else {\n const node = render(newChild);\n if (node.nodeType === 11) {\n const nodes = Array.from(node.childNodes);\n for (const n of nodes) { if (!n.__tovaOwner) n.__tovaOwner = marker; }\n parent.insertBefore(node, nextSiblingAfterMarker(marker));\n newNodes.push(...nodes);\n } else {\n if (!node.__tovaOwner) node.__tovaOwner = marker;\n newNodes.push(node);\n }\n }\n }\n\n // Remove unused old nodes\n for (const node of oldNodes) {\n if (!usedOld.has(node)) {\n disposeNode(node);\n if (node.parentNode === parent) parent.removeChild(node);\n }\n }\n\n // Arrange in correct order after marker using cursor approach\n let cursor = marker.nextSibling;\n for (const node of newNodes) {\n if (node === cursor) {\n cursor = node.nextSibling;\n } else {\n parent.insertBefore(node, cursor);\n }\n }\n\n marker.__tovaNodes = newNodes;\n}\n\n// Positional reconciliation within a marker's content range\nfunction patchPositionalInMarker(marker, newChildren) {\n const parent = marker.parentNode;\n const oldNodes = [...marker.__tovaNodes];\n const oldCount = oldNodes.length;\n const newCount = newChildren.length;\n\n // Patch in place\n const patchCount = Math.min(oldCount, newCount);\n for (let i = 0; i < patchCount; i++) {\n patchSingle(parent, oldNodes[i], newChildren[i]);\n }\n\n // Append new children\n const ref = nextSiblingAfterMarker(marker);\n for (let i = oldCount; i < newCount; i++) {\n const rendered = render(newChildren[i]);\n const inserted = insertRendered(parent, rendered, ref, marker);\n oldNodes.push(...inserted);\n }\n\n // Remove excess children\n for (let i = newCount; i < oldCount; i++) {\n disposeNode(oldNodes[i]);\n if (oldNodes[i].parentNode === parent) parent.removeChild(oldNodes[i]);\n }\n\n marker.__tovaNodes = oldNodes.slice(0, newCount);\n}\n\n// Keyed reconciliation for children of an element (not marker-based)\nfunction patchKeyedChildren(parent, newVNodes) {\n const logical = getLogicalChildren(parent);\n const oldKeyMap = new Map();\n\n for (const node of logical) {\n const key = getNodeKey(node);\n if (key != null) oldKeyMap.set(key, node);\n }\n\n const newNodes = [];\n const usedOld = new Set();\n\n for (const newChild of newVNodes) {\n const key = getKey(newChild);\n\n if (key != null && oldKeyMap.has(key)) {\n const oldNode = oldKeyMap.get(key);\n usedOld.add(oldNode);\n\n if (oldNode.nodeType === 1 && newChild.__tova &&\n oldNode.tagName.toLowerCase() === newChild.tag.toLowerCase()) {\n const oldVNode = oldNode.__vnode || { props: {}, children: [] };\n applyProps(oldNode, newChild.props, oldVNode.props);\n patchChildrenOfElement(oldNode, flattenVNodes(newChild.children));\n oldNode.__vnode = newChild;\n newNodes.push(oldNode);\n } else {\n newNodes.push(render(newChild));\n }\n } else {\n newNodes.push(render(newChild));\n }\n }\n\n // Remove unused old logical nodes\n for (const node of logical) {\n if (!usedOld.has(node) && node.parentNode === parent) {\n removeLogicalNode(parent, node);\n }\n }\n\n // Arrange in correct order\n for (let i = 0; i < newNodes.length; i++) {\n const expected = newNodes[i];\n const logicalNow = getLogicalChildren(parent);\n const current = logicalNow[i];\n if (current !== expected) {\n parent.insertBefore(expected, current || null);\n }\n }\n}\n\n// Positional reconciliation for children of an element\nfunction patchPositionalChildren(parent, newChildren) {\n const logical = getLogicalChildren(parent);\n const oldCount = logical.length;\n const newCount = newChildren.length;\n\n for (let i = 0; i < Math.min(oldCount, newCount); i++) {\n patchSingle(parent, logical[i], newChildren[i]);\n }\n\n for (let i = oldCount; i < newCount; i++) {\n parent.appendChild(render(newChildren[i]));\n }\n\n // Remove excess logical children\n const currentLogical = getLogicalChildren(parent);\n while (currentLogical.length > newCount) {\n const node = currentLogical.pop();\n removeLogicalNode(parent, node);\n }\n}\n\n// Patch children of a regular element\nfunction patchChildrenOfElement(el, newChildren) {\n const hasKeys = newChildren.some(c => getKey(c) != null);\n if (hasKeys) {\n patchKeyedChildren(el, newChildren);\n } else {\n patchPositionalChildren(el, newChildren);\n }\n}\n\n// Patch a single logical node in place\nfunction patchSingle(parent, existing, newVNode) {\n if (!existing) {\n parent.appendChild(render(newVNode));\n return;\n }\n\n if (newVNode === null || newVNode === undefined) {\n removeLogicalNode(parent, existing);\n return;\n }\n\n // Function vnode — replace with new dynamic block\n if (typeof newVNode === 'function') {\n const rendered = render(newVNode);\n if (existing.__tovaNodes) {\n // Existing is a marker — clear its content and replace\n clearMarkerContent(existing);\n parent.replaceChild(rendered, existing);\n } else {\n disposeNode(existing);\n parent.replaceChild(rendered, existing);\n }\n return;\n }\n\n // Text\n if (typeof newVNode === 'string' || typeof newVNode === 'number' || typeof newVNode === 'boolean') {\n const text = String(newVNode);\n if (existing.nodeType === 3) {\n if (existing.textContent !== text) existing.textContent = text;\n } else {\n removeLogicalNode(parent, existing);\n parent.insertBefore(document.createTextNode(text), null);\n }\n return;\n }\n\n if (!newVNode.__tova) {\n const text = String(newVNode);\n if (existing.nodeType === 3) {\n if (existing.textContent !== text) existing.textContent = text;\n } else {\n removeLogicalNode(parent, existing);\n parent.insertBefore(document.createTextNode(text), null);\n }\n return;\n }\n\n // Fragment — patch marker content\n if (newVNode.tag === '__fragment') {\n if (existing.__tovaFragment) {\n // Patch children within the marker range\n const oldNodes = [...existing.__tovaNodes];\n const newChildren = flattenVNodes(newVNode.children);\n // Simple approach: clear and re-render fragment content\n clearMarkerContent(existing);\n const ref = nextSiblingAfterMarker(existing);\n for (const child of newChildren) {\n const rendered = render(child);\n const inserted = insertRendered(parent, rendered, ref, existing);\n existing.__tovaNodes.push(...inserted);\n }\n existing.__vnode = newVNode;\n return;\n }\n removeLogicalNode(parent, existing);\n parent.appendChild(render(newVNode));\n return;\n }\n\n // Element — patch in place\n if (existing.nodeType === 1 && newVNode.tag &&\n existing.tagName.toLowerCase() === newVNode.tag.toLowerCase()) {\n const oldVNode = existing.__vnode || { props: {}, children: [] };\n applyProps(existing, newVNode.props, oldVNode.props);\n patchChildrenOfElement(existing, flattenVNodes(newVNode.children));\n existing.__vnode = newVNode;\n return;\n }\n\n // Different type — full replace\n removeLogicalNode(parent, existing);\n parent.appendChild(render(newVNode));\n}\n\n// ─── Hydration (SSR) ─────────────────────────────────────\n// SSR renders flat HTML without markers. Hydration attaches reactivity\n// to existing DOM nodes and inserts markers for dynamic blocks.\n\n// Dev-mode hydration mismatch detection\nfunction checkHydrationMismatch(domNode, vnode) {\n if (!__DEV__) return;\n if (!domNode || !vnode || !vnode.__tova) return;\n\n const props = vnode.props || {};\n\n // Check className\n if (props.className !== undefined) {\n const expected = typeof props.className === 'function' ? props.className() : props.className;\n const actual = domNode.className || '';\n if (expected && actual !== expected) {\n console.warn(`Tova hydration mismatch: <${vnode.tag}> class expected \"${expected}\" but got \"${actual}\"`);\n }\n }\n\n // Check attributes\n for (const [key, value] of Object.entries(props)) {\n if (key === 'key' || key === 'ref' || key === 'className' || key.startsWith('on')) continue;\n if (typeof value === 'function') continue; // reactive props — skip static check\n\n if (domNode.getAttribute) {\n const attrName = key === 'className' ? 'class' : key;\n const actual = domNode.getAttribute(attrName);\n const expected = String(value);\n if (actual !== null && actual !== expected) {\n console.warn(`Tova hydration mismatch: <${vnode.tag}> attribute \"${key}\" expected \"${expected}\" but got \"${actual}\"`);\n }\n }\n }\n}\n\n// Check if a DOM node is an SSR marker comment (<!--tova-s:ID-->)\nfunction isSSRMarker(node) {\n return node && node.nodeType === 8 && typeof node.data === 'string' && node.data.startsWith('tova-s:');\n}\n\n// Find the closing SSR marker and collect content nodes between them\nfunction collectSSRMarkerContent(startMarker) {\n const id = startMarker.data.replace('tova-s:', '');\n const closingText = `/tova-s:${id}`;\n const content = [];\n let cursor = startMarker.nextSibling;\n while (cursor) {\n if (cursor.nodeType === 8 && cursor.data === closingText) {\n return { content, endMarker: cursor };\n }\n content.push(cursor);\n cursor = cursor.nextSibling;\n }\n return { content, endMarker: null };\n}\n\nfunction hydrateVNode(domNode, vnode) {\n if (!domNode) return null;\n if (vnode === null || vnode === undefined) return domNode;\n\n // Function vnode (reactive text, JSXIf, JSXFor)\n if (typeof vnode === 'function') {\n if (domNode.nodeType === 3) {\n // Dev-mode: warn if text content differs\n if (__DEV__) {\n const val = vnode();\n const expected = val == null ? '' : String(val);\n if (domNode.textContent !== expected) {\n console.warn(`Tova hydration mismatch: text expected \"${expected}\" but got \"${domNode.textContent}\"`);\n }\n }\n // Reactive text: attach effect to existing text node\n domNode.__tovaReactive = true;\n createEffect(() => {\n const val = vnode();\n const text = val == null ? '' : String(val);\n if (domNode.textContent !== text) domNode.textContent = text;\n });\n return domNode.nextSibling;\n }\n // Complex dynamic block: insert marker-based render, replace SSR node\n const parent = domNode.parentNode;\n const next = domNode.nextSibling;\n const rendered = render(vnode);\n parent.replaceChild(rendered, domNode);\n return next;\n }\n\n // Primitive text — already correct from SSR\n if (typeof vnode === 'string' || typeof vnode === 'number' || typeof vnode === 'boolean') {\n if (__DEV__ && domNode.nodeType === 3) {\n const expected = String(vnode);\n if (domNode.textContent !== expected) {\n console.warn(`Tova hydration mismatch: text expected \"${expected}\" but got \"${domNode.textContent}\"`);\n }\n }\n return domNode.nextSibling;\n }\n\n // Array\n if (Array.isArray(vnode)) {\n let cursor = domNode;\n for (const child of flattenVNodes(vnode)) {\n if (!cursor) break;\n cursor = hydrateVNode(cursor, child);\n }\n return cursor;\n }\n\n if (!vnode.__tova) return domNode.nextSibling;\n\n // Fragment — children rendered inline in SSR (no wrapper)\n if (vnode.tag === '__fragment') {\n const children = flattenVNodes(vnode.children);\n let cursor = domNode;\n for (const child of children) {\n if (!cursor) break;\n cursor = hydrateVNode(cursor, child);\n }\n return cursor;\n }\n\n // Dynamic node — SSR marker-aware hydration\n if (vnode.tag === '__dynamic' && typeof vnode.compute === 'function') {\n // Check if current domNode is an SSR marker (<!--tova-s:ID-->)\n if (isSSRMarker(domNode)) {\n const { content, endMarker } = collectSSRMarkerContent(domNode);\n const parent = domNode.parentNode;\n\n // Remove SSR markers and content, replace with reactive marker\n const afterEnd = endMarker ? endMarker.nextSibling : null;\n for (const node of content) {\n if (node.parentNode === parent) parent.removeChild(node);\n }\n if (endMarker && endMarker.parentNode === parent) parent.removeChild(endMarker);\n\n const rendered = render(vnode);\n parent.replaceChild(rendered, domNode);\n return afterEnd;\n }\n\n // No SSR marker — fall back to standard behavior\n const parent = domNode.parentNode;\n const next = domNode.nextSibling;\n const rendered = render(vnode);\n parent.replaceChild(rendered, domNode);\n return next;\n }\n\n // Element — attach event handlers, reactive props, refs\n if (domNode.nodeType === 1 && domNode.tagName.toLowerCase() === vnode.tag.toLowerCase()) {\n if (__DEV__) checkHydrationMismatch(domNode, vnode);\n hydrateProps(domNode, vnode.props);\n domNode.__vnode = vnode;\n\n const children = flattenVNodes(vnode.children || []);\n let cursor = domNode.firstChild;\n for (const child of children) {\n if (!cursor) break;\n cursor = hydrateVNode(cursor, child);\n }\n return domNode.nextSibling;\n }\n\n // Tag mismatch — fall back to full render\n if (__DEV__) {\n const expectedTag = vnode.tag || '(unknown)';\n const actualTag = domNode.tagName ? domNode.tagName.toLowerCase() : `nodeType:${domNode.nodeType}`;\n console.warn(`Tova hydration mismatch: expected <${expectedTag}> but got <${actualTag}>, falling back to full render`);\n }\n const parent = domNode.parentNode;\n const next = domNode.nextSibling;\n const rendered = render(vnode);\n parent.replaceChild(rendered, domNode);\n return next;\n}\n\nfunction hydrateProps(el, props) {\n for (const [key, value] of Object.entries(props)) {\n if (key === 'ref') {\n if (typeof value === 'object' && value !== null && 'current' in value) {\n value.current = el;\n } else if (typeof value === 'function') {\n value(el);\n }\n } else if (key.startsWith('on')) {\n const eventName = key.slice(2).toLowerCase();\n el.addEventListener(eventName, value);\n if (!el.__handlers) el.__handlers = {};\n el.__handlers[eventName] = value;\n } else if (key === 'key') {\n // Skip\n } else if (typeof value === 'function' && !key.startsWith('on')) {\n createEffect(() => {\n const val = value();\n applyPropValue(el, key, val);\n });\n }\n }\n}\n\nexport function hydrate(component, container) {\n if (!container) {\n console.error('Tova: Hydration target not found');\n return;\n }\n\n const startTime = typeof performance !== 'undefined' ? performance.now() : 0;\n\n const result = createRoot(() => {\n const vnode = typeof component === 'function' ? component() : component;\n if (container.firstChild) {\n hydrateVNode(container.firstChild, vnode);\n }\n });\n\n // Dispatch hydration completion event\n const duration = typeof performance !== 'undefined' ? performance.now() - startTime : 0;\n if (typeof CustomEvent !== 'undefined' && typeof container.dispatchEvent === 'function') {\n container.dispatchEvent(new CustomEvent('tova:hydrated', { detail: { duration }, bubbles: true }));\n }\n\n if (__devtools_hooks && __devtools_hooks.onHydrate) {\n __devtools_hooks.onHydrate({ duration });\n }\n\n return result;\n}\n\nexport function mount(component, container) {\n if (!container) {\n console.error('Tova: Mount target not found');\n return;\n }\n\n const result = createRoot((dispose) => {\n const vnode = typeof component === 'function' ? component() : component;\n container.innerHTML = '';\n container.appendChild(render(vnode));\n return dispose;\n });\n\n if (__devtools_hooks && __devtools_hooks.onMount) {\n __devtools_hooks.onMount();\n }\n\n return result;\n}\n\n// ─── Progressive Hydration ──────────────────────────────────\n// Hydrate a component only when it becomes visible in the viewport.\n\nexport function hydrateWhenVisible(component, domNode, options = {}) {\n if (typeof IntersectionObserver === 'undefined') {\n // Fallback: hydrate immediately\n return hydrate(component, domNode);\n }\n\n const { rootMargin = '200px' } = options;\n let hydrated = false;\n\n const observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (entry.isIntersecting && !hydrated) {\n hydrated = true;\n observer.disconnect();\n hydrate(component, domNode);\n }\n }\n },\n { rootMargin },\n );\n\n observer.observe(domNode);\n\n return () => {\n observer.disconnect();\n };\n}\n";
|
|
4
4
|
|
|
5
5
|
export const RPC_SOURCE = "// RPC bridge — client calls to server functions are auto-routed via HTTP\n\nconst RPC_BASE = typeof window !== 'undefined'\n ? (window.__TOVA_RPC_BASE || '')\n : 'http://localhost:3000';\n\nexport async function rpc(functionName, args = []) {\n const url = `${RPC_BASE}/rpc/${functionName}`;\n\n // Convert positional args to object if needed\n let body;\n if (args.length === 1 && typeof args[0] === 'object' && !Array.isArray(args[0])) {\n body = args[0];\n } else if (args.length > 0) {\n // Send as array, server will handle positional mapping\n body = { __args: args };\n } else {\n body = {};\n }\n\n try {\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`RPC call to '${functionName}' failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n return data.result;\n } catch (error) {\n if (error.message.includes('RPC call')) throw error;\n throw new Error(`RPC call to '${functionName}' failed: ${error.message}`);\n }\n}\n\n// Configure RPC base URL\nexport function configureRPC(baseUrl) {\n if (typeof window !== 'undefined') {\n window.__TOVA_RPC_BASE = baseUrl;\n }\n}\n";
|
|
6
6
|
|
|
@@ -447,20 +447,36 @@ export function createErrorBoundary(options = {}) {
|
|
|
447
447
|
return { error, run, reset };
|
|
448
448
|
}
|
|
449
449
|
|
|
450
|
-
|
|
450
|
+
let __errorBoundaryIdCounter = 0;
|
|
451
|
+
|
|
452
|
+
export function ErrorBoundary({ fallback, children, onError, onReset, onErrorCleared, retry = 0 }) {
|
|
451
453
|
const [error, setError] = createSignal(null);
|
|
452
454
|
const [retryCount, setRetryCount] = createSignal(0);
|
|
455
|
+
const boundaryId = ++__errorBoundaryIdCounter;
|
|
456
|
+
let lastErrorId = 0;
|
|
453
457
|
|
|
454
458
|
function handleError(e) {
|
|
455
459
|
const stack = buildComponentStack();
|
|
456
|
-
|
|
460
|
+
const errorId = `EB${boundaryId}-${++lastErrorId}`;
|
|
461
|
+
|
|
462
|
+
if (e && typeof e === 'object') {
|
|
463
|
+
e.__tovaComponentStack = stack;
|
|
464
|
+
e.__tovaErrorId = errorId;
|
|
465
|
+
}
|
|
466
|
+
|
|
457
467
|
if (retryCount() < retry) {
|
|
458
468
|
setRetryCount(c => c + 1);
|
|
459
469
|
setError(null); // clear to re-trigger render
|
|
460
470
|
return;
|
|
461
471
|
}
|
|
462
472
|
setError(e);
|
|
463
|
-
if (onError) onError({ error: e, componentStack: stack });
|
|
473
|
+
if (onError) onError({ error: e, componentStack: stack, errorId, retryCount: retryCount() });
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function resetBoundary() {
|
|
477
|
+
setRetryCount(0);
|
|
478
|
+
setError(null);
|
|
479
|
+
if (onReset) onReset();
|
|
464
480
|
}
|
|
465
481
|
|
|
466
482
|
// Return a reactive wrapper that switches between children and fallback
|
|
@@ -479,14 +495,14 @@ export function ErrorBoundary({ fallback, children, onError, onReset, retry = 0
|
|
|
479
495
|
if (err) {
|
|
480
496
|
// Render fallback — if fallback itself throws, propagate to parent boundary
|
|
481
497
|
try {
|
|
498
|
+
const errorId = err && typeof err === 'object' ? err.__tovaErrorId : null;
|
|
482
499
|
return typeof fallback === 'function'
|
|
483
500
|
? fallback({
|
|
484
501
|
error: err,
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
},
|
|
502
|
+
errorId,
|
|
503
|
+
retryCount: retryCount(),
|
|
504
|
+
componentStack: err && typeof err === 'object' ? err.__tovaComponentStack : [],
|
|
505
|
+
reset: resetBoundary,
|
|
490
506
|
})
|
|
491
507
|
: fallback;
|
|
492
508
|
} catch (fallbackError) {
|
|
@@ -497,6 +513,10 @@ export function ErrorBoundary({ fallback, children, onError, onReset, retry = 0
|
|
|
497
513
|
return null;
|
|
498
514
|
}
|
|
499
515
|
}
|
|
516
|
+
// Children rendered successfully — fire onErrorCleared if we recovered from an error
|
|
517
|
+
if (onErrorCleared && lastErrorId > 0 && retryCount() === 0) {
|
|
518
|
+
queueMicrotask(() => onErrorCleared());
|
|
519
|
+
}
|
|
500
520
|
return childContent;
|
|
501
521
|
},
|
|
502
522
|
};
|
|
@@ -504,6 +524,58 @@ export function ErrorBoundary({ fallback, children, onError, onReset, retry = 0
|
|
|
504
524
|
return vnode;
|
|
505
525
|
}
|
|
506
526
|
|
|
527
|
+
// Built-in ErrorInfo component — renders a formatted error display
|
|
528
|
+
// Usage: <ErrorBoundary fallback={fn(props) ErrorInfo(props)} />
|
|
529
|
+
export function ErrorInfo({ error, errorId, componentStack, reset, retryCount }) {
|
|
530
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
531
|
+
const stackTrace = error instanceof Error && error.stack ? error.stack : '';
|
|
532
|
+
const compStack = (componentStack || []).join(' > ');
|
|
533
|
+
|
|
534
|
+
const children = [
|
|
535
|
+
tova_el('h3', { style: { margin: '0 0 8px 0', color: '#e53e3e' } }, ['Something went wrong']),
|
|
536
|
+
tova_el('p', { style: { margin: '4px 0', fontFamily: 'monospace', fontSize: '14px' } }, [message]),
|
|
537
|
+
];
|
|
538
|
+
|
|
539
|
+
if (compStack) {
|
|
540
|
+
children.push(
|
|
541
|
+
tova_el('p', { style: { margin: '4px 0', fontSize: '12px', color: '#718096' } }, [
|
|
542
|
+
'Component: ', compStack
|
|
543
|
+
])
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (errorId) {
|
|
548
|
+
children.push(
|
|
549
|
+
tova_el('p', { style: { margin: '4px 0', fontSize: '11px', color: '#a0aec0' } }, [
|
|
550
|
+
'Error ID: ', errorId
|
|
551
|
+
])
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if (stackTrace) {
|
|
556
|
+
children.push(
|
|
557
|
+
tova_el('details', { style: { marginTop: '8px', fontSize: '12px' } }, [
|
|
558
|
+
tova_el('summary', { style: { cursor: 'pointer', color: '#4a5568' } }, ['Stack trace']),
|
|
559
|
+
tova_el('pre', { style: { margin: '4px 0', padding: '8px', background: '#1a202c', color: '#e2e8f0', borderRadius: '4px', overflow: 'auto', fontSize: '11px', maxHeight: '200px' } }, [stackTrace]),
|
|
560
|
+
])
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (reset) {
|
|
565
|
+
children.push(
|
|
566
|
+
tova_el('button', {
|
|
567
|
+
style: { marginTop: '8px', padding: '6px 16px', background: '#3182ce', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '13px' },
|
|
568
|
+
onClick: reset,
|
|
569
|
+
}, [retryCount > 0 ? 'Retry again' : 'Try again'])
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
return tova_el('div', {
|
|
574
|
+
style: { padding: '16px', border: '1px solid #fed7d7', borderRadius: '8px', background: '#fff5f5', color: '#2d3748', fontFamily: 'system-ui, -apple-system, sans-serif' },
|
|
575
|
+
role: 'alert',
|
|
576
|
+
}, children);
|
|
577
|
+
}
|
|
578
|
+
|
|
507
579
|
// ─── Dynamic Component ──────────────────────────────────
|
|
508
580
|
// Renders a component dynamically based on a reactive signal.
|
|
509
581
|
// Usage: Dynamic({ component: mySignal, ...props })
|
|
@@ -629,6 +701,100 @@ export function tova_fragment(children) {
|
|
|
629
701
|
return { __tova: true, tag: '__fragment', props: {}, children };
|
|
630
702
|
}
|
|
631
703
|
|
|
704
|
+
// ─── Transitions ──────────────────────────────────────────
|
|
705
|
+
// CSS transition directives for mount/unmount animations.
|
|
706
|
+
// Usage: tova_transition(vnode, "fade", { duration: 300 })
|
|
707
|
+
|
|
708
|
+
const TRANSITION_DEFAULTS = {
|
|
709
|
+
fade: { duration: 200, easing: 'ease' },
|
|
710
|
+
slide: { duration: 300, easing: 'ease-out', axis: 'y' },
|
|
711
|
+
scale: { duration: 200, easing: 'ease' },
|
|
712
|
+
fly: { duration: 300, easing: 'ease-out', x: 0, y: -20 },
|
|
713
|
+
};
|
|
714
|
+
|
|
715
|
+
function getTransitionCSS(name, config, phase) {
|
|
716
|
+
const opts = { ...TRANSITION_DEFAULTS[name], ...config };
|
|
717
|
+
const dur = opts.duration + 'ms';
|
|
718
|
+
const ease = opts.easing;
|
|
719
|
+
|
|
720
|
+
switch (name) {
|
|
721
|
+
case 'fade':
|
|
722
|
+
if (phase === 'enter-from' || phase === 'leave-to') {
|
|
723
|
+
return { opacity: '0', transition: `opacity ${dur} ${ease}` };
|
|
724
|
+
}
|
|
725
|
+
return { opacity: '1', transition: `opacity ${dur} ${ease}` };
|
|
726
|
+
|
|
727
|
+
case 'slide': {
|
|
728
|
+
const axis = opts.axis || 'y';
|
|
729
|
+
const prop = axis === 'x' ? 'translateX' : 'translateY';
|
|
730
|
+
const dist = (opts.distance || 20) + 'px';
|
|
731
|
+
if (phase === 'enter-from' || phase === 'leave-to') {
|
|
732
|
+
return { transform: `${prop}(${dist})`, opacity: '0', transition: `transform ${dur} ${ease}, opacity ${dur} ${ease}` };
|
|
733
|
+
}
|
|
734
|
+
return { transform: `${prop}(0)`, opacity: '1', transition: `transform ${dur} ${ease}, opacity ${dur} ${ease}` };
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
case 'scale':
|
|
738
|
+
if (phase === 'enter-from' || phase === 'leave-to') {
|
|
739
|
+
return { transform: 'scale(0)', opacity: '0', transition: `transform ${dur} ${ease}, opacity ${dur} ${ease}` };
|
|
740
|
+
}
|
|
741
|
+
return { transform: 'scale(1)', opacity: '1', transition: `transform ${dur} ${ease}, opacity ${dur} ${ease}` };
|
|
742
|
+
|
|
743
|
+
case 'fly': {
|
|
744
|
+
const x = (opts.x || 0) + 'px';
|
|
745
|
+
const y = (opts.y || -20) + 'px';
|
|
746
|
+
if (phase === 'enter-from' || phase === 'leave-to') {
|
|
747
|
+
return { transform: `translate(${x}, ${y})`, opacity: '0', transition: `transform ${dur} ${ease}, opacity ${dur} ${ease}` };
|
|
748
|
+
}
|
|
749
|
+
return { transform: 'translate(0, 0)', opacity: '1', transition: `transform ${dur} ${ease}, opacity ${dur} ${ease}` };
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
default:
|
|
753
|
+
return {};
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
export function tova_transition(vnode, name, config = {}) {
|
|
758
|
+
if (!vnode || !vnode.__tova) return vnode;
|
|
759
|
+
vnode._transition = { name, config };
|
|
760
|
+
return vnode;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// Apply enter transition to a DOM element after render
|
|
764
|
+
function applyEnterTransition(el, trans) {
|
|
765
|
+
if (!trans) return;
|
|
766
|
+
const fromStyles = getTransitionCSS(trans.name, trans.config, 'enter-from');
|
|
767
|
+
const toStyles = getTransitionCSS(trans.name, trans.config, 'enter-to');
|
|
768
|
+
|
|
769
|
+
// Set initial state
|
|
770
|
+
Object.assign(el.style, fromStyles);
|
|
771
|
+
|
|
772
|
+
// Force reflow, then apply target state
|
|
773
|
+
requestAnimationFrame(() => {
|
|
774
|
+
requestAnimationFrame(() => {
|
|
775
|
+
Object.assign(el.style, toStyles);
|
|
776
|
+
});
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// Apply leave transition and return a Promise that resolves when done
|
|
781
|
+
function applyLeaveTransition(el, trans) {
|
|
782
|
+
if (!trans) return Promise.resolve();
|
|
783
|
+
const duration = (trans.config && trans.config.duration) || TRANSITION_DEFAULTS[trans.name]?.duration || 200;
|
|
784
|
+
const toStyles = getTransitionCSS(trans.name, trans.config, 'leave-to');
|
|
785
|
+
Object.assign(el.style, toStyles);
|
|
786
|
+
|
|
787
|
+
return new Promise(resolve => {
|
|
788
|
+
const handler = () => {
|
|
789
|
+
el.removeEventListener('transitionend', handler);
|
|
790
|
+
resolve();
|
|
791
|
+
};
|
|
792
|
+
el.addEventListener('transitionend', handler);
|
|
793
|
+
// Fallback timeout in case transitionend doesn't fire
|
|
794
|
+
setTimeout(resolve, duration + 50);
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
|
|
632
798
|
// Inject a key prop into a vnode for keyed reconciliation
|
|
633
799
|
export function tova_keyed(key, vnode) {
|
|
634
800
|
if (vnode && vnode.__tova) {
|
|
@@ -739,8 +905,17 @@ function insertRendered(parent, rendered, ref, owner) {
|
|
|
739
905
|
// Clear a marker's content from the DOM and reset __tovaNodes
|
|
740
906
|
function clearMarkerContent(marker) {
|
|
741
907
|
for (const node of marker.__tovaNodes) {
|
|
742
|
-
|
|
743
|
-
if (node.
|
|
908
|
+
// If element has a leave transition, animate out before removing
|
|
909
|
+
if (node.__tovaTransition && node.nodeType === 1) {
|
|
910
|
+
const el = node;
|
|
911
|
+
applyLeaveTransition(el, el.__tovaTransition).then(() => {
|
|
912
|
+
disposeNode(el);
|
|
913
|
+
if (el.parentNode) el.parentNode.removeChild(el);
|
|
914
|
+
});
|
|
915
|
+
} else {
|
|
916
|
+
disposeNode(node);
|
|
917
|
+
if (node.parentNode) node.parentNode.removeChild(node);
|
|
918
|
+
}
|
|
744
919
|
}
|
|
745
920
|
marker.__tovaNodes = [];
|
|
746
921
|
}
|
|
@@ -931,6 +1106,12 @@ export function render(vnode) {
|
|
|
931
1106
|
// Store vnode reference for patching
|
|
932
1107
|
el.__vnode = vnode;
|
|
933
1108
|
|
|
1109
|
+
// Apply enter transition if present
|
|
1110
|
+
if (vnode._transition) {
|
|
1111
|
+
el.__tovaTransition = vnode._transition;
|
|
1112
|
+
applyEnterTransition(el, vnode._transition);
|
|
1113
|
+
}
|
|
1114
|
+
|
|
934
1115
|
return el;
|
|
935
1116
|
}
|
|
936
1117
|
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Advanced collection data structures for Tova
|
|
2
|
+
// These are the real JS implementations used for testing and reference.
|
|
3
|
+
|
|
4
|
+
export class OrderedDict {
|
|
5
|
+
constructor(entries) {
|
|
6
|
+
this._map = new Map(entries || []);
|
|
7
|
+
}
|
|
8
|
+
get(key) { return this._map.has(key) ? this._map.get(key) : null; }
|
|
9
|
+
set(key, value) { const m = new Map(this._map); m.set(key, value); return new OrderedDict([...m]); }
|
|
10
|
+
delete(key) { const m = new Map(this._map); m.delete(key); return new OrderedDict([...m]); }
|
|
11
|
+
has(key) { return this._map.has(key); }
|
|
12
|
+
keys() { return [...this._map.keys()]; }
|
|
13
|
+
values() { return [...this._map.values()]; }
|
|
14
|
+
entries() { return [...this._map.entries()]; }
|
|
15
|
+
get length() { return this._map.size; }
|
|
16
|
+
[Symbol.iterator]() { return this._map[Symbol.iterator](); }
|
|
17
|
+
toString() { return 'OrderedDict(' + this._map.size + ' entries)'; }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class DefaultDict {
|
|
21
|
+
constructor(defaultFn) {
|
|
22
|
+
this._map = new Map();
|
|
23
|
+
this._default = defaultFn;
|
|
24
|
+
}
|
|
25
|
+
get(key) {
|
|
26
|
+
if (!this._map.has(key)) {
|
|
27
|
+
this._map.set(key, this._default());
|
|
28
|
+
}
|
|
29
|
+
return this._map.get(key);
|
|
30
|
+
}
|
|
31
|
+
set(key, value) { this._map.set(key, value); return this; }
|
|
32
|
+
has(key) { return this._map.has(key); }
|
|
33
|
+
delete(key) { this._map.delete(key); return this; }
|
|
34
|
+
keys() { return [...this._map.keys()]; }
|
|
35
|
+
values() { return [...this._map.values()]; }
|
|
36
|
+
entries() { return [...this._map.entries()]; }
|
|
37
|
+
get length() { return this._map.size; }
|
|
38
|
+
[Symbol.iterator]() { return this._map[Symbol.iterator](); }
|
|
39
|
+
toString() { return 'DefaultDict(' + this._map.size + ' entries)'; }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class Counter {
|
|
43
|
+
constructor(items) {
|
|
44
|
+
this._counts = new Map();
|
|
45
|
+
if (items) {
|
|
46
|
+
for (const item of items) {
|
|
47
|
+
const k = item;
|
|
48
|
+
this._counts.set(k, (this._counts.get(k) || 0) + 1);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
count(item) { return this._counts.get(item) || 0; }
|
|
53
|
+
total() { let s = 0; for (const v of this._counts.values()) s += v; return s; }
|
|
54
|
+
most_common(n) {
|
|
55
|
+
const sorted = [...this._counts.entries()].sort((a, b) => b[1] - a[1]);
|
|
56
|
+
return n !== undefined ? sorted.slice(0, n) : sorted;
|
|
57
|
+
}
|
|
58
|
+
keys() { return [...this._counts.keys()]; }
|
|
59
|
+
values() { return [...this._counts.values()]; }
|
|
60
|
+
entries() { return [...this._counts.entries()]; }
|
|
61
|
+
has(item) { return this._counts.has(item); }
|
|
62
|
+
get length() { return this._counts.size; }
|
|
63
|
+
[Symbol.iterator]() { return this._counts[Symbol.iterator](); }
|
|
64
|
+
toString() { return 'Counter(' + this._counts.size + ' items)'; }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export class Deque {
|
|
68
|
+
constructor(items) {
|
|
69
|
+
this._items = items ? [...items] : [];
|
|
70
|
+
}
|
|
71
|
+
push_back(val) { return new Deque([...this._items, val]); }
|
|
72
|
+
push_front(val) { return new Deque([val, ...this._items]); }
|
|
73
|
+
pop_back() { if (this._items.length === 0) return [null, this]; const items = this._items.slice(0, -1); return [this._items[this._items.length - 1], new Deque(items)]; }
|
|
74
|
+
pop_front() { if (this._items.length === 0) return [null, this]; return [this._items[0], new Deque(this._items.slice(1))]; }
|
|
75
|
+
peek_front() { return this._items.length > 0 ? this._items[0] : null; }
|
|
76
|
+
peek_back() { return this._items.length > 0 ? this._items[this._items.length - 1] : null; }
|
|
77
|
+
get length() { return this._items.length; }
|
|
78
|
+
toArray() { return [...this._items]; }
|
|
79
|
+
[Symbol.iterator]() { return this._items[Symbol.iterator](); }
|
|
80
|
+
toString() { return 'Deque(' + this._items.length + ' items)'; }
|
|
81
|
+
}
|