Skip to content

Refactor: Unify local-practice and collaborative checklists under a single Y.Doc shape #481

@InfinityBowman

Description

@InfinityBowman

Summary

Local-practice checklists (offline mode for logged-out users) and collaborative checklists currently use two entirely different storage shapes and code paths. Unify them: local-practice becomes a Y.Doc persisted via y-dexie without a WebSocket sync provider. Delete ~850 lines of duplication.

Problem

Today there are two parallel universes for the "same" checklist data:

Local-practice (packages/web/src/stores/localChecklistsStore.ts, 159 lines)

  • Stores answers as a flat JSON data field in the Dexie localChecklists table
  • Zustand store orchestrates reads/writes with debounced saves
  • Explicit comment: "Dexie table type from untyped db.js doesn't match our interface" with .any casts
  • Not cleared on logout (line 109 comment: "user's local practice data not tied to authentication")

Collaborative (ChecklistYjsWrapper + handlers, ~700 lines)

  • Answers live in nested Y.Map: reviews[studyId].checklists[checklistId].answers.qN.{answers, critical, note}
  • Persisted via y-dexie, synced via WebSocket to ProjectSync DO
  • Serialized back to flat objects for components

Cost of the split:

  1. Adding a new field means updating: Zustand store schema, Dexie table type, Y.Map handler (createAnswersYMap, serializeAnswers, updateAnswer), and component props for both views.
  2. LocalChecklistView (702 lines alongside ChecklistYjsWrapper) implements its own debounced-save, shallow-merge, and setChecklist state.
  3. Components have to accept both shapes via externalChecklist || localChecklist patterns.

Proposed solution

Local-practice checklists become Y.Docs persisted via y-dexie under a stable ID (e.g., local-practice-{userId} or just local-practice). No WebSocket provider attached. Same handler code, same serialization, same components.

Resulting architecture

Surface Before After
Storage Flat JSON in localChecklists Dexie table Same Y.Doc shape as collab, y-dexie persistence
State mgmt localChecklistsStore (Zustand) Delete; use the same Y.Doc hooks as collab
View LocalChecklistView (separate) Delete; unified ChecklistView works for both modes
Merge logic handleUpdate + debouncedSave in view Delete; Y.Doc + y-dexie handles it
Type casts .any casts throughout store None — same typed handler path

Migration

  1. Add a "local project" Y.Doc concept (y-dexie persistence, no sync provider).
  2. On app load, if the localChecklists Dexie table has entries, convert each using existing handler.createAnswersYMap + write the flat data into the new Y.Doc. Gated by a one-shot flag.
  3. Switch LocalChecklistView callers to the unified view path, picking the local Y.Doc.
  4. Leave the old table in place for one release as rollback insurance.
  5. Next release: drop the localChecklists and localChecklistPdfs Dexie tables, delete localChecklistsStore.ts, delete LocalChecklistView.

Migration correctness

Write a round-trip test: take every data blob shape that has appeared in the wild, run it through the migration, verify handler.serializeAnswers(migrated) equals the original data modulo Y.Text vs. string for note fields.

Non-goals

  • Not moving answers out of Yjs (separate strategic question).
  • Not changing collaborative sync behavior.
  • Not touching mode-specific UX affordances (sharing, assignment) — those stay as component conditionals; the data shape unifies, the UX can still differ.

Relationship to other issues

  • Depends on the useChecklistAnswers subscription hook (so the unified view has a clean read path).
  • Independent of the typed-answer-schemas and typed-project-API issues.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions