Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 75 additions & 5 deletions packages/rescript-signals/src/signals/Computed.res
Original file line number Diff line number Diff line change
@@ -1,18 +1,77 @@
let makeWithoutEquals = (
compute: unit => 'a,
~name: option<string>=?,
): Signal.t<'a> => {
let id = Id.make()
let equalsFn: ('a, 'a) => bool = (_a, _b) => false

// Create a mutable ref to hold the signal so the compute function can update it
// Using Obj.magic to avoid Option wrapper overhead
let signalRef: ref<Signal.t<'a>> = ref(Obj.magic())

let make = (compute: unit => 'a, ~name: option<string>=?): Signal.t<'a> => {
// Fast recompute path for default behavior (no custom equality checks)
let recompute = () => {
let currentSignal = signalRef.contents
let newValue = compute()
currentSignal.value = newValue
currentSignal.subs.version = currentSignal.subs.version + 1
}

// Create combined subs (this IS the observer for the computed)
let subs = Core.makeComputedSubs(recompute, ~deferEffectsUntilRecompute=false)

// Initial computation under tracking to establish dependencies
let prev = Scheduler.currentComputedSubs.contents
Scheduler.currentComputedSubs := Some(subs)
let initialValue = compute()
Scheduler.currentComputedSubs := prev

// Create the signal with the initial value
let signal: Signal.t<'a> = {
id,
value: initialValue,
equals: equalsFn,
name,
subs,
}

// Set the ref so recompute can access the signal
signalRef := signal
subs.lastGlobalVersion = Core.globalVersion.contents
Core.clearSubsDirty(subs)

signal
}

let makeWithEquals = (
compute: unit => 'a,
equalsFn: ('a, 'a) => bool,
~name: option<string>=?,
): Signal.t<'a> => {
let id = Id.make()

// Create a mutable ref to hold the signal so the compute function can update it
// Using Obj.magic to avoid Option wrapper overhead
let signalRef: ref<Signal.t<'a>> = ref(Obj.magic())

// Recompute function - updates the signal's value directly
// Recompute function - updates the signal's value and tracks if it changed
let recompute = () => {
signalRef.contents.value = compute()
let currentSignal = signalRef.contents
let previousValue = currentSignal.value
let newValue = compute()
let shouldUpdate = try {
!currentSignal.equals(previousValue, newValue)
} catch {
| _ => true
}
if shouldUpdate {
currentSignal.value = newValue
currentSignal.subs.version = currentSignal.subs.version + 1
}
}

// Create combined subs (this IS the observer for the computed)
let subs = Core.makeComputedSubs(recompute)
let subs = Core.makeComputedSubs(recompute, ~deferEffectsUntilRecompute=true)

// Initial computation under tracking to establish dependencies
let prev = Scheduler.currentComputedSubs.contents
Expand All @@ -24,18 +83,29 @@ let make = (compute: unit => 'a, ~name: option<string>=?): Signal.t<'a> => {
let signal: Signal.t<'a> = {
id,
value: initialValue,
equals: (_, _) => false, // Computeds always check freshness via dirty flag
equals: equalsFn,
name,
subs,
}

// Set the ref so recompute can access the signal
signalRef := signal
subs.lastGlobalVersion = Core.globalVersion.contents
Core.clearSubsDirty(subs)

signal
}

let make = (
compute: unit => 'a,
~name: option<string>=?,
~equals: option<('a, 'a) => bool>=?,
): Signal.t<'a> =>
switch equals {
| Some(eq) => makeWithEquals(compute, eq, ~name?)
| None => makeWithoutEquals(compute, ~name?)
}

let dispose = (signal: Signal.t<'a>): unit => {
Core.clearSubsDeps(signal.subs)
}
150 changes: 101 additions & 49 deletions packages/rescript-signals/src/signals/Core.res
Original file line number Diff line number Diff line change
Expand Up @@ -6,74 +6,99 @@ let flag_dirty = 1
let flag_pending = 2
let flag_running = 4

// Observer kind tag
// Global tracking version
let trackingVersion: ref<int> = ref(0)
// Global mutation version (increments on real signal writes)
let globalVersion: ref<int> = ref(0)

type kind = [#Effect | #Computed]

// Forward declare mutually recursive types
type rec link = {
// Direct reference to signal's subscriber list (type-erased)
mutable subs: subs,
// Direct reference to observer
mutable observer: observer,
// Links in the observer's dependency chain
mutable nextDep: option<link>,
mutable prevDep: option<link>,
// Links in the signal's subscriber chain
mutable nextSub: option<link>,
mutable prevSub: option<link>,
}
module rec Link: {
type t = {
// Direct reference to signal's subscriber list (type-erased)
mutable subs: Subs.t,
// Direct reference to observer
mutable observer: Observer.t,
// Links in the observer's dependency chain
mutable nextDep: option<Link.t>,
mutable prevDep: option<Link.t>,
// Links in the signal's subscriber chain
mutable nextSub: option<Link.t>,
mutable prevSub: option<Link.t>,
// Version stamp for duplicate detection within a compute cycle
mutable lastTrackedVersion: int,
}
} = Link

// Signal subscriber list (head/tail of linked list)
// For computeds, this same object also serves as the observer (combined structure)
and subs = {
mutable first: option<link>,
mutable last: option<link>,
mutable version: int,
// === Observer fields (only used for computeds) ===
// If compute is Some, this subs is a computed signal
mutable compute: option<unit => unit>,
mutable firstDep: option<link>,
mutable lastDep: option<link>,
mutable flags: int,
mutable level: int,
}
and Subs: {
type t = {
mutable first: option<Link.t>,
mutable last: option<Link.t>,
mutable computedSubscriberCount: int,
mutable version: int,
// === Observer fields (only used for computeds) ===
// If compute is Some, this subs is a computed signal
mutable compute: option<unit => unit>,
mutable firstDep: option<Link.t>,
mutable lastDep: option<Link.t>,
mutable flags: int,
mutable level: int,
mutable deferEffectsUntilRecompute: bool,
mutable lastGlobalVersion: int,
}
} = Subs

// Observer for effects only (computeds use subs directly)
and observer = {
id: int,
kind: kind,
run: unit => unit,
mutable firstDep: option<link>,
mutable lastDep: option<link>,
mutable flags: int,
mutable level: int,
name: option<string>,
// For computed observers: direct reference to backing subs (the combined object)
mutable backingSubs: option<subs>,
}
and Observer: {
type t = {
id: int,
kind: kind,
run: unit => unit,
mutable firstDep: option<Link.t>,
mutable lastDep: option<Link.t>,
mutable flags: int,
mutable level: int,
name: option<string>,
// For computed observers: direct reference to backing subs (the combined object)
mutable backingSubs: option<Subs.t>,
}
} = Observer

// Type aliases for convenience
type link = Link.t
type subs = Subs.t
type observer = Observer.t

// Create empty subscriber list (for plain signals)
let makeSubs = (): subs => {
first: None,
last: None,
computedSubscriberCount: 0,
version: 0,
compute: None,
firstDep: None,
lastDep: None,
flags: 0,
level: 0,
deferEffectsUntilRecompute: false,
lastGlobalVersion: 0,
}

// Create subs for a computed (with compute function)
let makeComputedSubs = (compute: unit => unit): subs => {
let makeComputedSubs = (compute: unit => unit, ~deferEffectsUntilRecompute: bool=false): subs => {
first: None,
last: None,
computedSubscriberCount: 0,
version: 0,
compute: Some(compute),
firstDep: None,
lastDep: None,
flags: flag_dirty, // start dirty
level: 0,
deferEffectsUntilRecompute,
lastGlobalVersion: 0,
}

// Create observer
Expand All @@ -95,7 +120,7 @@ let makeObserver = (
backingSubs,
}

// Flag operations for observer (using Int.Bitwise module)
// Flag operations for observer
let isDirty = (o: observer): bool => Int.bitwiseAnd(o.flags, flag_dirty) !== 0
let setDirty = (o: observer): unit => o.flags = Int.bitwiseOr(o.flags, flag_dirty)
let clearDirty = (o: observer): unit =>
Expand All @@ -105,7 +130,7 @@ let setPending = (o: observer): unit => o.flags = Int.bitwiseOr(o.flags, flag_pe
let clearPending = (o: observer): unit =>
o.flags = Int.bitwiseAnd(o.flags, Int.bitwiseNot(flag_pending))

// Flag operations for subs (for computeds - subs IS the observer)
// Flag operations for subs
let isSubsDirty = (s: subs): bool => Int.bitwiseAnd(s.flags, flag_dirty) !== 0
let setSubsDirty = (s: subs): unit => s.flags = Int.bitwiseOr(s.flags, flag_dirty)
let clearSubsDirty = (s: subs): unit =>
Expand All @@ -115,17 +140,20 @@ let setSubsPending = (s: subs): unit => s.flags = Int.bitwiseOr(s.flags, flag_pe
let clearSubsPending = (s: subs): unit =>
s.flags = Int.bitwiseAnd(s.flags, Int.bitwiseNot(flag_pending))

// Check if subs is a computed (has compute function)
// Check if subs is a computed
let isComputed = (s: subs): bool => s.compute !== None

// Create a link node
let makeLink = (subs: subs, observer: observer): link => {
subs,
observer,
nextDep: None,
prevDep: None,
nextSub: None,
prevSub: None,
let makeLink = (sourceSubs: subs, linkedObserver: observer): link => {
{
subs: sourceSubs,
observer: linkedObserver,
nextDep: None,
prevDep: None,
nextSub: None,
prevSub: None,
lastTrackedVersion: 0,
}
}

// Add link to signal's subscriber list
Expand All @@ -137,6 +165,11 @@ let linkToSubs = (subs: subs, link: link): unit => {
| None => subs.first = Some(link)
}
subs.last = Some(link)

let linkedSubs = (Obj.magic(link.observer): subs)
if isComputed(linkedSubs) {
subs.computedSubscriberCount = subs.computedSubscriberCount + 1
}
}

// Add link to observer's dependency list
Expand All @@ -163,6 +196,11 @@ let unlinkFromSubs = (link: link): unit => {
}
link.prevSub = None
link.nextSub = None

let linkedSubs = (Obj.magic(link.observer): subs)
if isComputed(linkedSubs) && subs.computedSubscriberCount > 0 {
subs.computedSubscriberCount = subs.computedSubscriberCount - 1
}
}

// Remove link from dependency list
Expand All @@ -179,6 +217,20 @@ let unlinkFromDeps = (observer: observer, link: link): unit => {
link.nextDep = None
}

// Remove link from subs's dependency list (for computeds - subs IS the observer)
let unlinkFromSubsDeps = (s: subs, link: link): unit => {
switch link.prevDep {
| Some(prev) => prev.nextDep = link.nextDep
| None => s.firstDep = link.nextDep
}
switch link.nextDep {
| Some(next) => next.prevDep = link.prevDep
| None => s.lastDep = link.prevDep
}
link.prevDep = None
link.nextDep = None
}

// Clear all dependencies from observer (unlinks from all signals)
let clearDeps = (observer: observer): unit => {
let link = ref(observer.firstDep)
Expand Down
Loading
Loading