Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/graph-traversal-dead-branches.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@systemfsoftware/xstate": none
---

The graph traversals in `@systemfsoftware/xstate/graph` drop code that changed no result: a queue compaction shortcut, a shortest-path weight improvement that breadth-first order never reaches, unreachable missing-entry errors, and a serializer fallback that the options spread always replaced. Every path, step and adjacency entry comes out the same as before.
80 changes: 20 additions & 60 deletions packages/xstate/src/graph/adjacency.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,41 +239,21 @@ const expand = <
enqueueTransitions(value, queue, queued, traversal)
}

const pastCompactThreshold = (head: number): boolean => head > 4096

const beyondHalf = (head: number, length: number): boolean => head * 2 > length

const shouldCompact = (head: number, length: number): boolean => pastCompactThreshold(head) && beyondHalf(head, length)

const compactFrontier = <TSnapshot, TEvent>(
queue: Array<QueueEntry<TSnapshot, TEvent>>,
head: number,
): number => {
if (!shouldCompact(head, queue.length)) {
return head
}
queue.splice(0, head + 1)
return -1
}

const advance = <
const expandLevel = <
TSnapshot extends AnySnapshot,
TEvent extends EventObject,
TInput,
TSystem extends AnyActorSystem,
>(
adj: AdjacencyMap<TSnapshot, TEvent>,
queue: Array<QueueEntry<TSnapshot, TEvent>>,
head: number,
level: ReadonlyArray<QueueEntry<TSnapshot, TEvent>>,
traversal: Traversal<TSnapshot, TEvent, TInput, TSystem>,
): number => {
const queued = queue[head]
if (queued === undefined) {
return head + 1
): Array<QueueEntry<TSnapshot, TEvent>> => {
const nextLevel: Array<QueueEntry<TSnapshot, TEvent>> = []
for (const queued of level) {
expand(adj, nextLevel, queued, traversal)
}
const nextHead = compactFrontier(queue, head)
expand(adj, queue, queued, traversal)
return nextHead + 1
return nextLevel
}

const drain = <
Expand All @@ -283,12 +263,12 @@ const drain = <
TSystem extends AnyActorSystem,
>(
adj: AdjacencyMap<TSnapshot, TEvent>,
queue: Array<QueueEntry<TSnapshot, TEvent>>,
start: QueueEntry<TSnapshot, TEvent>,
traversal: Traversal<TSnapshot, TEvent, TInput, TSystem>,
): void => {
let head = 0
while (head < queue.length) {
head = advance(adj, queue, head, traversal)
let level = [start]
while (level.length !== 0) {
level = expandLevel(adj, level, traversal)
}
}

Expand All @@ -312,10 +292,7 @@ function computeAdjacencyMap<
}
const fromState = config.fromState ?? initialSnapshotOf(logic, options.input, actorScope)
const adj: AdjacencyMap<TSnapshot, TEvent> = createNullDict<AdjacencyValue<TSnapshot, TEvent>>()
const queue: Array<QueueEntry<TSnapshot, TEvent>> = [
{ nextState: fromState, event: undefined, prevState: undefined },
]
drain(adj, queue, traversal)
drain(adj, { nextState: fromState, event: undefined, prevState: undefined }, traversal)
return adj
}

Expand Down Expand Up @@ -367,35 +344,18 @@ export function adjacencyMapToArray<TSnapshot, TEvent>(
event: TEvent
nextState: TSnapshot
}> {
return Object.keys(adjMap).flatMap((key) => adjacencyRow(adjMap, toSerializedSnapshot(key)))
}

const requiredValue = <TKey extends string, TValue>(
dict: Record<TKey, TValue>,
key: TKey,
): TValue => {
const value = dict[key]
if (value === undefined) {
throw new Error('Missing adjacency entry')
}
return value
return Object.values({ ...adjMap }).flatMap((adjValue) => adjacencyRow(adjValue))
}

const adjacencyRow = <TSnapshot, TEvent>(
adjMap: AdjacencyMap<TSnapshot, TEvent>,
key: SerializedSnapshot,
adjValue: AdjacencyValue<TSnapshot, TEvent>,
): Array<{
state: TSnapshot
event: TEvent
nextState: TSnapshot
}> => {
const adjValue = requiredValue(adjMap, key)
return Object.keys(adjValue.transitions).map((eventKey) => {
const transition = requiredValue(adjValue.transitions, toSerializedEvent(eventKey))
return {
state: adjValue.state,
event: transition.event,
nextState: transition.state,
}
})
}
}> =>
Object.values({ ...adjValue.transitions }).map((transition) => ({
state: adjValue.state,
event: transition.event,
nextState: transition.state,
}))
10 changes: 3 additions & 7 deletions packages/xstate/src/graph/alterPath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ const initStepOf = (state: AnySnapshot): ErasedStep => ({
event: { type: XSTATE_INIT },
})

const previousStepOf = (steps: ErasedSteps, index: number): ErasedStep | undefined =>
index === 0 ? undefined : steps[index - 1]
const previousStepOf = (steps: ErasedSteps, index: number): ErasedStep | undefined => steps[index - 1]

const eventForStep = (steps: ErasedSteps, index: number): EventObject => {
const previous = previousStepOf(steps, index)
Expand All @@ -30,18 +29,15 @@ const finalStepOf = (path: ErasedStatePath): ErasedStep | undefined => {
const appendedSteps = (path: ErasedStatePath): ErasedSteps => {
const steps = replaySteps(path)
const finalStep = finalStepOf(path)
if (finalStep !== undefined) {
steps.push(finalStep)
}
steps.push(finalStep === undefined ? initStepOf(path.state) : finalStep)
return steps
}

// TODO: rewrite parts of the algorithm leading to this to make this function obsolete
export function alterPath<T extends ErasedStatePath>(path: T): T
export function alterPath<T extends ErasedStatePath>(path: T): ErasedStatePath {
const steps = path.steps.length > 0 ? appendedSteps(path) : [initStepOf(path.state)]
return {
...path,
steps,
steps: appendedSteps(path),
}
}
21 changes: 1 addition & 20 deletions packages/xstate/src/graph/graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,19 +138,6 @@ function defaultSerializeState<TLogic extends AnyActorLogic>(
return JSON.stringify(state)
}

function firstDefined<T>(
candidates: readonly (T | undefined)[],
fallback: T,
): T {
return candidates.find((candidate) => candidate !== undefined) ?? fallback
}

function optionSerializeState<TLogic extends AnyActorLogic>(
options: OptionsOf<TLogic> | undefined,
): SerializeState<TLogic> | undefined {
return options?.serializeState
}

function optionFromState<TLogic extends AnyActorLogic>(
options: OptionsOf<TLogic> | undefined,
): SnapshotFrom<TLogic> | undefined {
Expand Down Expand Up @@ -278,13 +265,7 @@ export function resolveTraversalOptions<TLogic extends AnyActorLogic>(
},
): TraversalConfig<SnapshotFrom<TLogic>, EventFromLogic<TLogic>> {
const resolvedDefaultOptions = resolveDefaultOptions(logic, traversalOptions)
const serializeState: SerializeState<TLogic> = firstDefined(
[
optionSerializeState<TLogic>(traversalOptions),
optionSerializeState<TLogic>(resolvedDefaultOptions),
],
defaultSerializeState<TLogic>,
)
const serializeState: SerializeState<TLogic> = defaultSerializeState<TLogic>
const fromState = optionFromState<TLogic>(traversalOptions) ??
optionFromState<TLogic>(resolvedDefaultOptions)
const traversalConfig: TraversalConfig<
Expand Down
Loading
Loading