{"version":3,"file":"each.cjs","names":[],"sources":["../../src/directives/each.ts"],"sourcesContent":["import {\n  batch,\n  computed,\n  createScope,\n  type Readable,\n  effect as rawEffect,\n  type Scope,\n  type Signal,\n  signal,\n  untrack,\n} from '@vielzeug/ripple';\n\nimport { invariant, ORE_ERRORS, OreApiError, OreLifecycleError, reportRuntimeError } from '../errors';\nimport { createDirectiveResult, type DirectiveResult, type HTMLResult } from '../template/result';\nimport { removeNodes, runAll } from '../utils/dom';\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\ntype MaybeReactiveArray<T> = Readable<T[]> | (() => T[]) | T[];\n\ntype ItemEntry<T> = {\n  cleanups: (() => void)[];\n  data: Signal<T>;\n  index: Signal<number>;\n  /** The key used to identify this entry. */\n  key: string;\n  nodes: Node[];\n  scope: Scope;\n};\n\n// ─── Item lifecycle ───────────────────────────────────────────────────────────\n\nconst createItem = <T>(\n  item: T,\n  index: number,\n  render: (item: Readable<T>, index: Readable<number>) => HTMLResult,\n  parent: ParentNode,\n  insertBefore: Node,\n): ItemEntry<T> => {\n  const dataSignal: Signal<T> = signal(item);\n  const indexSignal: Signal<number> = signal(index);\n  const scope = createScope();\n  const cleanups: (() => void)[] = [];\n  let nodes: Node[] = [];\n\n  scope.run(() => {\n    const result = render(dataSignal, indexSignal);\n\n    nodes = result.mount(parent, insertBefore, (fn) => cleanups.push(fn));\n  });\n\n  return { cleanups, data: dataSignal, index: indexSignal, key: '', nodes, scope };\n};\n\nconst removeItem = <T>(entry: ItemEntry<T>): void => {\n  entry.scope.dispose();\n  runAll(entry.cleanups);\n  removeNodes(entry.nodes);\n};\n\n// ─── Reconciler ───────────────────────────────────────────────────────────────\n\n/**\n * Reconciles the live item map (mutated in-place) against the next array.\n * Stale entries are removed and destroyed; new entries are created and inserted.\n * Existing entries are updated in-place via signal writes.\n * Returns the ordered list of entries matching nextList.\n */\nconst reconcileItems = <T>(\n  itemsMap: Map<string, ItemEntry<T>>,\n  next: T[],\n  keyFn: (item: T, index: number) => string | number,\n  render: (item: Readable<T>, index: Readable<number>) => HTMLResult,\n  parent: ParentNode,\n  endMarker: Node,\n): ItemEntry<T>[] => {\n  const nextKeys: string[] = [];\n  const nextKeySet = new Set<string>();\n\n  for (let i = 0; i < next.length; i++) {\n    const key = String(keyFn(next[i], i));\n\n    if (nextKeySet.has(key)) throw new OreApiError(ORE_ERRORS.eachDuplicateKey(key, i));\n\n    nextKeySet.add(key);\n    nextKeys.push(key);\n  }\n\n  // Remove stale entries from the map\n  for (const [key, entry] of itemsMap) {\n    if (!nextKeySet.has(key)) {\n      removeItem(entry);\n      itemsMap.delete(key);\n    }\n  }\n\n  // Build the next ordered list: update existing items, create new ones\n  const nextOrdered: ItemEntry<T>[] = [];\n\n  for (let i = 0; i < next.length; i++) {\n    const key = nextKeys[i];\n    const existing = itemsMap.get(key);\n\n    if (existing) {\n      batch(() => {\n        existing.data.value = next[i];\n        existing.index.value = i;\n      });\n      nextOrdered.push(existing);\n    } else {\n      const entry = untrack(() => createItem(next[i], i, render, parent, endMarker));\n\n      entry.key = key;\n      itemsMap.set(key, entry);\n      nextOrdered.push(entry);\n    }\n  }\n\n  // DOM ordering: right-to-left pass — move any item not already adjacent to cursor.\n  // O(n) DOM operations in the worst case; optimal for the typical small list sizes\n  // encountered in UI components (tabs, options, menu items).\n  let cursor: Node = endMarker;\n\n  for (let j = nextOrdered.length - 1; j >= 0; j--) {\n    const entry = nextOrdered[j];\n    const firstNode = entry.nodes[0];\n\n    if (firstNode && firstNode !== cursor.previousSibling) {\n      for (const node of entry.nodes) parent.insertBefore(node, cursor);\n    }\n\n    cursor = firstNode ?? cursor;\n  }\n\n  return nextOrdered;\n};\n\n// ─── Public API ───────────────────────────────────────────────────────────────\n\n/**\n * Renders a keyed list of items as a `DirectiveResult`.\n *\n * Each item is rendered by the provided render function.\n * Items are reused by key when the list changes; only stale items are destroyed.\n *\n * The render function receives a `Readable<T>` signal and a `Readable<number>` index\n * signal. Use `item.value` to read the current item inside reactive expressions:\n *\n * ```ts\n * html`${each(items, (item) => item.id, (item) => html`<li>${() => item.value.name}</li>`)}`\n * ```\n *\n * **Plain array:** when a plain `T[]` is passed (not a signal or getter), it is\n * treated as a one-time static render. Mutations to the original array are not\n * tracked. Pass a `Signal<T[]>` or `() => T[]` for reactive lists.\n *\n * **Optional fallback:** the fourth argument renders when the list is empty.\n *\n * **Key choice:** pass a stable item identifier (e.g. `item.id`), never the\n * array index — an index-based key reassigns to a different item whenever the\n * list is reordered or an item is inserted/removed before it, causing full\n * item teardown/recreation instead of the in-place update `each()` is built\n * for.\n *\n * **Duplicate keys:** a reconciliation failure (e.g. duplicate keys, see `eachDuplicateKey`)\n * does not throw past this function — an uncaught exception inside the reactive effect that\n * drives `each()` would risk corrupting unrelated effects scheduled in the same update batch.\n * Instead the list is cleared and the failure is reported via the `ore:error` DOM event (see\n * `OreLifecycleError`, phase `'each-reconcile'`) plus a dev-only console log — listen for\n * `ore:error` on `document`/`window` to observe this in every build, including production.\n */\nexport function each<T>(\n  list: MaybeReactiveArray<T>,\n  keyFn: (item: T, index: number) => string | number,\n  render: (item: Readable<T>, index: Readable<number>) => HTMLResult,\n  fallback?: () => HTMLResult,\n): DirectiveResult {\n  const listSignal = Array.isArray(list)\n    ? signal(list as T[])\n    : typeof list === 'function'\n      ? computed(list as () => T[])\n      : list;\n\n  return createDirectiveResult((anchor, registerCleanup) => {\n    const parent = anchor.parentNode;\n\n    invariant(parent, 'each() anchor comment has no parent node');\n\n    const endMarker = document.createComment('each/end');\n\n    parent.insertBefore(endMarker, anchor.nextSibling);\n\n    let itemsMap = new Map<string, ItemEntry<T>>();\n    let itemsOrdered: ItemEntry<T>[] = [];\n    let fallbackNodes: Node[] | null = null;\n    let fallbackCleanups: (() => void)[] = [];\n\n    const mountFallback = (): void => {\n      if (!fallback) return;\n\n      const result = fallback();\n\n      fallbackNodes = result.mount(parent, endMarker, (fn) => fallbackCleanups.push(fn));\n    };\n\n    const clearFallback = (): void => {\n      if (fallbackNodes) {\n        runAll(fallbackCleanups);\n        removeNodes(fallbackNodes);\n        fallbackNodes = null;\n        fallbackCleanups = [];\n      }\n    };\n\n    const sub = rawEffect(() => {\n      const nextList = listSignal.value ?? [];\n\n      if (nextList.length === 0) {\n        for (const entry of untrack(() => itemsOrdered)) removeItem(entry);\n        itemsMap = new Map();\n        itemsOrdered = [];\n\n        if (!fallbackNodes) untrack(mountFallback);\n\n        return;\n      }\n\n      clearFallback();\n\n      try {\n        itemsOrdered = untrack(() => reconcileItems(itemsMap, nextList, keyFn, render, parent, endMarker));\n      } catch (err) {\n        const cause = err instanceof Error ? err : new Error(String(err));\n\n        // Dispatched on the anchor comment (always a live DOM node) rather than the enclosing\n        // component's host element, which each() has no direct reference to — the event still\n        // bubbles/composes up to any ancestor listener, including a global one on document.\n        reportRuntimeError(\n          new OreLifecycleError(`each() failed to reconcile a list update: ${cause.message}`, {\n            cause,\n            component: 'each()',\n            phase: 'each-reconcile',\n          }),\n          anchor,\n        );\n\n        for (const entry of itemsMap.values()) removeItem(entry);\n        itemsMap = new Map();\n        itemsOrdered = [];\n      }\n    });\n\n    registerCleanup(() => sub.dispose());\n    registerCleanup(() => {\n      clearFallback();\n      for (const entry of itemsOrdered) removeItem(entry);\n      endMarker.remove();\n    });\n  });\n}\n"],"mappings":"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"}