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–WordPracticeModeenum valuenodes– sequential batches of words with their own step sequencessteps– 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 flowdeleteUserFlow(id)– remove a custom flowgetActiveCustomFlowId()/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.
Navigator-Driven Transitions
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:
enterLoop– triggers when group size reached; setscursor.loopand resets indexadvanceLoopSubStep– progresses through loop iterationsleaveLoop– 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 firstnavigate– conditional routing (complete session or advance step)collectWrongWords– gather statisticsgenerateReport– 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
shuffleOnEnterwhen configured - Updates
nodeWorkingWordsfor the new phase - Signals completion at final step of final node
handleListEnd coordinates three scenarios when a word list exhausts:
- Enter
wordLoopsub-step if configured - Continue
wrongWordClearphase - Execute
onEndqueue 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.tscombining nodes, steps, templates, and advance rules. -
Runtime validation in
practice-flow-runtime.tsensures 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →