UNPKG

@zag-js/core

Version:

A minimal implementation of xstate fsm for UI machines

183 lines (182 loc) • 6.58 kB
// src/state.ts import { ensure, invariant } from "@zag-js/utils"; var STATE_DELIMITER = "."; var ABSOLUTE_PREFIX = "#"; var stateIndexCache = /* @__PURE__ */ new WeakMap(); var stateIdIndexCache = /* @__PURE__ */ new WeakMap(); function joinStatePath(parts) { return parts.join(STATE_DELIMITER); } function isAbsoluteStatePath(value) { return value.includes(STATE_DELIMITER); } function isExplicitAbsoluteStatePath(value) { return value.startsWith(ABSOLUTE_PREFIX); } function isChildTarget(value) { return value.startsWith(STATE_DELIMITER); } function stripAbsolutePrefix(value) { return isExplicitAbsoluteStatePath(value) ? value.slice(ABSOLUTE_PREFIX.length) : value; } function appendStatePath(base, segment) { return base ? `${base}${STATE_DELIMITER}${segment}` : segment; } function buildStateIndex(machine) { const index = /* @__PURE__ */ new Map(); const idIndex = /* @__PURE__ */ new Map(); const visit = (basePath, state) => { index.set(basePath, state); const stateId = state.id; if (stateId) { if (idIndex.has(stateId)) { invariant(`[zag-js] Duplicate state id: "${stateId}"`); } idIndex.set(stateId, basePath); } const childStates = state.states; if (!childStates) return; ensure(state.initial, () => `[zag-js] Compound state "${basePath}" has child states but no "initial" property`); if (!(state.initial in childStates)) { invariant( `[zag-js] Compound state "${basePath}" has initial "${String(state.initial)}" which is not a child state` ); } for (const [childKey, childState] of Object.entries(childStates)) { if (!childState) continue; const childPath = appendStatePath(basePath, childKey); visit(childPath, childState); } }; for (const [topKey, topState] of Object.entries(machine.states)) { if (!topState) continue; visit(topKey, topState); } return { index, idIndex }; } function ensureStateIndex(machine) { const cached = stateIndexCache.get(machine); if (cached) return cached; const { index, idIndex } = buildStateIndex(machine); stateIndexCache.set(machine, index); stateIdIndexCache.set(machine, idIndex); return index; } function getStatePathById(machine, stateId) { ensureStateIndex(machine); return stateIdIndexCache.get(machine)?.get(stateId); } function toSegments(value) { if (!value) return []; return String(value).split(STATE_DELIMITER).filter(Boolean); } function getStateChain(machine, state) { if (!state) return []; const stateIndex = ensureStateIndex(machine); const segments = toSegments(state); const chain = []; const statePath = []; for (const segment of segments) { statePath.push(segment); const path = joinStatePath(statePath); const current = stateIndex.get(path); if (!current) break; chain.push({ path, state: current }); } return chain; } function resolveAbsoluteStateValue(machine, value) { const stateIndex = ensureStateIndex(machine); const segments = toSegments(value); if (!segments.length) return value; const resolved = []; for (const segment of segments) { resolved.push(segment); const path = joinStatePath(resolved); if (!stateIndex.has(path)) return value; } let resolvedPath = joinStatePath(resolved); let current = stateIndex.get(resolvedPath); while (current?.initial) { const nextPath = `${resolvedPath}${STATE_DELIMITER}${current.initial}`; const nextState = stateIndex.get(nextPath); if (!nextState) break; resolvedPath = nextPath; current = nextState; } return resolvedPath; } function hasStatePath(machine, value) { const stateIndex = ensureStateIndex(machine); return stateIndex.has(value); } function resolveStateValue(machine, value, source) { const stateValue = String(value); if (isExplicitAbsoluteStatePath(stateValue)) { const stateId = stripAbsolutePrefix(stateValue); const statePath = getStatePathById(machine, stateId); ensure(statePath, () => `[zag-js] Unknown state id: "${stateId}"`); return resolveAbsoluteStateValue(machine, statePath); } if (isChildTarget(stateValue) && source) { const childPath = appendStatePath(source, stateValue.slice(1)); return resolveAbsoluteStateValue(machine, childPath); } if (!isAbsoluteStatePath(stateValue) && source) { const sourceSegments = toSegments(source); for (let index = sourceSegments.length - 1; index >= 1; index--) { const base = sourceSegments.slice(0, index).join(STATE_DELIMITER); const candidate = appendStatePath(base, stateValue); if (hasStatePath(machine, candidate)) return resolveAbsoluteStateValue(machine, candidate); } if (hasStatePath(machine, stateValue)) return resolveAbsoluteStateValue(machine, stateValue); } return resolveAbsoluteStateValue(machine, stateValue); } function getStateDefinition(machine, state) { const chain = getStateChain(machine, state); return chain[chain.length - 1]?.state; } function findTransition(machine, state, eventType) { const chain = getStateChain(machine, state); for (let index = chain.length - 1; index >= 0; index--) { const transitionMap = chain[index]?.state.on; const transition = transitionMap?.[eventType]; if (transition) return { transitions: transition, source: chain[index]?.path }; } const rootTransitionMap = machine.on; return { transitions: rootTransitionMap?.[eventType], source: void 0 }; } function getExitEnterStates(machine, prevState, nextState, reenter) { const prevChain = prevState ? getStateChain(machine, prevState) : []; const nextChain = getStateChain(machine, nextState); let commonIndex = 0; while (commonIndex < prevChain.length && commonIndex < nextChain.length && prevChain[commonIndex]?.path === nextChain[commonIndex]?.path) { commonIndex += 1; } let exiting = prevChain.slice(commonIndex).reverse(); let entering = nextChain.slice(commonIndex); const sameLeaf = prevChain.at(-1)?.path === nextChain.at(-1)?.path; if (reenter && sameLeaf) { exiting = prevChain.slice().reverse(); entering = nextChain; } return { exiting, entering }; } function matchesState(current, value) { if (!current) return false; return current === value || current.startsWith(`${value}${STATE_DELIMITER}`); } function hasTag(machine, state, tag) { return getStateChain(machine, state).some((item) => item.state.tags?.includes(tag)); } export { ensureStateIndex, findTransition, getExitEnterStates, getStateChain, getStateDefinition, hasTag, matchesState, resolveStateValue };