How to Manage Practice Flow Configurations and Transitions in TypeWords

TypeWords uses a declarative flow system where practice sessions are defined in practice-flow-config.ts, executed by practice-flow-runtime.ts, and driven through state transitions by usePracticeWordNavigator.ts.

Managing practice flow configurations in TypeWords requires understanding three core layers: static flow definitions, runtime resolution, and navigator-driven transitions. This article explains how the open-source typing application structures practice sessions, persists custom configurations, and handles complex transitions including word loops, wrong-word clearing, and step advancement.

Understanding Flow Structure and Configuration

The PracticeFlowConfig schema

Every practice session in TypeWords follows a PracticeFlowConfig defined in [practice-flow-config.ts](https://github.com/zyronon/TypeWords/blob/master/app/core/composables/practice-words/practice-flow-config.ts). This file contains both the type definitions and built-in flow implementations.

A flow consists of:

  • id – unique identifier (e.g., 'system', 'free')
  • mode – WordPracticeMode enum value
  • nodes – sequential batches of words with their own step sequences
  • steps – individual practice activities referencing step templates

Built-in system flow example

The default system flow demonstrates the full structure:

system: {
  id: 'system',
  version: CURRENT_FLOW_VERSION,
  mode: WordPracticeMode.System,
  label: 'smart_learning',
  nodes: [
    {
      id: 'new',
      label: 'new_words',
      source: 'taskNew',
      steps: [
        { templateId: 'followWrite', wordAdvance: WORD_LOOP_WITH_SPELL, onEnd: DEFAULT_ON_END },
        { templateId: 'listen', shuffleOnEnter: true, onEnd: DEFAULT_ON_END },
        { templateId: 'dictation', shuffleOnEnter: true, onEnd: DEFAULT_ON_END },
      ],
    },
    // additional nodes...
  ],
},

Step templates and advance rules

Step templates (followWrite, spell, listen, dictation, identify) define UI behavior. Advance rules control progression:

  • { type: 'increment' } – move to next word linearly
  • { type: 'wordLoop', loopSize: number } – repeat words in groups before advancing

The materializeWordAdvance helper converts configurations into concrete WordAdvanceRule objects.

Runtime Flow Management

Creating and validating flow runtimes

[practice-flow-runtime.ts](https://github.com/zyronon/TypeWords/blob/master/app/core/composables/practice-words/practice-flow-runtime.ts) exposes createPracticeFlowRuntime to instantiate isolated runtimes. Key functions include:

Function Purpose
isValidFlowConfig Schema validation for nodes, steps, sources, and template IDs
resolveFlowConfigOrSystem Fallback to system flow when validation fails
loadPracticeFlow Resolution by flowId, custom flow object, or direct config
resolveFlowStart Session initialization with word selection and cursor creation
resolvePhaseByCursor Phase definition lookup including inWrongWordClear and loop states

Custom flow persistence

Custom flows integrate with localStorage via getFlowStorage and setFlowStorage. The storage key is PracticeFlowV2. Methods include:

  • saveUserFlow(id, config, label) – persist a custom flow
  • deleteUserFlow(id) – remove a custom flow
  • getActiveCustomFlowId() / setActiveCustomFlowId(id) – manage active selection

Cursor Model and State Tracking

The PracticeFlowCursor interface in [practice-flow-types.ts](https://github.com/zyronon/TypeWords/blob/master/app/core/composables/practice-words/practice-flow-types.ts) uniquely identifies position within a flow:

export interface PracticeFlowCursor {
  nodeIndex: number;        // Current node in flow.nodes
  stepIndex: number;        // Current step in node.steps
  inWrongWordClear: boolean; // Wrong-word clearing active flag
  loop: null | {            // Word loop sub-step state
    startIndex: number;
    endIndex: number;
    subStepIndex: number;
  };
  endActionIndex: number | null; // Position in onEnd action queue
}

This cursor enables deterministic state restoration and transition prediction.

Core navigation architecture

[usePracticeWordNavigator.ts](https://github.com/zyronon/TypeWords/blob/master/app/core/composables/practice-words/usePracticeWordNavigator.ts) composes runtime logic with UI state. It receives dependencies including getPracticeData, getTaskWords, and completion callbacks.

Word source resolution

The resolveWordsFromSource function maps node source values to word lists:

  • 'taskNew' – new words from task
  • 'taskReview' – review words from task
  • 'current' – user-selected current list
  • 'wrongWords' – accumulated wrong words

Loop handling mechanics

Word loops create sub-steps within a main step:

  1. enterLoop – triggers when group size reached; sets cursor.loop and resets index
  2. advanceLoopSubStep – progresses through loop iterations
  3. leaveLoop – terminates loop, returns to main step progression
// Simplified loop entry logic from navigator
function enterLoop(start: number, end: number) {
  cursor.value = {
    ...cursor.value,
    loop: { startIndex: start, endIndex: end, subStepIndex: 0 },
    index: start, // Reset to loop start
  };
}

onEnd action queue processing

Each step's onEnd array defines post-completion actions:

  • wrongWordClear – suspend progress, clear wrong words first
  • navigate – conditional routing (complete session or advance step)
  • collectWrongWords – gather statistics
  • generateReport – create session summary

The processNextEndAction method executes actions sequentially, handling suspension for wrongWordClear.

Step advancement and list ending

runStepAdvance handles transitions between steps and nodes:

  • Skips empty nodes automatically
  • Applies shuffleOnEnter when configured
  • Updates nodeWorkingWords for the new phase
  • Signals completion at final step of final node

handleListEnd coordinates three scenarios when a word list exhausts:

  1. Enter wordLoop sub-step if configured
  2. Continue wrongWordClear phase
  3. Execute onEnd queue and advance

Practical Implementation Examples

Starting a practice session

import { usePracticeWordNavigator } from '@/app/core/composables/practice-words/usePracticeWordNavigator';
import { WordPracticeMode } from '@/core/types/enum';

const deps = {
  getPracticeData: () => practiceData,
  getTaskWords: () => taskWords,
  getCurrentWord: () => practiceData.words[practiceData.index],
  checkWordIsNeedNext: (w) => !w.mastered,
  complete: () => console.log('Session complete'),
  notify: (lvl, msg) => console.info(`[${lvl}] ${msg}`),
};

const nav = usePracticeWordNavigator(deps);

// Initialize from system flow
const start = nav.resolveFlowStart(WordPracticeMode.System, taskWords);
nav.initializeNodeWords(start.words);
// start.cursor contains initial position

Creating and saving a custom flow

import { CURRENT_FLOW_VERSION } from '@/app/core/composables/practice-words/practice-flow-config';
import { saveUserFlow } from '@/app/core/composables/practice-words/practice-flow-runtime';
import { WordPracticeMode } from '@/core/types/enum';

const customFlow = {
  id: 'minimalDrill',
  version: CURRENT_FLOW_VERSION,
  mode: WordPracticeMode.Custom,
  label: 'Minimal Drill',
  nodes: [
    {
      id: 'drill',
      label: 'drill_node',
      source: 'current',
      steps: [
        { 
          templateId: 'followWrite', 
          wordAdvance: { type: 'wordLoop', loopSize: 3 },
          onEnd: ['collectWrongWords', 'navigate'],
        },
      ],
    },
  ],
};

saveUserFlow(customFlow.id, customFlow, 'Minimal Drill');

Session snapshot restoration

// Before app backgrounding or navigation
const snapshot = nav.buildSessionSnapshot();
// Persist to IndexedDB or localStorage

// Later restoration
const restored = nav.restoreSessionSnapshot(storedSnapshot);
if (!restored) {
  // Fallback handled automatically: system flow with reset cursor
  console.warn('Corrupted snapshot, using default flow');
}

Transition Mechanics Summary

Trigger Method Outcome
User completes word next() → runWordAdvance() Index increments; may trigger loop entry
Loop group filled enterLoop() Cursor enters sub-step mode
Loop iterations complete leaveLoop() Returns to main step progression
Main list exhausted handleListEnd() Evaluates onEnd queue or advances step
Wrong-word clearing runWrongWordRetry() Shuffles wrong words, sets clearing flag
Navigate action processNextEndAction() Completes session or advances via runStepAdvance()
Manual restore restoreSessionSnapshot() Reloads full session state with validation

Summary

  • Flow configurations are declarative objects in practice-flow-config.ts combining nodes, steps, templates, and advance rules.

  • Runtime validation in practice-flow-runtime.ts ensures integrity and provides fallback to system defaults.

  • Custom flows persist to localStorage with full CRUD operations and active-flow tracking.

  • The cursor model captures complete session state including nested loop positions and action queue progress.

  • The navigator orchestrates all transitions: word advancement, loop handling, wrong-word clearing, and step progression.

  • Session snapshots enable seamless resumption across application lifecycles.

Frequently Asked Questions

How do I add a completely new practice mode to TypeWords?

Define a new PracticeFlowConfig with a unique id, then register it via saveUserFlow(). The mode must use WordPracticeMode.Custom. Ensure your step templateId values correspond to existing templates in STEP_TEMPLATE_META, or extend the template system in practice-flow-config.ts.

What happens if my custom flow configuration is invalid?

resolveFlowConfigOrSystem() automatically falls back to the built-in system flow. Validation in isValidFlowConfig checks node structure, step references, template IDs, and source values. Errors are logged via the navigator's notify dependency.

Can I resume a practice session after closing the browser?

Yes. Call buildSessionSnapshot() before shutdown to capture cursor position, working words, and flow state. Store this object persistently, then use restoreSessionSnapshot() on return. If restoration fails, the navigator automatically initializes a fresh session with the system flow.

How does word loop sub-step counting work?

When wordAdvance.type === 'wordLoop', the navigator tracks group boundaries. Upon reaching loopSize words, enterLoop() sets cursor.loop with startIndex, endIndex, and subStepIndex: 0. The index resets to startIndex, and advanceLoopSubStep() increments subStepIndex each iteration until the loop completes.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →