{"version":3,"file":"hydrate-CNPyhsxb.cjs","names":[],"sources":["../../src/island/hydrate.ts"],"sourcesContent":["import type { NixTemplate } from \"@deijose/nix-js\";\nimport { hydrate as hydrateTemplate } from \"@deijose/nix-js/hydrate\";\nimport type { IslandDirective } from \"./island.js\";\n\n// --- Client-side island hydration ---\n\n// Keep track of every active island dispose so we can clean them up before a\n// client-side navigation swaps the whole #app content.\nconst _islandDisposes = new Set<() => void>();\nconst _islandSchedules = new Set<() => void>();\n\n// Finds [data-nix-js-island] markers in the current document and mounts the\n// corresponding interactive components over them. This runs in the browser.\n\nexport type IslandComponent<TProps = unknown> = (props: TProps) => NixTemplate | null | false | undefined;\n\n// Re-exported from island.ts so the client entry and the server helper share a\n// single source of truth for the directive union (now includes \"only\").\nexport type { IslandDirective } from \"./island.js\";\n\n/** Lazy island loader in a discriminated form (no probe required to detect). */\nexport interface IslandLoader<TProps = unknown> {\n  load: () => Promise<IslandComponent<TProps>>;\n}\n\n/**\n * Island registry entry. Two unambiguous shapes:\n *   - `IslandComponent` (eager component function).\n *   - `IslandLoader` `{ load }` (lazy, code-split loader).\n *\n * Legacy async loader functions are also accepted for backwards compatibility\n * and detected *without invoking them* (via the `AsyncFunction` tag), so no\n * side effects or duplicate signal creation happen during detection.\n */\nexport type IslandRegistryEntry<TProps = unknown> =\n  | IslandComponent<TProps>\n  | IslandLoader<TProps>\n  | (() => Promise<IslandComponent<TProps>>);\n\nexport type IslandRegistry = Record<string, IslandRegistryEntry<any>>;\n\n/**\n * Wraps a lazy island loader in the discriminated `{ load }` form.\n */\nexport function lazyIsland<TProps = unknown>(\n  loader: () => Promise<IslandComponent<TProps>>,\n): IslandLoader<TProps> {\n  return { load: loader };\n}\n\nfunction isIslandLoader(value: unknown): value is IslandLoader {\n  return (\n    typeof value === \"object\" &&\n    value !== null &&\n    typeof (value as { load?: unknown }).load === \"function\"\n  );\n}\n\nfunction isAsyncFunction(value: unknown): boolean {\n  if (typeof value !== \"function\") return false;\n  const ctor = (value as { constructor?: { name?: string } }).constructor;\n  return ctor?.name === \"AsyncFunction\" || (value as { [Symbol.toStringTag]?: string })[Symbol.toStringTag] === \"AsyncFunction\";\n}\n\ninterface IslandMarker {\n  el: HTMLElement;\n  name: string;\n  directive: IslandDirective;\n  props: unknown;\n  propsError?: unknown;\n}\n\nfunction collectMarkers(): IslandMarker[] {\n  const elements = Array.from(\n    document.querySelectorAll<HTMLElement>(\"[data-nix-js-island]\"),\n  );\n  return elements.map((el) => {\n    const marker: IslandMarker = {\n      el,\n      name: el.dataset.nixJsIsland ?? \"\",\n      directive: (el.dataset.directive as IslandDirective) ?? \"load\",\n      props: null,\n    };\n    if (el.dataset.props) {\n      try {\n        marker.props = JSON.parse(el.dataset.props);\n      } catch (error) {\n        marker.propsError = error;\n      }\n    }\n    return marker;\n  });\n}\n\nasync function hydrate(marker: IslandMarker, registry: IslandRegistry): Promise<void> {\n  try {\n    if (marker.propsError) {\n      reportIslandError(marker, marker.propsError);\n      return;\n    }\n\n    const entry = registry[marker.name];\n    if (!entry) {\n      console.warn(`[nix-js-kit] No island registered for \"${marker.name}\"`);\n      return;\n    }\n\n    // Resolve the component without probing: a `{ load }` loader or an async\n    // function is lazy (awaited for the module/component); anything else is an\n    // eager component called directly with the island props. Detection never\n    // invokes the component with undefined props, so no side effects or\n    // duplicate signal creation occur during resolution. Eager components\n    // hydrate synchronously; lazy loaders hydrate once the module resolves.\n    let Component: IslandComponent | undefined;\n    if (isIslandLoader(entry)) {\n      const mod = (await (entry as IslandLoader).load()) as IslandComponent | { default?: unknown };\n      Component = typeof mod === \"function\" ? (mod as IslandComponent) : (mod as { default?: unknown }).default as IslandComponent | undefined;\n    } else if (typeof entry === \"function\") {\n      if (isAsyncFunction(entry)) {\n        const mod = (await (entry as () => Promise<unknown>)()) as IslandComponent | { default?: unknown };\n        Component = typeof mod === \"function\" ? (mod as IslandComponent) : (mod as { default?: unknown }).default as IslandComponent | undefined;\n      } else {\n        Component = entry as IslandComponent;\n      }\n    }\n\n    if (typeof Component !== \"function\") {\n      console.warn(`[nix-js-kit] Island \"${marker.name}\" did not resolve to a component function`);\n      return;\n    }\n\n    const template = Component(marker.props);\n    if (template === null || template === false || template === undefined) return;\n    const prevDispose = (marker.el as any).__nix_js_island_dispose;\n    if (typeof prevDispose === \"function\") prevDispose();\n\n    // Islands with directive \"only\" or options.ssr:false have no SSR-rendered\n    // DOM inside the marker — only fallback HTML or nothing. hydrateTemplate\n    // assumes the SSR DOM is already present and walks it for hydration markers\n    // (<!--nix-N-->, data-nix-e-*). When there's nothing to walk, it silently\n    // does nothing (no contexts to match → empty loop → no mount). So we detect\n    // the absence of hydration markers and do a fresh _render mount instead.\n    const hasSSRMarkers = marker.el.innerHTML.includes(\"<!--nix-\");\n    const handle = hasSSRMarkers\n      ? hydrateTemplate(template, marker.el, { mismatch: \"warn-remount\" })\n      : freshMount(template, marker.el);\n\n    const wrappedDispose = () => {\n      handle.unmount();\n      _islandDisposes.delete(wrappedDispose);\n      delete (marker.el as any).__nix_js_island_dispose;\n    };\n    (marker.el as any).__nix_js_island_dispose = wrappedDispose;\n    _islandDisposes.add(wrappedDispose);\n  } catch (error) {\n    reportIslandError(marker, error);\n  }\n}\n\nfunction reportIslandError(marker: IslandMarker, error: unknown): void {\n  console.error(`[nix-js-kit] Failed to hydrate island \"${marker.name}\":`, error);\n  const EventConstructor = marker.el.ownerDocument.defaultView?.CustomEvent;\n  if (EventConstructor) {\n    marker.el.dispatchEvent(new EventConstructor(\"nix-js:island-error\", {\n      bubbles: true,\n      detail: { name: marker.name, error },\n    }));\n  }\n}\n\n/**\n * Mounts a template fresh into a container (no hydration).\n * Used for islands with no SSR DOM (directive \"only\" or ssr:false) where\n * hydrateTemplate can't work — there's nothing to hydrate against.\n */\nfunction freshMount(template: NixTemplate, container: Element): { unmount: () => void } {\n  container.replaceChildren();\n  const dispose = template._render(container, null);\n  return { unmount: dispose };\n}\n\n/**\n * Hydrates all islands on the page using the provided registry.\n *\n * @param registry Map from island name to component factory.\n */\n/**\n * Dispose all currently hydrated islands. Called by the client router before\n * swapping the page body to prevent leaked effects and stale DOM writes.\n */\nexport function cleanupHydratedIslands(): void {\n  for (const cancel of _islandSchedules) cancel();\n  _islandSchedules.clear();\n  for (const dispose of _islandDisposes) dispose();\n  _islandDisposes.clear();\n}\n\nexport function hydrateIslands(registry: IslandRegistry): void {\n  if (typeof window === \"undefined\") return;\n\n  const markers = collectMarkers();\n\n  for (const marker of markers) {\n    if (marker.directive === \"load\" || marker.directive === \"only\") {\n      // \"only\" is client-only (no SSR) but hydrates immediately on the\n      // client, just like \"load\" — the difference is purely server-side.\n      void hydrate(marker, registry);\n      continue;\n    }\n\n    if (marker.directive === \"idle\") {\n      let cancel = () => { };\n      if (\"requestIdleCallback\" in window) {\n        const id = window.requestIdleCallback(() => {\n          _islandSchedules.delete(cancel);\n          void hydrate(marker, registry);\n        });\n        cancel = () => window.cancelIdleCallback(id);\n      } else {\n        const id = globalThis.setTimeout(() => {\n          _islandSchedules.delete(cancel);\n          void hydrate(marker, registry);\n        }, 0);\n        cancel = () => globalThis.clearTimeout(id);\n      }\n      _islandSchedules.add(cancel);\n      continue;\n    }\n\n    if (marker.directive === \"visible\") {\n      if (\"IntersectionObserver\" in window) {\n        let cancel = () => { };\n        const observer = new IntersectionObserver(\n          (entries) => {\n            for (const entry of entries) {\n              if (entry.isIntersecting) {\n                _islandSchedules.delete(cancel);\n                observer.disconnect();\n                void hydrate(marker, registry);\n              }\n            }\n          },\n          { rootMargin: \"0px\", threshold: 0 },\n        );\n        cancel = () => observer.disconnect();\n        _islandSchedules.add(cancel);\n        observer.observe(marker.el);\n      } else {\n        void hydrate(marker, registry);\n      }\n    }\n  }\n}\n"],"mappings":"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"}