PR: new state management setup

Public
by a
Created December 06, 2025
Updated 9 days ago

PR: new state management setup

FlowStateManager

We now have some indirection between the db and manipulating the state within it. Rather than writing changes directly to the WASM instance of Sqlite3, we first write it to a serializable, in-memory representation.

A potential plan for this, other than making the state flows a little more intuitive, is to enable more convenient iteration on the underlying persistence technology, now we can more easily translate the in memory representation to any new schema or DB technology choice.

This should now also offer a natural point to issue automated syncing requests to the backend whenever the user is logged in.

We now have some indirection between the db and manipulating the state within it. Rather than writing changes directly to the WASM instance of Sqlite3, we first write it to a serializable, in-memory representation.

A potential plan for this, other than making the state flows a little more intuitive, is to enable more convenient iteration on the underlying persistence technology, now we can more easily translate the in memory representation to any new schema or DB technology choice.

This should now also offer a natural point to issue automated syncing requests to the backend whenever the user is logged in.

View on GitHub

   1: import { FlowState, FlowListItem, MatchState } from './types';
   2: import { getDatabase, saveDatabase } from '../db-init';
   3: import { queryAsync, getAsync, runAsync } from '../db/dbAsync';
   4: 
=> 5: class FlowStateManager {
   6:     private activeFlowState: FlowState | null = null;
   7:     private flowsListCache: FlowListItem[] | null = null;
   8: 
   9:     // Read Operations
   10:     async loadFlowsList(filter?: { repoRoot?: string }): Promise<FlowListItem[]> {

Fetch and cache

Now we are fetching the minimal set of data needed to present the "active" state in the app - namely the list of "flow" titiles (for the sidepanel) an the active flows combined state to render the markdown preview.

Now we are fetching the minimal set of data needed to present the "active" state in the app - namely the list of "flow" titiles (for the sidepanel) an the active flows combined state to render the markdown preview.

View on GitHub

   5: class FlowStateManager {
   6:     private activeFlowState: FlowState | null = null;
   7:     private flowsListCache: FlowListItem[] | null = null;
   8: 
   9:     // Read Operations
=> 10:     async loadFlowsList(filter?: { repoRoot?: string }): Promise<FlowListItem[]> {
   11:         const db = getDatabase();
   12:         
   13:         const params: any[] = [];
   14:         let whereClause = 'WHERE f.archived = 0';
   15:         

Step 3

View on GitHub

   29:         `;
   30:         this.flowsListCache = await queryAsync(db, sql, params);
   31:         return this.flowsListCache;
   32:     }
   33: 
=> 34:     async loadFlowState(flowId: string): Promise<FlowState | null> {
   35:         const db = getDatabase();
   36:         
   37:         const flow = await getAsync(db, `
   38:             SELECT id, name, description, git_repo_root, git_commit_sha, git_branch,
   39:                    archived, created_at, updated_at

Easier non-destructive change management

Now that we have a simple in-memory representation of the current state, we can more easily manipulate it. Non-destructive changes can first be done in memory, and later written to the db.

In-memory Schema

View on GitHub

   246:     private transformToMatchState(rows: any[]): MatchState[] {
   247:         return rows.map(r => ({
   248:             flow_match_id: r.id,
   249:             match_id: r.matches_id,
   250:             order_index: r.order_index,
   251:             content_kind: r.content_kind,
   252:             match: r.file_path ? {
   253:                 line: r.line,
   254:                 file_path: r.file_path,
   255:                 repo_relative_file_path: r.repo_relative_file_path,
   256:                 file_name: r.file_name,
   257:                 line_no: r.line_no,
   258:                 grep_meta: r.grep_meta,
   259:                 git_repo_root: r.git_repo_root,
   260:                 git_commit_sha: r.git_commit_sha,
   261:                 git_branch: r.git_branch
   262:             } : undefined,
   263:             note: (r.note_name || r.note_text) ? {
   264:                 name: r.note_name,
   265:                 description: r.note_text
   266:             } : undefined,
   267:             step_content: (r.step_content_title || r.step_content_body || r.step_content_file_path) ? {
   268:                 title: r.step_content_title,
   269:                 body: r.step_content_body,
   270:                 file_path: r.step_content_file_path
   271:             } : undefined
   272:         }));
   273:     }

Step 6

View on GitHub

   85:             this.activeFlowState.flow.description = description;
   86:         }
   87:         this.activeFlowState.isDirty = true;
   88:     }
   89: 
=> 90:     updateMatchNote(flowMatchId: string, name: string, description: string): void {
   91:         if (!this.activeFlowState) {
   92:             return;
   93:         }
   94:         const match = this.activeFlowState.matches.find(m => m.flow_match_id === flowMatchId);
   95:         if (match) {

Step 7

View on GitHub

   111:             match.step_content.body = body;
   112:             this.activeFlowState.isDirty = true;
   113:         }
   114:     }
   115: 
=> 116:     reorderMatch(flowMatchId: string, newIndex: number): void {
   117:         if (!this.activeFlowState) {
   118:             return;
   119:         }
   120:         const matches = this.activeFlowState.matches;
   121:         const currentIndex = matches.findIndex(m => m.flow_match_id === flowMatchId);

Opt in persistence

Explicitly write out changes to the db when needed.

Explicitly write out changes to the db when needed.

View on GitHub

   193:     async persist(): Promise<void> {
   194:         if (!this.activeFlowState || !this.activeFlowState.isDirty) {
   195:             return;
   196:         }
   197:         
   198:         const db = getDatabase();
   199:         const { flow, matches } = this.activeFlowState;
   200:         
   201:         await runAsync(db, `

The only exception

FlowMatch changes are destructive, we need to handle them immediately so we can recompute the order of steps correctly.

FlowMatch changes are destructive, we need to handle them immediately so we can recompute the order of steps correctly.

View on GitHub

   145:             matches.forEach((m, i) => m.order_index = i);
   146:             this.activeFlowState.isDirty = true;
   147:         }
   148:     }
   149: 
=> 150:     async deleteFlowMatch(flowMatchId: string): Promise<void> {
   151:         console.log(`[FlowStateManager] Deleting flow match id: ${flowMatchId}`);
   152:         const db = getDatabase();
   153:         
   154:         // Get flows_id and order_index before deletion
   155:         const current = await getAsync(db,