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)