From 0645947267a11b8e4d5bc78d7ddf7a93df608e9c Mon Sep 17 00:00:00 2001 From: Bernardo Gurgel Date: Wed, 22 Apr 2026 11:48:50 +0200 Subject: [PATCH 1/4] refactor: rename signal modules --- packages/rescript-signals/rescript.json | 1 - packages/rescript-signals/src/Signals.res | 14 +++++++++++--- .../{Computed.res => Signals__Computed.res} | 16 ++++++++-------- .../signals/{Effect.res => Signals__Effect.res} | 0 .../signals/{Signal.res => Signals__Signal.res} | 0 5 files changed, 19 insertions(+), 12 deletions(-) rename packages/rescript-signals/src/signals/{Computed.res => Signals__Computed.res} (89%) rename packages/rescript-signals/src/signals/{Effect.res => Signals__Effect.res} (100%) rename packages/rescript-signals/src/signals/{Signal.res => Signals__Signal.res} (100%) diff --git a/packages/rescript-signals/rescript.json b/packages/rescript-signals/rescript.json index 5e7c222..859cd6f 100644 --- a/packages/rescript-signals/rescript.json +++ b/packages/rescript-signals/rescript.json @@ -1,6 +1,5 @@ { "name": "rescript-signals", - "namespace": "Signals", "sources": [ { "dir": "src", diff --git a/packages/rescript-signals/src/Signals.res b/packages/rescript-signals/src/Signals.res index 4077dd8..453963e 100644 --- a/packages/rescript-signals/src/Signals.res +++ b/packages/rescript-signals/src/Signals.res @@ -1,3 +1,11 @@ -module Signal = Signal -module Computed = Computed -module Effect = Effect +module Signal = { + include Signals__Signal +} + +module Computed = { + include Signals__Computed +} + +module Effect = { + include Signals__Effect +} diff --git a/packages/rescript-signals/src/signals/Computed.res b/packages/rescript-signals/src/signals/Signals__Computed.res similarity index 89% rename from packages/rescript-signals/src/signals/Computed.res rename to packages/rescript-signals/src/signals/Signals__Computed.res index 683f2e4..fac3c20 100644 --- a/packages/rescript-signals/src/signals/Computed.res +++ b/packages/rescript-signals/src/signals/Signals__Computed.res @@ -1,13 +1,13 @@ let makeWithoutEquals = ( compute: unit => 'a, ~name: option=?, -): Signal.t<'a> => { +): Signals__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> = ref(Obj.magic()) + let signalRef: ref> = ref(Obj.magic()) // Fast recompute path for default behavior (no custom equality checks) let recompute = () => { @@ -27,7 +27,7 @@ let makeWithoutEquals = ( Scheduler.currentComputedSubs := prev // Create the signal with the initial value - let signal: Signal.t<'a> = { + let signal: Signals__Signal.t<'a> = { id, value: initialValue, equals: equalsFn, @@ -47,12 +47,12 @@ let makeWithEquals = ( compute: unit => 'a, equalsFn: ('a, 'a) => bool, ~name: option=?, -): Signal.t<'a> => { +): Signals__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> = ref(Obj.magic()) + let signalRef: ref> = ref(Obj.magic()) // Recompute function - updates the signal's value and tracks if it changed let recompute = () => { @@ -80,7 +80,7 @@ let makeWithEquals = ( Scheduler.currentComputedSubs := prev // Create the signal with the initial value - let signal: Signal.t<'a> = { + let signal: Signals__Signal.t<'a> = { id, value: initialValue, equals: equalsFn, @@ -100,12 +100,12 @@ let make = ( compute: unit => 'a, ~name: option=?, ~equals: option<('a, 'a) => bool>=?, -): Signal.t<'a> => +): Signals__Signal.t<'a> => switch equals { | Some(eq) => makeWithEquals(compute, eq, ~name?) | None => makeWithoutEquals(compute, ~name?) } -let dispose = (signal: Signal.t<'a>): unit => { +let dispose = (signal: Signals__Signal.t<'a>): unit => { Core.clearSubsDeps(signal.subs) } diff --git a/packages/rescript-signals/src/signals/Effect.res b/packages/rescript-signals/src/signals/Signals__Effect.res similarity index 100% rename from packages/rescript-signals/src/signals/Effect.res rename to packages/rescript-signals/src/signals/Signals__Effect.res diff --git a/packages/rescript-signals/src/signals/Signal.res b/packages/rescript-signals/src/signals/Signals__Signal.res similarity index 100% rename from packages/rescript-signals/src/signals/Signal.res rename to packages/rescript-signals/src/signals/Signals__Signal.res From f8d606e7fd3f13680af511fad1ea7e3951693c11 Mon Sep 17 00:00:00 2001 From: Bernardo Gurgel Date: Wed, 22 Apr 2026 11:50:11 +0200 Subject: [PATCH 2/4] refactor: rename internal signal modules --- .../src/signals/Signals__Computed.res | 30 ++-- .../signals/{Core.res => Signals__Core.res} | 0 .../src/signals/Signals__Effect.res | 18 +-- .../src/signals/{Id.res => Signals__Id.res} | 0 .../{Scheduler.res => Signals__Scheduler.res} | 138 +++++++++--------- .../src/signals/Signals__Signal.res | 24 +-- 6 files changed, 105 insertions(+), 105 deletions(-) rename packages/rescript-signals/src/signals/{Core.res => Signals__Core.res} (100%) rename packages/rescript-signals/src/signals/{Id.res => Signals__Id.res} (100%) rename packages/rescript-signals/src/signals/{Scheduler.res => Signals__Scheduler.res} (79%) diff --git a/packages/rescript-signals/src/signals/Signals__Computed.res b/packages/rescript-signals/src/signals/Signals__Computed.res index fac3c20..e8cea52 100644 --- a/packages/rescript-signals/src/signals/Signals__Computed.res +++ b/packages/rescript-signals/src/signals/Signals__Computed.res @@ -2,7 +2,7 @@ let makeWithoutEquals = ( compute: unit => 'a, ~name: option=?, ): Signals__Signal.t<'a> => { - let id = Id.make() + let id = Signals__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 @@ -18,13 +18,13 @@ let makeWithoutEquals = ( } // Create combined subs (this IS the observer for the computed) - let subs = Core.makeComputedSubs(recompute, ~deferEffectsUntilRecompute=false) + let subs = Signals__Core.makeComputedSubs(recompute, ~deferEffectsUntilRecompute=false) // Initial computation under tracking to establish dependencies - let prev = Scheduler.currentComputedSubs.contents - Scheduler.currentComputedSubs := Some(subs) + let prev = Signals__Scheduler.currentComputedSubs.contents + Signals__Scheduler.currentComputedSubs := Some(subs) let initialValue = compute() - Scheduler.currentComputedSubs := prev + Signals__Scheduler.currentComputedSubs := prev // Create the signal with the initial value let signal: Signals__Signal.t<'a> = { @@ -37,8 +37,8 @@ let makeWithoutEquals = ( // Set the ref so recompute can access the signal signalRef := signal - subs.lastGlobalVersion = Core.globalVersion.contents - Core.clearSubsDirty(subs) + subs.lastGlobalVersion = Signals__Core.globalVersion.contents + Signals__Core.clearSubsDirty(subs) signal } @@ -48,7 +48,7 @@ let makeWithEquals = ( equalsFn: ('a, 'a) => bool, ~name: option=?, ): Signals__Signal.t<'a> => { - let id = Id.make() + let id = Signals__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 @@ -71,13 +71,13 @@ let makeWithEquals = ( } // Create combined subs (this IS the observer for the computed) - let subs = Core.makeComputedSubs(recompute, ~deferEffectsUntilRecompute=true) + let subs = Signals__Core.makeComputedSubs(recompute, ~deferEffectsUntilRecompute=true) // Initial computation under tracking to establish dependencies - let prev = Scheduler.currentComputedSubs.contents - Scheduler.currentComputedSubs := Some(subs) + let prev = Signals__Scheduler.currentComputedSubs.contents + Signals__Scheduler.currentComputedSubs := Some(subs) let initialValue = compute() - Scheduler.currentComputedSubs := prev + Signals__Scheduler.currentComputedSubs := prev // Create the signal with the initial value let signal: Signals__Signal.t<'a> = { @@ -90,8 +90,8 @@ let makeWithEquals = ( // Set the ref so recompute can access the signal signalRef := signal - subs.lastGlobalVersion = Core.globalVersion.contents - Core.clearSubsDirty(subs) + subs.lastGlobalVersion = Signals__Core.globalVersion.contents + Signals__Core.clearSubsDirty(subs) signal } @@ -107,5 +107,5 @@ let make = ( } let dispose = (signal: Signals__Signal.t<'a>): unit => { - Core.clearSubsDeps(signal.subs) + Signals__Core.clearSubsDeps(signal.subs) } diff --git a/packages/rescript-signals/src/signals/Core.res b/packages/rescript-signals/src/signals/Signals__Core.res similarity index 100% rename from packages/rescript-signals/src/signals/Core.res rename to packages/rescript-signals/src/signals/Signals__Core.res diff --git a/packages/rescript-signals/src/signals/Signals__Effect.res b/packages/rescript-signals/src/signals/Signals__Effect.res index 060894f..109ea98 100644 --- a/packages/rescript-signals/src/signals/Signals__Effect.res +++ b/packages/rescript-signals/src/signals/Signals__Effect.res @@ -1,7 +1,7 @@ type disposer = {dispose: unit => unit} let runWithDisposer = (fn: unit => option unit>, ~name: option=?): disposer => { - let observerId = Id.make() + let observerId = Signals__Id.make() let cleanup: ref unit>> = ref(None) // Wrapper that handles cleanup @@ -17,24 +17,24 @@ let runWithDisposer = (fn: unit => option unit>, ~name: option=? } // Create observer using Core types - let observer = Core.makeObserver(observerId, #Effect, runWithCleanup, ~name?) + let observer = Signals__Core.makeObserver(observerId, #Effect, runWithCleanup, ~name?) // Initial run under tracking (no need to clearDeps - observer is fresh) - let prev = Scheduler.currentObserver.contents - Scheduler.currentObserver := Some(observer) + let prev = Signals__Scheduler.currentObserver.contents + Signals__Scheduler.currentObserver := Some(observer) try { observer.run() - Core.clearDirty(observer) - Scheduler.currentObserver := prev + Signals__Core.clearDirty(observer) + Signals__Scheduler.currentObserver := prev } catch { | exn => - Scheduler.currentObserver := prev + Signals__Scheduler.currentObserver := prev throw(exn) } // Compute level - observer.level = Scheduler.computeLevel(observer) + observer.level = Signals__Scheduler.computeLevel(observer) // Return disposer - stores observer reference directly (no Map lookup needed) let disposed = ref(false) @@ -49,7 +49,7 @@ let runWithDisposer = (fn: unit => option unit>, ~name: option=? | None => () } - Core.clearDeps(observer) + Signals__Core.clearDeps(observer) } } diff --git a/packages/rescript-signals/src/signals/Id.res b/packages/rescript-signals/src/signals/Signals__Id.res similarity index 100% rename from packages/rescript-signals/src/signals/Id.res rename to packages/rescript-signals/src/signals/Signals__Id.res diff --git a/packages/rescript-signals/src/signals/Scheduler.res b/packages/rescript-signals/src/signals/Signals__Scheduler.res similarity index 79% rename from packages/rescript-signals/src/signals/Scheduler.res rename to packages/rescript-signals/src/signals/Signals__Scheduler.res index 640fa04..0ff66f0 100644 --- a/packages/rescript-signals/src/signals/Scheduler.res +++ b/packages/rescript-signals/src/signals/Signals__Scheduler.res @@ -1,20 +1,20 @@ // Current execution context for computeds (subs IS the observer) -let currentComputedSubs: ref> = ref(None) +let currentComputedSubs: ref> = ref(None) // Current execution context for effects -let currentObserver: ref> = ref(None) +let currentObserver: ref> = ref(None) // Current dependency tracking version (shared across nested compute/effect runs) let currentTrackingVersion: ref = ref(0) // Per-run dependency cursors (separate from true tail pointers). -let currentComputedDepCursor: ref> = ref(None) -let currentObserverDepCursor: ref> = ref(None) +let currentComputedDepCursor: ref> = ref(None) +let currentObserverDepCursor: ref> = ref(None) // Pending effects to execute -let pendingEffects: array = [] +let pendingEffects: array = [] // Pending computeds to recompute (subs that are dirty) -let pendingComputedSubs: array = [] +let pendingComputedSubs: array = [] let flushing: ref = ref(false) let pendingEffectsNeedsSort: ref = ref(false) let pendingComputedNeedsSort: ref = ref(false) @@ -22,7 +22,7 @@ let lastEnqueuedEffectLevel: ref = ref(0) let lastEnqueuedComputedLevel: ref = ref(0) // Queue for iterative dirty marking -let dirtyQueue: array = [] +let dirtyQueue: array = [] // Efficient array clear let clearArray: array<'a> => unit = %raw(`function(arr) { arr.length = 0 }`) @@ -39,9 +39,9 @@ function(arr, processedCount) { `) // Add effect to pending if not already there -let addEffectToPending = (observer: Core.observer): unit => { - if !Core.isPending(observer) { - Core.setPending(observer) +let addEffectToPending = (observer: Signals__Core.observer): unit => { + if !Signals__Core.isPending(observer) { + Signals__Core.setPending(observer) let lengthBefore = pendingEffects->Array.length if lengthBefore == 0 { pendingEffectsNeedsSort := false @@ -54,9 +54,9 @@ let addEffectToPending = (observer: Core.observer): unit => { } // Add computed to pending if not already there -let addComputedToPending = (subs: Core.subs): unit => { - if !Core.isSubsPending(subs) { - Core.setSubsPending(subs) +let addComputedToPending = (subs: Signals__Core.subs): unit => { + if !Signals__Core.isSubsPending(subs) { + Signals__Core.setSubsPending(subs) let lengthBefore = pendingComputedSubs->Array.length if lengthBefore == 0 { pendingComputedNeedsSort := false @@ -69,14 +69,14 @@ let addComputedToPending = (subs: Core.subs): unit => { } // Track a dependency from a computed (subs tracks subs) -let trackDepFromComputed = (computedSubs: Core.subs, sourceSubs: Core.subs): unit => { - let computedObserver: Core.observer = Obj.magic(computedSubs) +let trackDepFromComputed = (computedSubs: Signals__Core.subs, sourceSubs: Signals__Core.subs): unit => { + let computedObserver: Signals__Core.observer = Obj.magic(computedSubs) if computedSubs.firstDep === None { - let newLink: Core.link = Core.makeLink(sourceSubs, computedObserver) + let newLink: Signals__Core.link = Signals__Core.makeLink(sourceSubs, computedObserver) newLink.lastTrackedVersion = currentTrackingVersion.contents - Core.linkToSubsDeps(computedSubs, newLink) - Core.linkToSubs(sourceSubs, newLink) + Signals__Core.linkToSubsDeps(computedSubs, newLink) + Signals__Core.linkToSubs(sourceSubs, newLink) currentComputedDepCursor := Some(newLink) } else { let currentVersion = currentTrackingVersion.contents @@ -116,7 +116,7 @@ let trackDepFromComputed = (computedSubs: Core.subs, sourceSubs: Core.subs): uni if !fastPathFound.contents { // Fall back to full scan let found = ref(false) - let foundLink: ref> = ref(None) + let foundLink: ref> = ref(None) let link = ref(computedSubs.firstDep) while link.contents !== None && !found.contents { switch link.contents { @@ -134,10 +134,10 @@ let trackDepFromComputed = (computedSubs: Core.subs, sourceSubs: Core.subs): uni // Create new link only if not found if !found.contents { - let newLink: Core.link = Core.makeLink(sourceSubs, computedObserver) + let newLink: Signals__Core.link = Signals__Core.makeLink(sourceSubs, computedObserver) newLink.lastTrackedVersion = currentVersion - Core.linkToSubsDeps(computedSubs, newLink) - Core.linkToSubs(sourceSubs, newLink) + Signals__Core.linkToSubsDeps(computedSubs, newLink) + Signals__Core.linkToSubs(sourceSubs, newLink) currentComputedDepCursor := Some(newLink) } else { currentComputedDepCursor := foundLink.contents @@ -148,12 +148,12 @@ let trackDepFromComputed = (computedSubs: Core.subs, sourceSubs: Core.subs): uni // Track a dependency from an effect (observer tracks subs) // Uses version-based duplicate detection within a run cycle -let trackDepFromEffect = (observer: Core.observer, sourceSubs: Core.subs): unit => { +let trackDepFromEffect = (observer: Signals__Core.observer, sourceSubs: Signals__Core.subs): unit => { if observer.firstDep === None { - let newLink: Core.link = Core.makeLink(sourceSubs, observer) + let newLink: Signals__Core.link = Signals__Core.makeLink(sourceSubs, observer) newLink.lastTrackedVersion = currentTrackingVersion.contents - Core.linkToDeps(observer, newLink) - Core.linkToSubs(sourceSubs, newLink) + Signals__Core.linkToDeps(observer, newLink) + Signals__Core.linkToSubs(sourceSubs, newLink) currentObserverDepCursor := Some(newLink) } else { let currentVersion = currentTrackingVersion.contents @@ -192,7 +192,7 @@ let trackDepFromEffect = (observer: Core.observer, sourceSubs: Core.subs): unit if !fastPathFound.contents { let found = ref(false) - let foundLink: ref> = ref(None) + let foundLink: ref> = ref(None) let link = ref(observer.firstDep) while link.contents !== None && !found.contents { switch link.contents { @@ -210,10 +210,10 @@ let trackDepFromEffect = (observer: Core.observer, sourceSubs: Core.subs): unit // Create new link only if not found if !found.contents { - let newLink: Core.link = Core.makeLink(sourceSubs, observer) + let newLink: Signals__Core.link = Signals__Core.makeLink(sourceSubs, observer) newLink.lastTrackedVersion = currentVersion - Core.linkToDeps(observer, newLink) - Core.linkToSubs(sourceSubs, newLink) + Signals__Core.linkToDeps(observer, newLink) + Signals__Core.linkToSubs(sourceSubs, newLink) currentObserverDepCursor := Some(newLink) } else { currentObserverDepCursor := foundLink.contents @@ -223,7 +223,7 @@ let trackDepFromEffect = (observer: Core.observer, sourceSubs: Core.subs): unit } // Track dependency - routes to appropriate function based on current context -let trackDep = (subs: Core.subs): unit => { +let trackDep = (subs: Signals__Core.subs): unit => { switch currentComputedSubs.contents { | Some(computedSubs) => trackDepFromComputed(computedSubs, subs) | None => @@ -235,23 +235,23 @@ let trackDep = (subs: Core.subs): unit => { } // Compare by level for sorting -let compareEffectsByLevel = (a: Core.observer, b: Core.observer): float => { +let compareEffectsByLevel = (a: Signals__Core.observer, b: Signals__Core.observer): float => { Int.toFloat(a.level - b.level) } -let compareSubsByLevel = (a: Core.subs, b: Core.subs): float => { +let compareSubsByLevel = (a: Signals__Core.subs, b: Signals__Core.subs): float => { Int.toFloat(a.level - b.level) } // Compute level for a computed (based on its dependencies) -let computeSubsLevel = (s: Core.subs): int => { +let computeSubsLevel = (s: Signals__Core.subs): int => { let maxLevel = ref(0) let link = ref(s.firstDep) while link.contents !== None { switch link.contents { | Some(l) => // Check if the source is a computed - if Core.isComputed(l.subs) { + if Signals__Core.isComputed(l.subs) { if l.subs.level > maxLevel.contents { maxLevel := l.subs.level } @@ -264,13 +264,13 @@ let computeSubsLevel = (s: Core.subs): int => { } // Compute level for an effect -let computeLevel = (observer: Core.observer): int => { +let computeLevel = (observer: Signals__Core.observer): int => { let maxLevel = ref(0) let link = ref(observer.firstDep) while link.contents !== None { switch link.contents { | Some(l) => - if Core.isComputed(l.subs) { + if Signals__Core.isComputed(l.subs) { if l.subs.level > maxLevel.contents { maxLevel := l.subs.level } @@ -283,17 +283,17 @@ let computeLevel = (observer: Core.observer): int => { } // Run one computed recompute cycle with link reuse. -let runComputedCycle = (subs: Core.subs, ~clearPending: bool): unit => { +let runComputedCycle = (subs: Signals__Core.subs, ~clearPending: bool): unit => { let previousTrackingVersion = currentTrackingVersion.contents let previousVersion = subs.version // Increment tracking version for this cycle - Core.trackingVersion := Core.trackingVersion.contents + 1 - currentTrackingVersion.contents = Core.trackingVersion.contents + Signals__Core.trackingVersion := Signals__Core.trackingVersion.contents + 1 + currentTrackingVersion.contents = Signals__Core.trackingVersion.contents // DON'T clear deps - we'll reuse existing links if clearPending { - Core.clearSubsPending(subs) + Signals__Core.clearSubsPending(subs) } let prev = currentComputedSubs.contents @@ -315,16 +315,16 @@ let runComputedCycle = (subs: Core.subs, ~clearPending: bool): unit => { let next = l.nextDep if l.lastTrackedVersion !== currentTrackingVersion.contents { // Stale - unlink from source's subscriber list and our dep list - Core.unlinkFromSubs(l) - Core.unlinkFromSubsDeps(subs, l) + Signals__Core.unlinkFromSubs(l) + Signals__Core.unlinkFromSubsDeps(subs, l) } link := next | None => () } } - Core.clearSubsDirty(subs) - subs.lastGlobalVersion = Core.globalVersion.contents + Signals__Core.clearSubsDirty(subs) + subs.lastGlobalVersion = Signals__Core.globalVersion.contents // Propagate only when computed output changed. if subs.first !== None && subs.version !== previousVersion { @@ -332,10 +332,10 @@ let runComputedCycle = (subs: Core.subs, ~clearPending: bool): unit => { while subLink.contents !== None { switch subLink.contents { | Some(l) => - let linkedSubs = (Obj.magic(l.observer): Core.subs) - if Core.isComputed(linkedSubs) { + let linkedSubs = (Obj.magic(l.observer): Signals__Core.subs) + if Signals__Core.isComputed(linkedSubs) { // Mark downstream computed dirty (lazy propagation). - Core.setSubsDirty(linkedSubs) + Signals__Core.setSubsDirty(linkedSubs) } else { // Effects get queued for execution unless this effect is already running. switch currentObserver.contents { @@ -365,7 +365,7 @@ let runComputedCycle = (subs: Core.subs, ~clearPending: bool): unit => { } // Retrack a computed (recompute with link reuse) -let retrackComputed = (s: Core.subs): unit => { +let retrackComputed = (s: Signals__Core.subs): unit => { let oldLevel = s.level runComputedCycle(s, ~clearPending=true) @@ -375,16 +375,16 @@ let retrackComputed = (s: Core.subs): unit => { } // Retrack an effect (with link reuse) -let retrackEffect = (observer: Core.observer): unit => { +let retrackEffect = (observer: Signals__Core.observer): unit => { let oldLevel = observer.level let previousTrackingVersion = currentTrackingVersion.contents // Increment tracking version for this cycle - Core.trackingVersion := Core.trackingVersion.contents + 1 - currentTrackingVersion.contents = Core.trackingVersion.contents + Signals__Core.trackingVersion := Signals__Core.trackingVersion.contents + 1 + currentTrackingVersion.contents = Signals__Core.trackingVersion.contents // DON'T clear deps - we'll reuse existing links - Core.clearPending(observer) + Signals__Core.clearPending(observer) let prev = currentObserver.contents let prevCursor = currentObserverDepCursor.contents @@ -402,15 +402,15 @@ let retrackEffect = (observer: Core.observer): unit => { let next = l.nextDep if l.lastTrackedVersion !== currentTrackingVersion.contents { // Stale - unlink from source's subscriber list and our dep list - Core.unlinkFromSubs(l) - Core.unlinkFromDeps(observer, l) + Signals__Core.unlinkFromSubs(l) + Signals__Core.unlinkFromDeps(observer, l) } link := next | None => () } } - Core.clearDirty(observer) + Signals__Core.clearDirty(observer) currentObserver := prev currentObserverDepCursor := prevCursor currentTrackingVersion.contents = previousTrackingVersion @@ -489,11 +489,11 @@ let flush = (): unit => { // Marks computeds dirty transitively. // Direct effects are queued immediately. // Effects reached through dirty computeds are deferred until parent computed recompute. -let notifySubs = (subs: Core.subs): unit => { +let notifySubs = (subs: Signals__Core.subs): unit => { // Fast path: no subscribers, nothing to notify. if subs.first === None { () - } else if !Core.isComputed(subs) && subs.computedSubscriberCount == 0 { + } else if !Signals__Core.isComputed(subs) && subs.computedSubscriberCount == 0 { // Fast path for plain signals with direct effect subscribers only. let link = ref(subs.first) while link.contents !== None { @@ -519,18 +519,18 @@ let notifySubs = (subs: Core.subs): unit => { switch link.contents { | Some(l) => // The observer field might be a real observer (effect) or a subs (computed) - let linkedSubs = (Obj.magic(l.observer): Core.subs) - if Core.isComputed(linkedSubs) { + let linkedSubs = (Obj.magic(l.observer): Signals__Core.subs) + if Signals__Core.isComputed(linkedSubs) { // It's a computed - mark dirty and propagate transitively - if !Core.isSubsDirty(linkedSubs) { - Core.setSubsDirty(linkedSubs) + if !Signals__Core.isSubsDirty(linkedSubs) { + Signals__Core.setSubsDirty(linkedSubs) dirtyQueue->Array.push(linkedSubs)->ignore } } else { // It's an effect. // If reached via a dirty computed, defer effect until computed recompute. // This lets computed equality short-circuit downstream effect runs. - if Core.isComputed(s) { + if Signals__Core.isComputed(s) { if s.deferEffectsUntilRecompute { addComputedToPending(s) } else { @@ -555,12 +555,12 @@ let notifySubs = (subs: Core.subs): unit => { } // Ensure a computed signal is fresh before reading (with link reuse) -let ensureComputedFresh = (subs: Core.subs): unit => { - if Core.isComputed(subs) { - if Core.isSubsDirty(subs) { +let ensureComputedFresh = (subs: Signals__Core.subs): unit => { + if Signals__Core.isComputed(subs) { + if Signals__Core.isSubsDirty(subs) { // Dirty without a newer global write means stale dirty flag only. - if subs.lastGlobalVersion === Core.globalVersion.contents { - Core.clearSubsDirty(subs) + if subs.lastGlobalVersion === Signals__Core.globalVersion.contents { + Signals__Core.clearSubsDirty(subs) } else { let oldLevel = subs.level runComputedCycle(subs, ~clearPending=false) @@ -574,7 +574,7 @@ let ensureComputedFresh = (subs: Core.subs): unit => { } // Schedule an effect for execution -let schedule = (observer: Core.observer): unit => { +let schedule = (observer: Signals__Core.observer): unit => { addEffectToPending(observer) if !flushing.contents { flush() diff --git a/packages/rescript-signals/src/signals/Signals__Signal.res b/packages/rescript-signals/src/signals/Signals__Signal.res index b7f8a84..e952b56 100644 --- a/packages/rescript-signals/src/signals/Signals__Signal.res +++ b/packages/rescript-signals/src/signals/Signals__Signal.res @@ -4,7 +4,7 @@ type t<'a> = { equals: ('a, 'a) => bool, name: option, // Subscriber linked list (replaces signalObservers Map lookup) - subs: Core.subs, + subs: Signals__Core.subs, } let defaultEquals = (a: 'a, b: 'a): bool => a === b @@ -13,7 +13,7 @@ let neverEquals: ('a, 'a) => bool = (_a, _b) => false let make = (initialValue: 'a, ~name: option=?, ~equals: option<('a, 'a) => bool>=?): t< 'a, > => { - let id = Id.make() + let id = Signals__Id.make() let equalsFn = switch equals { | Some(eq) => eq | None => defaultEquals @@ -24,35 +24,35 @@ let make = (initialValue: 'a, ~name: option=?, ~equals: option<('a, 'a) value: initialValue, equals: equalsFn, name, - subs: Core.makeSubs(), + subs: Signals__Core.makeSubs(), } } // Optimized signal creation for computed backing signals (no equals check needed) let makeForComputed = (initialValue: 'a, ~name: option=?): t<'a> => { - let id = Id.make() + let id = Signals__Id.make() { id, value: initialValue, equals: neverEquals, // Computeds always check freshness via dirty flag name, - subs: Core.makeSubs(), + subs: Signals__Core.makeSubs(), } } // Optimized get - inlined hot path checks let get = (signal: t<'a>): 'a => { // Ensure computed is fresh - Scheduler.ensureComputedFresh(signal.subs) + Signals__Scheduler.ensureComputedFresh(signal.subs) // Track dependency if we're inside a computed or effect - Scheduler.trackDep(signal.subs) + Signals__Scheduler.trackDep(signal.subs) signal.value } let peek = (signal: t<'a>): 'a => { - Scheduler.ensureComputedFresh(signal.subs) + Signals__Scheduler.ensureComputedFresh(signal.subs) signal.value } @@ -66,13 +66,13 @@ let set = (signal: t<'a>, newValue: 'a): unit => { if shouldUpdate { signal.value = newValue signal.subs.version = signal.subs.version + 1 - Core.globalVersion := Core.globalVersion.contents + 1 - Scheduler.notifySubs(signal.subs) + Signals__Core.globalVersion := Signals__Core.globalVersion.contents + 1 + Signals__Scheduler.notifySubs(signal.subs) } } let update = (signal: t<'a>, fn: 'a => 'a): unit => signal->set(fn(signal.value)) -let batch = Scheduler.batch +let batch = Signals__Scheduler.batch -let untrack = Scheduler.untrack +let untrack = Signals__Scheduler.untrack From a0376b3479e9f9f7f5f9db92ca5db11d68e9de13 Mon Sep 17 00:00:00 2001 From: Bernardo Gurgel Date: Wed, 22 Apr 2026 11:52:38 +0200 Subject: [PATCH 3/4] fix: benchmark module imports --- benchmark.mjs | 4 +- scripts/ci/benchmark-pr-vs-frameworks.mjs | 47 ++++++++++++++++++++-- scripts/ci/benchmark-pr-vs-main.mjs | 49 +++++++++++++++++++++-- 3 files changed, 90 insertions(+), 10 deletions(-) diff --git a/benchmark.mjs b/benchmark.mjs index 14a145f..b00d9b1 100644 --- a/benchmark.mjs +++ b/benchmark.mjs @@ -1,5 +1,5 @@ // Simple benchmark for rescript-signals -import { Signal, Computed, Effect } from './src/Signals.res.mjs'; +import { Signal, Computed, Effect } from './packages/rescript-signals/src/Signals.res.mjs'; import { writeFileSync, mkdirSync, existsSync } from 'fs'; const results = []; @@ -135,7 +135,7 @@ console.log('\n--- Effects ---'); { let effectCount = 0; const source = Signal.make(0); - const effect = Effect.run(() => { + const effect = Effect.runWithDisposer(() => { Signal.get(source); effectCount++; return undefined; diff --git a/scripts/ci/benchmark-pr-vs-frameworks.mjs b/scripts/ci/benchmark-pr-vs-frameworks.mjs index 0fa14d3..e6c8dd3 100644 --- a/scripts/ci/benchmark-pr-vs-frameworks.mjs +++ b/scripts/ci/benchmark-pr-vs-frameworks.mjs @@ -125,16 +125,43 @@ function createReScriptFramework(name, modules) { function resolveSignalsDir(baseDir) { const candidates = [ baseDir, + resolve(baseDir, "src"), resolve(baseDir, "src/signals"), resolve(baseDir, "lib/bs/src/signals"), ]; for (const candidate of candidates) { + const bundledEntryFile = resolve(candidate, "Signals.res.mjs"); + if (existsSync(bundledEntryFile)) { + return { + type: "entry", + file: bundledEntryFile, + }; + } + const signalFile = resolve(candidate, "Signal.res.mjs"); const computedFile = resolve(candidate, "Computed.res.mjs"); const effectFile = resolve(candidate, "Effect.res.mjs"); if (existsSync(signalFile) && existsSync(computedFile) && existsSync(effectFile)) { - return candidate; + return { + type: "split", + dir: candidate, + }; + } + + const prefixedSignalFile = resolve(candidate, "Signals__Signal.res.mjs"); + const prefixedComputedFile = resolve(candidate, "Signals__Computed.res.mjs"); + const prefixedEffectFile = resolve(candidate, "Signals__Effect.res.mjs"); + if ( + existsSync(prefixedSignalFile) && + existsSync(prefixedComputedFile) && + existsSync(prefixedEffectFile) + ) { + return { + type: "split", + dir: candidate, + prefixed: true, + }; } } @@ -143,10 +170,22 @@ function resolveSignalsDir(baseDir) { async function importSignalModules(baseDir) { const signalsDir = resolveSignalsDir(baseDir); + + if (signalsDir.type === "entry") { + const modules = await import(pathToFileURL(signalsDir.file).href); + return { + Signal: modules.Signal, + Computed: modules.Computed, + Effect: modules.Effect, + }; + } + + const prefix = signalsDir.prefixed ? "Signals__" : ""; + const dir = signalsDir.dir; return { - Signal: await import(pathToFileURL(resolve(signalsDir, "Signal.res.mjs")).href), - Computed: await import(pathToFileURL(resolve(signalsDir, "Computed.res.mjs")).href), - Effect: await import(pathToFileURL(resolve(signalsDir, "Effect.res.mjs")).href), + Signal: await import(pathToFileURL(resolve(dir, `${prefix}Signal.res.mjs`)).href), + Computed: await import(pathToFileURL(resolve(dir, `${prefix}Computed.res.mjs`)).href), + Effect: await import(pathToFileURL(resolve(dir, `${prefix}Effect.res.mjs`)).href), }; } diff --git a/scripts/ci/benchmark-pr-vs-main.mjs b/scripts/ci/benchmark-pr-vs-main.mjs index 2d2899f..55f345b 100644 --- a/scripts/ci/benchmark-pr-vs-main.mjs +++ b/scripts/ci/benchmark-pr-vs-main.mjs @@ -126,16 +126,43 @@ function createReScriptFramework(name, modules) { function resolveSignalsDir(baseDir, label) { const candidates = [ baseDir, + resolve(baseDir, "src"), resolve(baseDir, "src/signals"), resolve(baseDir, "lib/bs/src/signals"), ]; for (const candidate of candidates) { + const bundledEntryFile = resolve(candidate, "Signals.res.mjs"); + if (existsSync(bundledEntryFile)) { + return { + type: "entry", + file: bundledEntryFile, + }; + } + const signalFile = resolve(candidate, "Signal.res.mjs"); const computedFile = resolve(candidate, "Computed.res.mjs"); const effectFile = resolve(candidate, "Effect.res.mjs"); if (existsSync(signalFile) && existsSync(computedFile) && existsSync(effectFile)) { - return candidate; + return { + type: "split", + dir: candidate, + }; + } + + const prefixedSignalFile = resolve(candidate, "Signals__Signal.res.mjs"); + const prefixedComputedFile = resolve(candidate, "Signals__Computed.res.mjs"); + const prefixedEffectFile = resolve(candidate, "Signals__Effect.res.mjs"); + if ( + existsSync(prefixedSignalFile) && + existsSync(prefixedComputedFile) && + existsSync(prefixedEffectFile) + ) { + return { + type: "split", + dir: candidate, + prefixed: true, + }; } } @@ -146,10 +173,24 @@ function resolveSignalsDir(baseDir, label) { async function importSignalModules(signalsDir, label) { const resolvedSignalsDir = resolveSignalsDir(signalsDir, label); + + if (resolvedSignalsDir.type === "entry") { + const modules = await import(pathToFileURL(resolvedSignalsDir.file).href); + return { + Signal: modules.Signal, + Computed: modules.Computed, + Effect: modules.Effect, + }; + } + + const dir = resolvedSignalsDir.dir; + const isPrefixed = resolvedSignalsDir.prefixed; + const prefix = isPrefixed ? "Signals__" : ""; + return { - Signal: await import(pathToFileURL(resolve(resolvedSignalsDir, "Signal.res.mjs")).href), - Computed: await import(pathToFileURL(resolve(resolvedSignalsDir, "Computed.res.mjs")).href), - Effect: await import(pathToFileURL(resolve(resolvedSignalsDir, "Effect.res.mjs")).href), + Signal: await import(pathToFileURL(resolve(dir, `${prefix}Signal.res.mjs`)).href), + Computed: await import(pathToFileURL(resolve(dir, `${prefix}Computed.res.mjs`)).href), + Effect: await import(pathToFileURL(resolve(dir, `${prefix}Effect.res.mjs`)).href), }; } From 769fcf6421a991ad1e811738f7d1339dd9546f66 Mon Sep 17 00:00:00 2001 From: Bernardo Gurgel Date: Wed, 22 Apr 2026 11:54:06 +0200 Subject: [PATCH 4/4] chore: add internal module aliases --- packages/rescript-signals/src/signals/Core.res | 1 + packages/rescript-signals/src/signals/Id.res | 1 + packages/rescript-signals/src/signals/Scheduler.res | 1 + 3 files changed, 3 insertions(+) create mode 100644 packages/rescript-signals/src/signals/Core.res create mode 100644 packages/rescript-signals/src/signals/Id.res create mode 100644 packages/rescript-signals/src/signals/Scheduler.res diff --git a/packages/rescript-signals/src/signals/Core.res b/packages/rescript-signals/src/signals/Core.res new file mode 100644 index 0000000..fbccdb0 --- /dev/null +++ b/packages/rescript-signals/src/signals/Core.res @@ -0,0 +1 @@ +include Signals__Core diff --git a/packages/rescript-signals/src/signals/Id.res b/packages/rescript-signals/src/signals/Id.res new file mode 100644 index 0000000..3b0e3a2 --- /dev/null +++ b/packages/rescript-signals/src/signals/Id.res @@ -0,0 +1 @@ +include Signals__Id diff --git a/packages/rescript-signals/src/signals/Scheduler.res b/packages/rescript-signals/src/signals/Scheduler.res new file mode 100644 index 0000000..ed31002 --- /dev/null +++ b/packages/rescript-signals/src/signals/Scheduler.res @@ -0,0 +1 @@ +include Signals__Scheduler