@@ -6,74 +6,99 @@ let flag_dirty = 1
66let flag_pending = 2
77let flag_running = 4
88
9- // Observer kind tag
9+ // Global tracking version
10+ let trackingVersion : ref <int > = ref (0 )
11+ // Global mutation version (increments on real signal writes)
12+ let globalVersion : ref <int > = ref (0 )
13+
1014type kind = [#Effect | #Computed ]
1115
12- // Forward declare mutually recursive types
13- type rec link = {
14- // Direct reference to signal's subscriber list (type-erased)
15- mutable subs : subs ,
16- // Direct reference to observer
17- mutable observer : observer ,
18- // Links in the observer's dependency chain
19- mutable nextDep : option <link >,
20- mutable prevDep : option <link >,
21- // Links in the signal's subscriber chain
22- mutable nextSub : option <link >,
23- mutable prevSub : option <link >,
24- }
16+ module rec Link : {
17+ type t = {
18+ // Direct reference to signal's subscriber list (type-erased)
19+ mutable subs : Subs .t ,
20+ // Direct reference to observer
21+ mutable observer : Observer .t ,
22+ // Links in the observer's dependency chain
23+ mutable nextDep : option <Link .t >,
24+ mutable prevDep : option <Link .t >,
25+ // Links in the signal's subscriber chain
26+ mutable nextSub : option <Link .t >,
27+ mutable prevSub : option <Link .t >,
28+ // Version stamp for duplicate detection within a compute cycle
29+ mutable lastTrackedVersion : int ,
30+ }
31+ } = Link
2532
2633// Signal subscriber list (head/tail of linked list)
2734// For computeds, this same object also serves as the observer (combined structure)
28- and subs = {
29- mutable first : option <link >,
30- mutable last : option <link >,
31- mutable version : int ,
32- // === Observer fields (only used for computeds) ===
33- // If compute is Some, this subs is a computed signal
34- mutable compute : option <unit => unit >,
35- mutable firstDep : option <link >,
36- mutable lastDep : option <link >,
37- mutable flags : int ,
38- mutable level : int ,
39- }
35+ and Subs : {
36+ type t = {
37+ mutable first : option <Link .t >,
38+ mutable last : option <Link .t >,
39+ mutable computedSubscriberCount : int ,
40+ mutable version : int ,
41+ // === Observer fields (only used for computeds) ===
42+ // If compute is Some, this subs is a computed signal
43+ mutable compute : option <unit => unit >,
44+ mutable firstDep : option <Link .t >,
45+ mutable lastDep : option <Link .t >,
46+ mutable flags : int ,
47+ mutable level : int ,
48+ mutable deferEffectsUntilRecompute : bool ,
49+ mutable lastGlobalVersion : int ,
50+ }
51+ } = Subs
4052
4153// Observer for effects only (computeds use subs directly)
42- and observer = {
43- id : int ,
44- kind : kind ,
45- run : unit => unit ,
46- mutable firstDep : option <link >,
47- mutable lastDep : option <link >,
48- mutable flags : int ,
49- mutable level : int ,
50- name : option <string >,
51- // For computed observers: direct reference to backing subs (the combined object)
52- mutable backingSubs : option <subs >,
53- }
54+ and Observer : {
55+ type t = {
56+ id : int ,
57+ kind : kind ,
58+ run : unit => unit ,
59+ mutable firstDep : option <Link .t >,
60+ mutable lastDep : option <Link .t >,
61+ mutable flags : int ,
62+ mutable level : int ,
63+ name : option <string >,
64+ // For computed observers: direct reference to backing subs (the combined object)
65+ mutable backingSubs : option <Subs .t >,
66+ }
67+ } = Observer
68+
69+ // Type aliases for convenience
70+ type link = Link .t
71+ type subs = Subs .t
72+ type observer = Observer .t
5473
5574// Create empty subscriber list (for plain signals)
5675let makeSubs = (): subs => {
5776 first : None ,
5877 last : None ,
78+ computedSubscriberCount : 0 ,
5979 version : 0 ,
6080 compute : None ,
6181 firstDep : None ,
6282 lastDep : None ,
6383 flags : 0 ,
6484 level : 0 ,
85+ deferEffectsUntilRecompute : false ,
86+ lastGlobalVersion : 0 ,
6587}
6688
6789// Create subs for a computed (with compute function)
68- let makeComputedSubs = (compute : unit => unit ): subs => {
90+ let makeComputedSubs = (compute : unit => unit , ~ deferEffectsUntilRecompute : bool = false ): subs => {
6991 first : None ,
7092 last : None ,
93+ computedSubscriberCount : 0 ,
7194 version : 0 ,
7295 compute : Some (compute ),
7396 firstDep : None ,
7497 lastDep : None ,
7598 flags : flag_dirty , // start dirty
7699 level : 0 ,
100+ deferEffectsUntilRecompute ,
101+ lastGlobalVersion : 0 ,
77102}
78103
79104// Create observer
@@ -95,7 +120,7 @@ let makeObserver = (
95120 backingSubs ,
96121}
97122
98- // Flag operations for observer (using Int.Bitwise module)
123+ // Flag operations for observer
99124let isDirty = (o : observer ): bool => Int .bitwiseAnd (o .flags , flag_dirty ) !== 0
100125let setDirty = (o : observer ): unit => o .flags = Int .bitwiseOr (o .flags , flag_dirty )
101126let clearDirty = (o : observer ): unit =>
@@ -105,7 +130,7 @@ let setPending = (o: observer): unit => o.flags = Int.bitwiseOr(o.flags, flag_pe
105130let clearPending = (o : observer ): unit =>
106131 o .flags = Int .bitwiseAnd (o .flags , Int .bitwiseNot (flag_pending ))
107132
108- // Flag operations for subs (for computeds - subs IS the observer)
133+ // Flag operations for subs
109134let isSubsDirty = (s : subs ): bool => Int .bitwiseAnd (s .flags , flag_dirty ) !== 0
110135let setSubsDirty = (s : subs ): unit => s .flags = Int .bitwiseOr (s .flags , flag_dirty )
111136let clearSubsDirty = (s : subs ): unit =>
@@ -115,17 +140,20 @@ let setSubsPending = (s: subs): unit => s.flags = Int.bitwiseOr(s.flags, flag_pe
115140let clearSubsPending = (s : subs ): unit =>
116141 s .flags = Int .bitwiseAnd (s .flags , Int .bitwiseNot (flag_pending ))
117142
118- // Check if subs is a computed (has compute function)
143+ // Check if subs is a computed
119144let isComputed = (s : subs ): bool => s .compute !== None
120145
121146// Create a link node
122- let makeLink = (subs : subs , observer : observer ): link => {
123- subs ,
124- observer ,
125- nextDep : None ,
126- prevDep : None ,
127- nextSub : None ,
128- prevSub : None ,
147+ let makeLink = (sourceSubs : subs , linkedObserver : observer ): link => {
148+ {
149+ subs : sourceSubs ,
150+ observer : linkedObserver ,
151+ nextDep : None ,
152+ prevDep : None ,
153+ nextSub : None ,
154+ prevSub : None ,
155+ lastTrackedVersion : 0 ,
156+ }
129157}
130158
131159// Add link to signal's subscriber list
@@ -137,6 +165,11 @@ let linkToSubs = (subs: subs, link: link): unit => {
137165 | None => subs .first = Some (link )
138166 }
139167 subs .last = Some (link )
168+
169+ let linkedSubs = (Obj .magic (link .observer ): subs )
170+ if isComputed (linkedSubs ) {
171+ subs .computedSubscriberCount = subs .computedSubscriberCount + 1
172+ }
140173}
141174
142175// Add link to observer's dependency list
@@ -163,6 +196,11 @@ let unlinkFromSubs = (link: link): unit => {
163196 }
164197 link .prevSub = None
165198 link .nextSub = None
199+
200+ let linkedSubs = (Obj .magic (link .observer ): subs )
201+ if isComputed (linkedSubs ) && subs .computedSubscriberCount > 0 {
202+ subs .computedSubscriberCount = subs .computedSubscriberCount - 1
203+ }
166204}
167205
168206// Remove link from dependency list
@@ -179,6 +217,20 @@ let unlinkFromDeps = (observer: observer, link: link): unit => {
179217 link .nextDep = None
180218}
181219
220+ // Remove link from subs's dependency list (for computeds - subs IS the observer)
221+ let unlinkFromSubsDeps = (s : subs , link : link ): unit => {
222+ switch link .prevDep {
223+ | Some (prev ) => prev .nextDep = link .nextDep
224+ | None => s .firstDep = link .nextDep
225+ }
226+ switch link .nextDep {
227+ | Some (next ) => next .prevDep = link .prevDep
228+ | None => s .lastDep = link .prevDep
229+ }
230+ link .prevDep = None
231+ link .nextDep = None
232+ }
233+
182234// Clear all dependencies from observer (unlinks from all signals)
183235let clearDeps = (observer : observer ): unit => {
184236 let link = ref (observer .firstDep )
0 commit comments