State.res

  1// client/src/State.res
  2// Module-level state that persists across component re-renders
  3// Waypoints are keyed by document ID since multiple docs may be open
  4
  5// Waypoint type - re-export from Waypoint module for convenience
  6type waypoint = Waypoint.attributes
  7
  8// Map of document ID -> waypoints signal
  9// Each document has its own signal for its waypoints array
 10let waypoints: Map.t<string, Preact.Signal.t<array<waypoint>>> = Map.make()
 11
 12// Map of document ID -> Yjs document
 13// Documents persist across component re-renders
 14let docs: Map.t<string, Y.Doc.t> = Map.make()
 15
 16// View state
 17let center: Preact.Signal.t<(float, float)> = Preact.Signal.make((0., 0.))
 18let zoom: Preact.Signal.t<float> = Preact.Signal.make(0.)
 19
 20// Panel content
 21let content: Preact.Signal.t<option<Jsx.element>> = Preact.Signal.make(None)
 22
 23// Initdata readonly document parsed from <script id="initdata">
 24let readonly: Preact.Signal.t<bool> = Preact.Signal.make(false)
 25
 26// Logged in user
 27type user = {
 28  id: string, //Base36 encoded, see server/pkg/id.go
 29  email: string,
 30  handle: string,
 31  registered: bool,
 32}
 33let user: Preact.Signal.t<option<user>> = Preact.Signal.make(None)
 34
 35// Helper to get or create a waypoints signal for a document
 36let getOrCreateWaypointsSignal = (docID: string): Preact.Signal.t<array<waypoint>> => {
 37  switch waypoints->Map.get(docID) {
 38  | Some(signal) => signal
 39  | None => {
 40      let signal = Preact.Signal.make([])
 41      waypoints->Map.set(docID, signal)
 42      signal
 43    }
 44  }
 45}
 46
 47// Editor cache module - stores TipTap editor instances by document ID
 48module Editor = {
 49  // Cache for instantiated editors, recalled from cache if component is unmounted/remounted
 50  let cache: Map.t<string, Tiptap.t> = Map.make()
 51
 52  // Get or create editor for a document
 53  // Returns None if no editor exists (Editor.res creates it)
 54  let get = (docID: string): option<Tiptap.t> => {
 55    cache->Map.get(docID)
 56  }
 57
 58  // Store editor instance in cache
 59  let set = (docID: string, editor: Tiptap.t): unit => {
 60    cache->Map.set(docID, editor)
 61  }
 62}
 63
 64// Debounced waypoint update handling
 65module WaypointUpdater = {
 66  // Debounce delay in milliseconds
 67  let debounceMs = 1000
 68
 69  // Store timeout handles per document ID for debouncing
 70  let timeoutHandles: Map.t<string, Debounce.t> = Map.make()
 71  // Store pending editor references per document ID
 72  let pendingEditors: Map.t<string, Tiptap.t> = Map.make()
 73
 74  // Trigger an update for a document (called from editor onUpdate)
 75  // This is debounced - multiple rapid calls will only result in one extraction
 76  let trigger = (docID: string, editor: Tiptap.t): unit => {
 77    // Cancel any pending update for this document
 78    switch timeoutHandles->Map.get(docID) {
 79    | Some(handle) => {
 80        Debounce.cancel(handle)
 81        let _ = timeoutHandles->Map.delete(docID)
 82      }
 83    | None => ()
 84    }
 85
 86    // Store the editor reference for when the debounce fires
 87    pendingEditors->Map.set(docID, editor)
 88
 89    // Schedule new update
 90    let waypointsSignal = getOrCreateWaypointsSignal(docID)
 91
 92    let handle = Debounce.make(() => {
 93      // Clear the handle since we're executing
 94      let _ = timeoutHandles->Map.delete(docID)
 95
 96      // Get the editor and perform extraction
 97      switch pendingEditors->Map.get(docID) {
 98      | Some(ed) => {
 99          Log.debug("WaypointUpdater: extracting waypoints after debounce", ~kv={"doc": docID})
100
101          // Get JSON from editor and extract waypoints
102          let docJson = ed->Tiptap.getJSON
103          let waypoints = Waypoint.extractFromDocument(docJson)
104
105          // Update signal
106          waypointsSignal->Preact.Signal.set(waypoints)
107
108          Log.debug(
109            "WaypointUpdater: updated waypoints",
110            ~kv={
111              "doc": docID,
112              "count": Int.toString(waypoints->Array.length),
113            },
114          )
115        }
116      | None => ()
117      }
118
119      // Clean up pending editor reference
120      let _ = pendingEditors->Map.delete(docID)
121    }, debounceMs)
122
123    timeoutHandles->Map.set(docID, handle)
124  }
125}
126
127// Helper to get or create a Yjs document for a document ID
128// This sets up the observer once when the document is first created
129let getOrCreateYjsDoc = (docID: string): Y.Doc.t => {
130  switch docs->Map.get(docID) {
131  | Some(ydoc) => ydoc
132  | None => {
133      let ydoc = Y.Doc.make()
134      docs->Map.set(docID, ydoc)
135
136      Log.debug("State: Created new Yjs doc", ~kv={"doc": docID})
137
138      ydoc
139    }
140  }
141}
142
143type ysweetStatus = Offline | Connecting | Synced | Saving
144let ysweetStatus: Preact.Signal.t<ysweetStatus> = Preact.Signal.make(Offline)