From 08336b3c66926da085e0602acea7610f2dad0924 Mon Sep 17 00:00:00 2001 From: Seven Du <5564821+medz@users.noreply.github.com> Date: Sat, 6 Dec 2025 23:26:56 +0800 Subject: [PATCH 01/19] Replace custom reactive system with alien_signals preset --- packages/solidart/lib/src/core/alien.dart | 77 +++------ packages/solidart/lib/src/core/batch.dart | 4 +- packages/solidart/lib/src/core/computed.dart | 8 +- packages/solidart/lib/src/core/core.dart | 3 +- packages/solidart/lib/src/core/effect.dart | 47 ++++-- .../lib/src/core/reactive_system.dart | 159 ------------------ .../solidart/lib/src/core/read_signal.dart | 33 ++-- packages/solidart/lib/src/core/untracked.dart | 4 +- packages/solidart/pubspec.yaml | 2 +- 9 files changed, 74 insertions(+), 263 deletions(-) diff --git a/packages/solidart/lib/src/core/alien.dart b/packages/solidart/lib/src/core/alien.dart index d6417dfa..9de7d4bd 100644 --- a/packages/solidart/lib/src/core/alien.dart +++ b/packages/solidart/lib/src/core/alien.dart @@ -1,74 +1,35 @@ part of 'core.dart'; -class _AlienComputed extends alien.ReactiveNode implements _AlienUpdatable { - _AlienComputed(this.parent, this.getter) - : super(flags: 17 /* Mutable | Dirty */); +class _AlienComputed extends alien_preset.ComputedNode { + _AlienComputed(this.parent, T Function(T? oldValue) getter) + : super(flags: alien.ReactiveFlags.none, getter: getter); final Computed parent; - final T Function(T? oldValue) getter; - T? value; - - void dispose() => reactiveSystem.stopEffect(this); - - @override - bool update() { - final prevSub = reactiveSystem.setCurrentSub(this); - reactiveSystem.startTracking(this); - try { - final oldValue = value; - return oldValue != (value = getter(oldValue)); - } finally { - reactiveSystem - ..setCurrentSub(prevSub) - ..endTracking(this); - } - } + void dispose() => alien_preset.stop(this); } -class _AlienEffect extends alien.ReactiveNode { - _AlienEffect(this.parent, this.run, {bool? detach}) - : detach = detach ?? SolidartConfig.detachEffects, - super(flags: 2 /* Watching */); - - _AlienEffect? nextEffect; +class _AlienEffect extends alien_preset.EffectNode { + _AlienEffect( + this.parent, + {required super.fn, + bool? detach, + required super.flags, + }) : detach = detach ?? SolidartConfig.detachEffects; final bool detach; final Effect parent; - final void Function() run; - void dispose() => reactiveSystem.stopEffect(this); + void dispose() => alien_preset.stop(this); } -class _AlienSignal extends alien.ReactiveNode implements _AlienUpdatable { - _AlienSignal(this.parent, this.value) - : previousValue = value, - super(flags: 1 /* Mutable */); +class _AlienSignal extends alien_preset.SignalNode> { + _AlienSignal(this.parent, Option value) + : super( + flags: alien.ReactiveFlags.mutable, + currentValue: value, + pendingValue: value, + ); final SignalBase parent; - - Option previousValue; - Option value; - - bool forceDirty = false; - - @override - bool update() { - flags = 1 /* Mutable */; - if (forceDirty) { - forceDirty = false; - return true; - } - if (!parent._compare(previousValue.safeUnwrap(), value.safeUnwrap())) { - previousValue = value; - return true; - } - - return false; - } -} - -// ignore: one_member_abstracts -abstract interface class _AlienUpdatable { - bool update(); } diff --git a/packages/solidart/lib/src/core/batch.dart b/packages/solidart/lib/src/core/batch.dart index 3c874676..37f03f58 100644 --- a/packages/solidart/lib/src/core/batch.dart +++ b/packages/solidart/lib/src/core/batch.dart @@ -21,10 +21,10 @@ part of 'core.dart'; /// So when `x` changes, the effect is paused and you never see it printing: /// "x = 11, y = 20". T batch(T Function() fn) { - reactiveSystem.startBatch(); + alien_preset.startBatch(); try { return fn(); } finally { - reactiveSystem.endBatch(); + alien_preset.endBatch(); } } diff --git a/packages/solidart/lib/src/core/computed.dart b/packages/solidart/lib/src/core/computed.dart index 1df6260a..450fc314 100644 --- a/packages/solidart/lib/src/core/computed.dart +++ b/packages/solidart/lib/src/core/computed.dart @@ -152,7 +152,13 @@ class Computed extends ReadSignal { return _untrackedValue; } - final value = reactiveSystem.getComputedValue(_internalComputed); + if ((_internalComputed.flags & alien.ReactiveFlags.pending) != + alien.ReactiveFlags.none && + _internalComputed.deps == null) { + _internalComputed.flags &= ~alien.ReactiveFlags.pending; + } + + final value = _internalComputed.get(); if (autoDispose) { _mayDispose(); } diff --git a/packages/solidart/lib/src/core/core.dart b/packages/solidart/lib/src/core/core.dart index 18a6a069..e62105bb 100644 --- a/packages/solidart/lib/src/core/core.dart +++ b/packages/solidart/lib/src/core/core.dart @@ -4,7 +4,8 @@ import 'dart:convert'; import 'dart:developer' as dev; import 'dart:math'; -import 'package:alien_signals/alien_signals.dart' as alien; +import 'package:alien_signals/preset.dart' as alien_preset; +import 'package:alien_signals/system.dart' as alien; import 'package:collection/collection.dart'; import 'package:meta/meta.dart'; import 'package:solidart/src/extensions/until.dart'; diff --git a/packages/solidart/lib/src/core/effect.dart b/packages/solidart/lib/src/core/effect.dart index 16b2eba4..1ca791f2 100644 --- a/packages/solidart/lib/src/core/effect.dart +++ b/packages/solidart/lib/src/core/effect.dart @@ -148,7 +148,26 @@ class Effect implements ReactionInterface { ErrorCallback? onError, bool? detach, }) : _onError = onError { - _internalEffect = _AlienEffect(this, callback, detach: detach); + VoidCallback safeCallback() { + return () { + try { + callback(); + } catch (e, s) { + if (_onError != null) { + _onError!.call(SolidartCaughtException(e, stackTrace: s)); + return; + } + rethrow; + } + }; + } + + _internalEffect = _AlienEffect( + this, + fn: safeCallback(), + detach: detach, + flags: alien.ReactiveFlags.watching | alien.ReactiveFlags.dirty, + ); } /// The name of the effect, useful for logging purposes. @@ -175,25 +194,21 @@ class Effect implements ReactionInterface { /// Runs the effect, tracking any signal read during the execution. void run() { - final currentSub = reactiveSystem.activeSub; - if (!SolidartConfig.detachEffects && currentSub != null) { - if (currentSub is! _AlienEffect || - (!_internalEffect.detach && !currentSub.detach)) { - reactiveSystem.link(_internalEffect, currentSub); - } + final currentSub = alien_preset.getActiveSub(); + if (!SolidartConfig.detachEffects && + currentSub != null && + (currentSub is! _AlienEffect || + (!_internalEffect.detach && !currentSub.detach))) { + alien_preset.link(_internalEffect, currentSub, alien_preset.cycle); } - final prevSub = reactiveSystem.setCurrentSub(_internalEffect); try { - _internalEffect.run(); - } catch (e, s) { - if (_onError != null) { - _onError.call(SolidartCaughtException(e, stackTrace: s)); - } else { - rethrow; - } + alien_preset.run(_internalEffect); + } catch (_) { + // The callback handles the error reporting, just rethrow to preserve + // the behavior when no handler is provided. + rethrow; } finally { - reactiveSystem.setCurrentSub(prevSub); if (SolidartConfig.autoDispose) { _mayDispose(); } diff --git a/packages/solidart/lib/src/core/reactive_system.dart b/packages/solidart/lib/src/core/reactive_system.dart index 25d88943..17a9d075 100644 --- a/packages/solidart/lib/src/core/reactive_system.dart +++ b/packages/solidart/lib/src/core/reactive_system.dart @@ -42,162 +42,3 @@ class ReactiveName { return '$prefix@${_instance.nextId}'; } } - -@protected -final reactiveSystem = ReactiveSystem(); - -class ReactiveSystem extends alien.ReactiveSystem { - int batchDepth = 0; - alien.ReactiveNode? activeSub; - _AlienEffect? queuedEffects; - _AlienEffect? queuedEffectsTail; - - @override - void notify(alien.ReactiveNode node) { - final flags = node.flags; - if ((flags & 64 /* Queued */ ) == 0) { - node.flags = flags | 64 /* Queued */; - final subs = node.subs; - if (subs != null) { - notify(subs.sub); - } else if (queuedEffectsTail != null) { - queuedEffectsTail = queuedEffectsTail!.nextEffect = - node as _AlienEffect; - } else { - queuedEffectsTail = queuedEffects = node as _AlienEffect; - } - } - } - - @override - void unwatched(alien.ReactiveNode node) { - if (node is _AlienComputed) { - var toRemove = node.deps; - if (toRemove != null) { - node.flags = 17 /* Mutable | Dirty */; - do { - toRemove = unlink(toRemove!, node); - } while (toRemove != null); - } - } else if (node is! _AlienSignal) { - stopEffect(node); - } - } - - @override - bool update(alien.ReactiveNode node) { - assert( - node is _AlienUpdatable, - 'Reactive node type must be signal or computed', - ); - return (node as _AlienUpdatable).update(); - } - - void startBatch() => ++batchDepth; - void endBatch() { - if ((--batchDepth) == 0) flush(); - } - - alien.ReactiveNode? setCurrentSub(alien.ReactiveNode? sub) { - final prevSub = activeSub; - activeSub = sub; - return prevSub; - } - - T getComputedValue(_AlienComputed computed) { - final flags = computed.flags; - if ((flags & 16 /* Dirty */ ) != 0 || - ((flags & 32 /* Pending */ ) != 0 && - computed.deps != null && - checkDirty(computed.deps!, computed))) { - if (computed.update()) { - final subs = computed.subs; - if (subs != null) shallowPropagate(subs); - } - } else if ((flags & 32 /* Pending */ ) != 0) { - computed.flags = flags & -33 /* ~Pending */; - } - if (activeSub != null) { - link(computed, activeSub!); - } - - return computed.value as T; - } - - Option getSignalValue(_AlienSignal signal) { - final value = signal.value; - if ((signal.flags & 16 /* Dirty */ ) != 0) { - if (signal.update()) { - final subs = signal.subs; - if (subs != null) shallowPropagate(subs); - } - } - - if (activeSub != null) link(signal, activeSub!); - return value; - } - - void setSignalValue(_AlienSignal signal, Option value) { - if (signal.value != (signal.value = value)) { - signal.flags = 17 /* Mutable | Dirty */; - final subs = signal.subs; - if (subs != null) { - propagate(subs); - if (batchDepth == 0) flush(); - } - } - } - - void stopEffect(alien.ReactiveNode effect) { - assert(effect is! _AlienSignal, 'Reactive node type not matched'); - var dep = effect.deps; - while (dep != null) { - dep = unlink(dep, effect); - } - - final sub = effect.subs; - if (sub != null) unlink(sub, effect); - effect.flags = 0 /* None */; - } - - void run(alien.ReactiveNode effect, int flags) { - if ((flags & 16 /* Dirty */ ) != 0 || - ((flags & 32 /* Pending */ ) != 0 && - effect.deps != null && - checkDirty(effect.deps!, effect))) { - final prevSub = setCurrentSub(effect); - startTracking(effect); - try { - (effect as _AlienEffect).run(); - } finally { - activeSub = prevSub; - endTracking(effect); - } - return; - } else if ((flags & 32 /* Pending */ ) != 0) { - effect.flags = flags & -33 /* ~Pending */; - } - var link = effect.deps; - while (link != null) { - final dep = link.dep; - final depFlags = dep.flags; - if ((depFlags & 64 /* Queued */ ) != 0) { - run(dep, dep.flags = depFlags & -65 /* ~Queued */); - } - link = link.nextDep; - } - } - - void flush() { - while (queuedEffects != null) { - final effect = queuedEffects!; - if ((queuedEffects = effect.nextEffect) != null) { - effect.nextEffect = null; - } else { - queuedEffectsTail = null; - } - - run(effect, effect.flags &= -65 /* ~Queued */); - } - } -} diff --git a/packages/solidart/lib/src/core/read_signal.dart b/packages/solidart/lib/src/core/read_signal.dart index 9f1b9cec..0edba55b 100644 --- a/packages/solidart/lib/src/core/read_signal.dart +++ b/packages/solidart/lib/src/core/read_signal.dart @@ -118,7 +118,7 @@ class ReadableSignal implements ReadSignal { @override bool get hasValue { - _reportObserved(); + _internalSignal.get(); return _hasValue; } @@ -133,11 +133,10 @@ class ReadableSignal implements ReadSignal { T get _value { if (_disposed) { return untracked( - () => reactiveSystem.getSignalValue(_internalSignal).unwrap(), + () => _internalSignal.get().unwrap(), ); } - _reportObserved(); - final value = reactiveSystem.getSignalValue(_internalSignal).unwrap(); + final value = _internalSignal.get().unwrap(); if (autoDispose) { _subs.clear(); @@ -174,7 +173,7 @@ class ReadableSignal implements ReadSignal { set _value(T newValue) { _untrackedPreviousValue = _untrackedValue; _untrackedValue = newValue; - reactiveSystem.setSignalValue(_internalSignal, Some(newValue)); + _internalSignal.set(Some(newValue)); } @override @@ -270,7 +269,7 @@ class ReadableSignal implements ReadSignal { // This will dispose the signal untracked(() { - reactiveSystem.getSignalValue(_internalSignal); + _internalSignal.get(); }); if (SolidartConfig.autoDispose) { @@ -321,12 +320,6 @@ class ReadableSignal implements ReadSignal { _onDisposeCallbacks.add(cb); } - void _reportObserved() { - if (reactiveSystem.activeSub != null) { - reactiveSystem.link(_internalSignal, reactiveSystem.activeSub!); - } - } - /// Forces a change notification even when the value /// hasn't substantially changed. /// @@ -336,21 +329,15 @@ class ReadableSignal implements ReadSignal { // ignore: comment_references /// use [reactiveSystem.setSignalValue] instead. void _reportChanged() { - _internalSignal.forceDirty = true; - _internalSignal.flags = 17 /* Mutable | Dirty */; - final subs = _internalSignal.subs; - if (subs != null) { - // coverage:ignore-start - reactiveSystem.propagate(subs); - if (reactiveSystem.batchDepth == 0) { - reactiveSystem.flush(); - } - // coverage:ignore-end - } + _internalSignal.set(Some(_untrackedValue)); } /// Indicates if the signal should update its value. bool shouldUpdate() { + if ((_internalSignal.flags & alien.ReactiveFlags.dirty) == + alien.ReactiveFlags.none) { + return false; + } return _internalSignal.update(); } diff --git a/packages/solidart/lib/src/core/untracked.dart b/packages/solidart/lib/src/core/untracked.dart index ed833142..290f32fe 100644 --- a/packages/solidart/lib/src/core/untracked.dart +++ b/packages/solidart/lib/src/core/untracked.dart @@ -5,10 +5,10 @@ part of 'core.dart'; /// This can be useful inside Effects or Observations to prevent a signal from /// being tracked. T untracked(T Function() callback) { - final prevSub = reactiveSystem.setCurrentSub(null); + final prevSub = alien_preset.setActiveSub(null); try { return callback(); } finally { - reactiveSystem.setCurrentSub(prevSub); + alien_preset.setActiveSub(prevSub); } } diff --git a/packages/solidart/pubspec.yaml b/packages/solidart/pubspec.yaml index 4a424839..6757d795 100644 --- a/packages/solidart/pubspec.yaml +++ b/packages/solidart/pubspec.yaml @@ -14,7 +14,7 @@ resolution: workspace dependencies: # we depend on the alien signals reactivity implementation because it's the fastest available right now (30/12/2024) - alien_signals: ^0.5.4 + alien_signals: ^2.0.1 collection: ^1.18.0 meta: ^1.11.0 From 10f733f17df5d966e1a9841b4d16478b39d77318 Mon Sep 17 00:00:00 2001 From: Seven Du <5564821+medz@users.noreply.github.com> Date: Sat, 6 Dec 2025 23:38:54 +0800 Subject: [PATCH 02/19] Migrate to alien_signals for reactive system management --- .../lib/src/widgets/signal_builder.dart | 11 +++++++---- packages/flutter_solidart/pubspec.yaml | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/flutter_solidart/lib/src/widgets/signal_builder.dart b/packages/flutter_solidart/lib/src/widgets/signal_builder.dart index 559d2b74..74b3bb62 100644 --- a/packages/flutter_solidart/lib/src/widgets/signal_builder.dart +++ b/packages/flutter_solidart/lib/src/widgets/signal_builder.dart @@ -1,5 +1,7 @@ // ignore_for_file: document_ignores +import 'package:alien_signals/preset.dart' as alien_preset; +import 'package:alien_signals/system.dart' as alien; import 'package:flutter/widgets.dart'; import 'package:meta/meta.dart'; import 'package:solidart/solidart.dart'; @@ -83,9 +85,9 @@ class _SignalBuilderElement extends StatelessElement { @override Widget build() { - final prevSub = reactiveSystem.activeSub; - // ignore: invalid_use_of_protected_member - final node = reactiveSystem.activeSub = effect.subscriber; + final prevSub = alien_preset.getActiveSub(); + final node = effect.subscriber; + alien_preset.setActiveSub(node); try { final built = super.build(); @@ -98,10 +100,11 @@ You can disable this check by setting `SolidartConfig.assertSignalBuilderWithout } // ignore: invalid_use_of_internal_member effect.setDependencies(node); + node.flags = alien.ReactiveFlags.watching; return built; } finally { - reactiveSystem.activeSub = prevSub; + alien_preset.setActiveSub(prevSub); } } } diff --git a/packages/flutter_solidart/pubspec.yaml b/packages/flutter_solidart/pubspec.yaml index 96bd8db7..6b5b5fe6 100644 --- a/packages/flutter_solidart/pubspec.yaml +++ b/packages/flutter_solidart/pubspec.yaml @@ -18,7 +18,7 @@ dependencies: flutter: sdk: flutter meta: ^1.11.0 - solidart: ^2.8.2 + solidart: ^2.8.3 dev_dependencies: disco: ^1.0.0 From 0b3795e097449279e7e6e32abb34fc3f35b2b7b4 Mon Sep 17 00:00:00 2001 From: Seven Du <5564821+medz@users.noreply.github.com> Date: Sat, 6 Dec 2025 23:51:30 +0800 Subject: [PATCH 03/19] Replace alien_signals imports with internal deps re-exports --- .../lib/src/widgets/signal_builder.dart | 12 ++++++------ packages/solidart/lib/deps/preset.dart | 1 + packages/solidart/lib/deps/system.dart | 1 + packages/solidart/lib/src/core/alien.dart | 14 +++++++------- packages/solidart/lib/src/core/batch.dart | 4 ++-- packages/solidart/lib/src/core/computed.dart | 8 ++++---- packages/solidart/lib/src/core/core.dart | 4 ++-- packages/solidart/lib/src/core/effect.dart | 14 +++++++------- .../solidart/lib/src/core/reactive_system.dart | 8 ++++---- packages/solidart/lib/src/core/read_signal.dart | 6 +++--- packages/solidart/lib/src/core/untracked.dart | 4 ++-- 11 files changed, 39 insertions(+), 37 deletions(-) create mode 100644 packages/solidart/lib/deps/preset.dart create mode 100644 packages/solidart/lib/deps/system.dart diff --git a/packages/flutter_solidart/lib/src/widgets/signal_builder.dart b/packages/flutter_solidart/lib/src/widgets/signal_builder.dart index 74b3bb62..f6f847c6 100644 --- a/packages/flutter_solidart/lib/src/widgets/signal_builder.dart +++ b/packages/flutter_solidart/lib/src/widgets/signal_builder.dart @@ -1,7 +1,7 @@ // ignore_for_file: document_ignores -import 'package:alien_signals/preset.dart' as alien_preset; -import 'package:alien_signals/system.dart' as alien; +import 'package:solidart/deps/preset.dart' as preset; +import 'package:solidart/deps/system.dart' as system; import 'package:flutter/widgets.dart'; import 'package:meta/meta.dart'; import 'package:solidart/solidart.dart'; @@ -85,9 +85,9 @@ class _SignalBuilderElement extends StatelessElement { @override Widget build() { - final prevSub = alien_preset.getActiveSub(); + final prevSub = preset.getActiveSub(); final node = effect.subscriber; - alien_preset.setActiveSub(node); + preset.setActiveSub(node); try { final built = super.build(); @@ -100,11 +100,11 @@ You can disable this check by setting `SolidartConfig.assertSignalBuilderWithout } // ignore: invalid_use_of_internal_member effect.setDependencies(node); - node.flags = alien.ReactiveFlags.watching; + node.flags = system.ReactiveFlags.watching; return built; } finally { - alien_preset.setActiveSub(prevSub); + preset.setActiveSub(prevSub); } } } diff --git a/packages/solidart/lib/deps/preset.dart b/packages/solidart/lib/deps/preset.dart new file mode 100644 index 00000000..484f2f55 --- /dev/null +++ b/packages/solidart/lib/deps/preset.dart @@ -0,0 +1 @@ +export 'package:alien_signals/preset.dart'; diff --git a/packages/solidart/lib/deps/system.dart b/packages/solidart/lib/deps/system.dart new file mode 100644 index 00000000..2b6b1a29 --- /dev/null +++ b/packages/solidart/lib/deps/system.dart @@ -0,0 +1 @@ +export 'package:alien_signals/system.dart'; diff --git a/packages/solidart/lib/src/core/alien.dart b/packages/solidart/lib/src/core/alien.dart index 9de7d4bd..4c0ce153 100644 --- a/packages/solidart/lib/src/core/alien.dart +++ b/packages/solidart/lib/src/core/alien.dart @@ -1,15 +1,15 @@ part of 'core.dart'; -class _AlienComputed extends alien_preset.ComputedNode { +class _AlienComputed extends preset.ComputedNode { _AlienComputed(this.parent, T Function(T? oldValue) getter) - : super(flags: alien.ReactiveFlags.none, getter: getter); + : super(flags: system.ReactiveFlags.none, getter: getter); final Computed parent; - void dispose() => alien_preset.stop(this); + void dispose() => preset.stop(this); } -class _AlienEffect extends alien_preset.EffectNode { +class _AlienEffect extends preset.EffectNode { _AlienEffect( this.parent, {required super.fn, @@ -20,13 +20,13 @@ class _AlienEffect extends alien_preset.EffectNode { final bool detach; final Effect parent; - void dispose() => alien_preset.stop(this); + void dispose() => preset.stop(this); } -class _AlienSignal extends alien_preset.SignalNode> { +class _AlienSignal extends preset.SignalNode> { _AlienSignal(this.parent, Option value) : super( - flags: alien.ReactiveFlags.mutable, + flags: system.ReactiveFlags.mutable, currentValue: value, pendingValue: value, ); diff --git a/packages/solidart/lib/src/core/batch.dart b/packages/solidart/lib/src/core/batch.dart index 37f03f58..8cd4fcfb 100644 --- a/packages/solidart/lib/src/core/batch.dart +++ b/packages/solidart/lib/src/core/batch.dart @@ -21,10 +21,10 @@ part of 'core.dart'; /// So when `x` changes, the effect is paused and you never see it printing: /// "x = 11, y = 20". T batch(T Function() fn) { - alien_preset.startBatch(); + preset.startBatch(); try { return fn(); } finally { - alien_preset.endBatch(); + preset.endBatch(); } } diff --git a/packages/solidart/lib/src/core/computed.dart b/packages/solidart/lib/src/core/computed.dart index 450fc314..b7473aa2 100644 --- a/packages/solidart/lib/src/core/computed.dart +++ b/packages/solidart/lib/src/core/computed.dart @@ -124,7 +124,7 @@ class Computed extends ReadSignal { @override bool get hasValue => true; - final _deps = {}; + final _deps = {}; @override void dispose() { @@ -152,10 +152,10 @@ class Computed extends ReadSignal { return _untrackedValue; } - if ((_internalComputed.flags & alien.ReactiveFlags.pending) != - alien.ReactiveFlags.none && + if ((_internalComputed.flags & system.ReactiveFlags.pending) != + system.ReactiveFlags.none && _internalComputed.deps == null) { - _internalComputed.flags &= ~alien.ReactiveFlags.pending; + _internalComputed.flags &= ~system.ReactiveFlags.pending; } final value = _internalComputed.get(); diff --git a/packages/solidart/lib/src/core/core.dart b/packages/solidart/lib/src/core/core.dart index e62105bb..b29fb58a 100644 --- a/packages/solidart/lib/src/core/core.dart +++ b/packages/solidart/lib/src/core/core.dart @@ -4,8 +4,8 @@ import 'dart:convert'; import 'dart:developer' as dev; import 'dart:math'; -import 'package:alien_signals/preset.dart' as alien_preset; -import 'package:alien_signals/system.dart' as alien; +import 'package:solidart/deps/preset.dart' as preset; +import 'package:solidart/deps/system.dart' as system; import 'package:collection/collection.dart'; import 'package:meta/meta.dart'; import 'package:solidart/src/extensions/until.dart'; diff --git a/packages/solidart/lib/src/core/effect.dart b/packages/solidart/lib/src/core/effect.dart index 1ca791f2..7a7bfe2c 100644 --- a/packages/solidart/lib/src/core/effect.dart +++ b/packages/solidart/lib/src/core/effect.dart @@ -166,7 +166,7 @@ class Effect implements ReactionInterface { this, fn: safeCallback(), detach: detach, - flags: alien.ReactiveFlags.watching | alien.ReactiveFlags.dirty, + flags: system.ReactiveFlags.watching | system.ReactiveFlags.dirty, ); } @@ -183,27 +183,27 @@ class Effect implements ReactionInterface { late final _AlienEffect _internalEffect; - final _deps = {}; + final _deps = {}; /// The subscriber of the effect, do not use it directly. @protected - alien.ReactiveNode get subscriber => _internalEffect; + system.ReactiveNode get subscriber => _internalEffect; @override bool get disposed => _disposed; /// Runs the effect, tracking any signal read during the execution. void run() { - final currentSub = alien_preset.getActiveSub(); + final currentSub = preset.getActiveSub(); if (!SolidartConfig.detachEffects && currentSub != null && (currentSub is! _AlienEffect || (!_internalEffect.detach && !currentSub.detach))) { - alien_preset.link(_internalEffect, currentSub, alien_preset.cycle); + preset.link(_internalEffect, currentSub, preset.cycle); } try { - alien_preset.run(_internalEffect); + preset.run(_internalEffect); } catch (_) { // The callback handles the error reporting, just rethrow to preserve // the behavior when no handler is provided. @@ -217,7 +217,7 @@ class Effect implements ReactionInterface { /// Sets the dependencies of the effect, do not use it directly. @internal - void setDependencies(alien.ReactiveNode node) { + void setDependencies(system.ReactiveNode node) { _deps ..clear() ..addAll(node.getDependencies()); diff --git a/packages/solidart/lib/src/core/reactive_system.dart b/packages/solidart/lib/src/core/reactive_system.dart index 17a9d075..fbc72b97 100644 --- a/packages/solidart/lib/src/core/reactive_system.dart +++ b/packages/solidart/lib/src/core/reactive_system.dart @@ -3,17 +3,17 @@ // Reactive flags map: https://github.com/medz/alien-signals-dart/blob/main/flags.md part of 'core.dart'; -extension MayDisposeDependencies on alien.ReactiveNode { - Iterable getDependencies() { +extension MayDisposeDependencies on system.ReactiveNode { + Iterable getDependencies() { var link = deps; - final foundDeps = {}; + final foundDeps = {}; for (; link != null; link = link.nextDep) { foundDeps.add(link.dep); } return foundDeps; } - void mayDisposeDependencies([Iterable? include]) { + void mayDisposeDependencies([Iterable? include]) { final dependencies = {...getDependencies(), ...?include}; for (final dep in dependencies) { switch (dep) { diff --git a/packages/solidart/lib/src/core/read_signal.dart b/packages/solidart/lib/src/core/read_signal.dart index 0edba55b..3fe73ba8 100644 --- a/packages/solidart/lib/src/core/read_signal.dart +++ b/packages/solidart/lib/src/core/read_signal.dart @@ -259,7 +259,7 @@ class ReadableSignal implements ReadSignal { @override int get listenerCount => _subs.length; - final _subs = {}; + final _subs = {}; @override void dispose() { @@ -334,8 +334,8 @@ class ReadableSignal implements ReadSignal { /// Indicates if the signal should update its value. bool shouldUpdate() { - if ((_internalSignal.flags & alien.ReactiveFlags.dirty) == - alien.ReactiveFlags.none) { + if ((_internalSignal.flags & system.ReactiveFlags.dirty) == + system.ReactiveFlags.none) { return false; } return _internalSignal.update(); diff --git a/packages/solidart/lib/src/core/untracked.dart b/packages/solidart/lib/src/core/untracked.dart index 290f32fe..e4b296c4 100644 --- a/packages/solidart/lib/src/core/untracked.dart +++ b/packages/solidart/lib/src/core/untracked.dart @@ -5,10 +5,10 @@ part of 'core.dart'; /// This can be useful inside Effects or Observations to prevent a signal from /// being tracked. T untracked(T Function() callback) { - final prevSub = alien_preset.setActiveSub(null); + final prevSub = preset.setActiveSub(null); try { return callback(); } finally { - alien_preset.setActiveSub(prevSub); + preset.setActiveSub(prevSub); } } From 1de6865a9465e72c7ea8225b211a2293d37b17ca Mon Sep 17 00:00:00 2001 From: Seven Du <5564821+medz@users.noreply.github.com> Date: Sun, 7 Dec 2025 00:23:40 +0800 Subject: [PATCH 04/19] Refactor Effect to extend preset.EffectNode directly - Remove _AlienEffect wrapper class - Simplify delayed callback logic - Improve dependency cleanup in ReadableSignal - Reorder imports consistently --- .../lib/src/widgets/signal_builder.dart | 6 +- packages/solidart/lib/src/core/alien.dart | 14 -- packages/solidart/lib/src/core/computed.dart | 23 ++-- packages/solidart/lib/src/core/core.dart | 4 +- packages/solidart/lib/src/core/effect.dart | 125 +++++++----------- .../lib/src/core/reactive_system.dart | 2 +- .../solidart/lib/src/core/read_signal.dart | 12 +- .../solidart/lib/src/core/signal_base.dart | 1 + packages/solidart/lib/src/core/untracked.dart | 2 +- 9 files changed, 74 insertions(+), 115 deletions(-) diff --git a/packages/flutter_solidart/lib/src/widgets/signal_builder.dart b/packages/flutter_solidart/lib/src/widgets/signal_builder.dart index f6f847c6..8abef8b3 100644 --- a/packages/flutter_solidart/lib/src/widgets/signal_builder.dart +++ b/packages/flutter_solidart/lib/src/widgets/signal_builder.dart @@ -1,9 +1,9 @@ // ignore_for_file: document_ignores -import 'package:solidart/deps/preset.dart' as preset; -import 'package:solidart/deps/system.dart' as system; import 'package:flutter/widgets.dart'; import 'package:meta/meta.dart'; +import 'package:solidart/deps/preset.dart' as preset; +import 'package:solidart/deps/system.dart' as system; import 'package:solidart/solidart.dart'; /// {@template signalbuilder} @@ -86,7 +86,7 @@ class _SignalBuilderElement extends StatelessElement { @override Widget build() { final prevSub = preset.getActiveSub(); - final node = effect.subscriber; + final node = effect; preset.setActiveSub(node); try { diff --git a/packages/solidart/lib/src/core/alien.dart b/packages/solidart/lib/src/core/alien.dart index 4c0ce153..efae3b1c 100644 --- a/packages/solidart/lib/src/core/alien.dart +++ b/packages/solidart/lib/src/core/alien.dart @@ -9,20 +9,6 @@ class _AlienComputed extends preset.ComputedNode { void dispose() => preset.stop(this); } -class _AlienEffect extends preset.EffectNode { - _AlienEffect( - this.parent, - {required super.fn, - bool? detach, - required super.flags, - }) : detach = detach ?? SolidartConfig.detachEffects; - - final bool detach; - final Effect parent; - - void dispose() => preset.stop(this); -} - class _AlienSignal extends preset.SignalNode> { _AlienSignal(this.parent, Option value) : super( diff --git a/packages/solidart/lib/src/core/computed.dart b/packages/solidart/lib/src/core/computed.dart index b7473aa2..c9448176 100644 --- a/packages/solidart/lib/src/core/computed.dart +++ b/packages/solidart/lib/src/core/computed.dart @@ -1,4 +1,5 @@ part of 'core.dart'; +// ignore_for_file: unused_element /// {@template computed} /// A special Signal that notifies only whenever the selected @@ -229,20 +230,6 @@ class Computed extends ReadSignal { _onDisposeCallbacks.add(cb); } - // coverage:ignore-start - /// Indicates if the [oldValue] and the [newValue] are equal - @override - bool _compare(T? oldValue, T? newValue) { - // skip if the value are equals - if (equals) { - return oldValue == newValue; - } - - // return the [comparator] result - return comparator(oldValue, newValue); - } - // coverage:ignore-end - /// Manually runs the computed to update its value. /// This is usually not necessary, as the computed will automatically /// update when its dependencies change. @@ -260,4 +247,12 @@ class Computed extends ReadSignal { value; return '''Computed<$T>(value: $untrackedValue, previousValue: $untrackedPreviousValue)'''; } + + @override + bool _compare(T? oldValue, T? newValue) { + if (equals) { + return oldValue == newValue; + } + return comparator(oldValue, newValue); + } } diff --git a/packages/solidart/lib/src/core/core.dart b/packages/solidart/lib/src/core/core.dart index b29fb58a..95e58b02 100644 --- a/packages/solidart/lib/src/core/core.dart +++ b/packages/solidart/lib/src/core/core.dart @@ -4,10 +4,10 @@ import 'dart:convert'; import 'dart:developer' as dev; import 'dart:math'; -import 'package:solidart/deps/preset.dart' as preset; -import 'package:solidart/deps/system.dart' as system; import 'package:collection/collection.dart'; import 'package:meta/meta.dart'; +import 'package:solidart/deps/preset.dart' as preset; +import 'package:solidart/deps/system.dart' as system; import 'package:solidart/src/extensions/until.dart'; import 'package:solidart/src/utils.dart'; diff --git a/packages/solidart/lib/src/core/effect.dart b/packages/solidart/lib/src/core/effect.dart index 7a7bfe2c..d90f33f1 100644 --- a/packages/solidart/lib/src/core/effect.dart +++ b/packages/solidart/lib/src/core/effect.dart @@ -64,7 +64,7 @@ abstract class ReactionInterface { /// /// > An effect is useless after it is disposed, you must not use it anymore. /// {@endtemplate} -class Effect implements ReactionInterface { +class Effect extends preset.EffectNode implements ReactionInterface { /// {@macro effect} factory Effect( void Function() callback, { @@ -93,48 +93,29 @@ class Effect implements ReactionInterface { try { final effectiveName = name ?? ReactiveName.nameFor('Effect'); final effectiveAutoDispose = autoDispose ?? SolidartConfig.autoDispose; - if (delay == null) { - effect = Effect._internal( - callback: () => callback(), - onError: onError, - name: effectiveName, - autoDispose: effectiveAutoDispose, - detach: detach, - ); - } else { - final scheduler = createDelayedScheduler(delay); - var isScheduled = false; - Timer? timer; - - effect = Effect._internal( - callback: () { - if (!isScheduled) { - isScheduled = true; - - // coverage:ignore-start - timer?.cancel(); - // coverage:ignore-end - timer = null; - - timer = scheduler(() { - isScheduled = false; - if (!effect.disposed) { - callback(); - } else { - // coverage:ignore-start - timer?.cancel(); - // coverage:ignore-end - } - }); - } - }, - onError: onError, - name: effectiveName, - autoDispose: effectiveAutoDispose, - detach: detach, - ); + Timer? timer; + void delayedCallback() { + // coverage:ignore-start + timer?.cancel(); + // coverage:ignore-end + timer = createDelayedScheduler(delay!)(() { + if (!effect.disposed) { + callback(); + } else { + // coverage:ignore-start + timer?.cancel(); + // coverage:ignore-end + } + }); } - return effect; + + return effect = Effect._internal( + callback: delay == null ? callback : delayedCallback, + onError: onError, + name: effectiveName, + autoDispose: effectiveAutoDispose, + detach: detach, + ); } finally { if (autorun ?? true) effect.run(); } @@ -147,28 +128,21 @@ class Effect implements ReactionInterface { required this.autoDispose, ErrorCallback? onError, bool? detach, - }) : _onError = onError { - VoidCallback safeCallback() { - return () { - try { - callback(); - } catch (e, s) { - if (_onError != null) { - _onError!.call(SolidartCaughtException(e, stackTrace: s)); - return; - } - rethrow; - } - }; - } - - _internalEffect = _AlienEffect( - this, - fn: safeCallback(), - detach: detach, - flags: system.ReactiveFlags.watching | system.ReactiveFlags.dirty, - ); - } + }) : detach = detach ?? SolidartConfig.detachEffects, + super( + fn: () { + try { + callback(); + } catch (e, s) { + if (onError != null) { + onError(SolidartCaughtException(e, stackTrace: s)); + return; + } + rethrow; + } + }, + flags: system.ReactiveFlags.watching | system.ReactiveFlags.dirty, + ); /// The name of the effect, useful for logging purposes. final String name; @@ -176,18 +150,19 @@ class Effect implements ReactionInterface { /// Whether to automatically dispose the effect (defaults to true). final bool autoDispose; - /// Optionally handle the error case - final ErrorCallback? _onError; + /// Whether this effect is detached from parent subscribers. + bool get isDetached => detach; bool _disposed = false; - late final _AlienEffect _internalEffect; + /// Whether the effect should detach from parent subscribers. + final bool detach; final _deps = {}; /// The subscriber of the effect, do not use it directly. @protected - system.ReactiveNode get subscriber => _internalEffect; + system.ReactiveNode get subscriber => this; @override bool get disposed => _disposed; @@ -197,17 +172,13 @@ class Effect implements ReactionInterface { final currentSub = preset.getActiveSub(); if (!SolidartConfig.detachEffects && currentSub != null && - (currentSub is! _AlienEffect || - (!_internalEffect.detach && !currentSub.detach))) { - preset.link(_internalEffect, currentSub, preset.cycle); + (currentSub is! preset.EffectNode || + !(detach || (currentSub is Effect && currentSub.detach)))) { + preset.link(this, currentSub, preset.cycle); } try { - preset.run(_internalEffect); - } catch (_) { - // The callback handles the error reporting, just rethrow to preserve - // the behavior when no handler is provided. - rethrow; + preset.run(this); } finally { if (SolidartConfig.autoDispose) { _mayDispose(); @@ -237,7 +208,7 @@ class Effect implements ReactionInterface { _disposed = true; final dependencies = {...subscriber.getDependencies(), ..._deps}; - _internalEffect.dispose(); + preset.stop(this); subscriber.mayDisposeDependencies(dependencies); } diff --git a/packages/solidart/lib/src/core/reactive_system.dart b/packages/solidart/lib/src/core/reactive_system.dart index fbc72b97..b6013ebd 100644 --- a/packages/solidart/lib/src/core/reactive_system.dart +++ b/packages/solidart/lib/src/core/reactive_system.dart @@ -1,4 +1,4 @@ -// ignore_for_file: public_member_api_docs, library_private_types_in_public_api +// ignore_for_file: public_member_api_docs // // Reactive flags map: https://github.com/medz/alien-signals-dart/blob/main/flags.md part of 'core.dart'; diff --git a/packages/solidart/lib/src/core/read_signal.dart b/packages/solidart/lib/src/core/read_signal.dart index 3fe73ba8..43b80201 100644 --- a/packages/solidart/lib/src/core/read_signal.dart +++ b/packages/solidart/lib/src/core/read_signal.dart @@ -274,15 +274,21 @@ class ReadableSignal implements ReadSignal { if (SolidartConfig.autoDispose) { for (final sub in _subs) { - if (sub is _AlienEffect) { + if (sub is Effect) { + sub.dispose(); + continue; + } + if (sub is preset.EffectNode) { if (sub.deps?.dep == _internalSignal) { sub.deps = null; } if (sub.depsTail?.dep == _internalSignal) { sub.depsTail = null; } - - sub.parent._mayDispose(); + // coverage:ignore-start + sub.mayDisposeDependencies(); + preset.stop(sub); + // coverage:ignore-end } if (sub is _AlienComputed) { // coverage:ignore-start diff --git a/packages/solidart/lib/src/core/signal_base.dart b/packages/solidart/lib/src/core/signal_base.dart index a0b7a598..df6334c7 100644 --- a/packages/solidart/lib/src/core/signal_base.dart +++ b/packages/solidart/lib/src/core/signal_base.dart @@ -105,5 +105,6 @@ abstract class SignalBase { void dispose(); /// Indicates if the [oldValue] and the [newValue] are equal + // ignore: unused_element bool _compare(T? oldValue, T? newValue); } diff --git a/packages/solidart/lib/src/core/untracked.dart b/packages/solidart/lib/src/core/untracked.dart index e4b296c4..37cf0517 100644 --- a/packages/solidart/lib/src/core/untracked.dart +++ b/packages/solidart/lib/src/core/untracked.dart @@ -5,7 +5,7 @@ part of 'core.dart'; /// This can be useful inside Effects or Observations to prevent a signal from /// being tracked. T untracked(T Function() callback) { - final prevSub = preset.setActiveSub(null); + final prevSub = preset.setActiveSub(); try { return callback(); } finally { From 9ad4ae79b84ca894d699a92077b29ac92457c0e4 Mon Sep 17 00:00:00 2001 From: Seven Du <5564821+medz@users.noreply.github.com> Date: Mon, 8 Dec 2025 06:41:34 +0800 Subject: [PATCH 05/19] Add v3 API implementation --- packages/solidart/lib/src/v3.dart | 351 ++++++++++++++++++++++++++++++ 1 file changed, 351 insertions(+) create mode 100644 packages/solidart/lib/src/v3.dart diff --git a/packages/solidart/lib/src/v3.dart b/packages/solidart/lib/src/v3.dart new file mode 100644 index 00000000..a10383bc --- /dev/null +++ b/packages/solidart/lib/src/v3.dart @@ -0,0 +1,351 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'dart:convert' as convert; +import 'dart:developer' as developer; + +import 'package:meta/meta.dart'; +import 'package:solidart/deps/preset.dart' as preset; +import 'package:solidart/deps/system.dart' as system; + +typedef ValueComparator = bool Function(T a, T b); +typedef VoidCallback = void Function(); + +T batch(T Function() callback) { + preset.startBatch(); + try { + return callback(); + } finally { + preset.endBatch(); + } +} + +T untracked(T Function() callback) { + final prevSub = preset.setActiveSub(); + try { + return callback(); + } finally { + preset.setActiveSub(prevSub); + } +} + +sealed class Option { + const Option(); + + T unwrap() => switch (this) { + Some(:final value) => value, + _ => throw StateError('Option is None'), + }; + + T? safeUnwrap() => switch (this) { + Some(:final value) => value, + _ => null, + }; +} + +final class Some extends Option { + final T value; + + const Some(this.value); +} + +final class None extends Option { + const None(); +} + +abstract class SolidartConfig { + static bool equals = false; + static bool autoDispose = true; + static bool devToolsEnabled = false; + static bool trackPreviousValue = true; + static bool useRefreshing = true; + static bool assertSignalBuilderWithoutDependencies = true; + static final observers = []; + static bool detachEffects = false; +} + +abstract class SolidartObserver { + // coverage:ignore-start + const SolidartObserver(); + // coverage:ignore-end + + void didCreateSignal(ReadonlySignal signal); + void didUpdateSignal(ReadonlySignal signal); + void didDisposeSignal(ReadonlySignal signal); +} + +mixin Disposable { + bool _disposed = false; + final List _onDisposeCallbacks = []; + + bool get disposed => _disposed; + + @mustCallSuper + void dispose() { + if (disposed) return; + try { + for (final callback in _onDisposeCallbacks) { + callback(); + } + } finally { + _disposed = true; + _onDisposeCallbacks.clear(); + } + } + + void onDispose(VoidCallback callback) => _onDisposeCallbacks.add(callback); +} + +abstract interface class ReadonlySignal implements Disposable { + String? get name; + bool get equals; + ValueComparator get comparator; + bool get autoDispose; + bool get trackInDevTools; + bool get trackPreviousValue; + int get listenerCount; + + T get value; + set value(T newValue); + bool get hasValue; + T get untrackedValue; + + T? get previousValue; + T? get untrackedPreviousValue; + bool get hasPreviousValue; + + T call(); +} + +class Signal extends preset.SignalNode> + with Disposable + implements ReadonlySignal { + @override + final bool autoDispose; + + @override + final ValueComparator comparator; + + @override + final bool equals; + + @override + final String? name; + + @override + final bool trackInDevTools; + + @override + final bool trackPreviousValue; + + @override + bool hasValue; + + @override + bool hasPreviousValue = false; + + T? _untrackedPreviousValue; + + Signal( + T initialValue, { + ValueComparator? comparator, + String? name, + bool? autoDispose, + bool? equals, + bool? trackInDevTools, + bool? trackPreviousValue, + }) : this._internal( + Some(initialValue), + hasValue: true, + comparator: comparator ?? identical, + name: name, + autoDispose: autoDispose, + equals: equals, + trackInDevTools: trackInDevTools, + trackPreviousValue: trackPreviousValue, + ); + + Signal.lazy({ + ValueComparator? comparator, + String? name, + bool? autoDispose, + bool? equals, + bool? trackInDevTools, + bool? trackPreviousValue, + }) : this._internal( + const None(), + hasValue: false, + comparator: comparator ?? identical, + name: name, + autoDispose: autoDispose, + equals: equals, + trackInDevTools: trackInDevTools, + trackPreviousValue: trackPreviousValue, + ); + + Signal._internal( + Option initialValue, { + required this.hasValue, + required this.comparator, + this.name, + bool? autoDispose, + bool? equals, + bool? trackInDevTools, + bool? trackPreviousValue, + }) : autoDispose = autoDispose ?? SolidartConfig.autoDispose, + equals = equals ?? SolidartConfig.equals, + trackInDevTools = trackInDevTools ?? SolidartConfig.devToolsEnabled, + trackPreviousValue = + trackPreviousValue ?? SolidartConfig.trackPreviousValue, + super( + flags: system.ReactiveFlags.mutable, + currentValue: initialValue, + pendingValue: initialValue, + ); + + @override + T? get untrackedPreviousValue { + if (hasPreviousValue) return _untrackedPreviousValue; + throw StateError('Signal has no previous value'); + } + + @override + T? get previousValue { + if (trackPreviousValue) get(); + return untrackedPreviousValue; + } + + @override + T get value { + if (hasValue) return get().unwrap(); + throw StateError('Signal has no value'); + } + + @override + set value(T newValue) { + final oldValue = pendingValue.safeUnwrap(); + if (!hasValue) { + hasValue = true; + set(Some(newValue)); + SolidartConfig.observers.emit(.created, this); + return; + } + if ((!equals && !comparator(oldValue as T, newValue)) || + (equals && oldValue != newValue)) { + return; + } + + final prevHasPreviousValue = hasPreviousValue; + _untrackedPreviousValue = oldValue; + if (!prevHasPreviousValue) { + hasPreviousValue = true; + SolidartConfig.observers.emit(.created, this); + } + + set(Some(newValue)); + if (prevHasPreviousValue) { + SolidartConfig.observers.emit(.updated, this); + } + } + + @override + T get untrackedValue => currentValue.unwrap(); + + @override + // TODO: implement listenerCount + int get listenerCount => throw UnimplementedError(); + + @override + T call() => value; + + ReadonlySignal toReadonlySignal() => this; +} + +// TODO +class Computed extends preset.ComputedNode + with Disposable + implements ReadonlySignal {} + +// TODO +class Effect extends preset.EffectNode with Disposable { + // 额外需要此设置! + final bool detach; +} + +enum _ObserverEvent { created, updated, disposed } + +final class _DevTools { + const _DevTools(); + + static void emit(_ObserverEvent event, ReadonlySignal signal) { + assert(() { + final kind = 'ext.solidart.signal.${event.name}'; + var value = signal.value; + var previousValue = signal.previousValue; + // TODO: + // if (signal is Resource) { + // value = signal._value.asReady?.value; + // previousValue = signal._previousValue?.asReady?.value; + // } + + developer.postEvent(kind, { + // '_id': signal._id, + 'name': signal.name, + 'value': trySerializeJson(value), + 'previousValue': trySerializeJson(previousValue), + 'hasPreviousValue': signal.hasPreviousValue, + 'type': switch (signal) { + // TODO: + // Resource() => 'Resource', + // ListSignal() => 'ListSignal', + // MapSignal() => 'MapSignal', + // SetSignal() => 'SetSignal', + // Signal() => 'Signal', + // Computed() => 'Computed', + ReadonlySignal() => 'ReadonlySignal', + }, + 'valueType': value.runtimeType.toString(), + if (signal.hasPreviousValue) + 'previousValueType': previousValue.runtimeType.toString(), + 'disposed': signal.disposed, + 'autoDispose': signal.autoDispose, + 'listenerCount': signal.listenerCount, + 'lastUpdate': DateTime.now().toIso8601String(), + }); + return true; + }(), 'Post devtools event assertion failed'); + } + + static Object? trySerializeJson(Object? value) { + try { + return convert.json.encode(value); + } catch (_) { + return switch (value) { + Iterable() => trySerializeJson(value.map(trySerializeJson).toList()), + Map() => trySerializeJson( + value.map( + (key, value) => + MapEntry(trySerializeJson(key), trySerializeJson(value)), + ), + ), + _ => convert.json.encode(value), + }; + } + } +} + +extension on Iterable { + void emit(_ObserverEvent event, ReadonlySignal signal) { + _DevTools.emit(event, signal); + for (final observer in this) { + try { + switch (event) { + case .created: + observer.didCreateSignal(signal); + case .updated: + observer.didUpdateSignal(signal); + case .disposed: + observer.didDisposeSignal(signal); + } + } catch (_) {} + } + } +} From 3d34fb9c6a889d10e7605043ec9ff34ac3a208fa Mon Sep 17 00:00:00 2001 From: Seven Du <5564821+medz@users.noreply.github.com> Date: Mon, 8 Dec 2025 06:41:59 +0800 Subject: [PATCH 06/19] Rename v3 module to next --- packages/solidart/lib/src/{v3.dart => next.dart} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/solidart/lib/src/{v3.dart => next.dart} (100%) diff --git a/packages/solidart/lib/src/v3.dart b/packages/solidart/lib/src/next.dart similarity index 100% rename from packages/solidart/lib/src/v3.dart rename to packages/solidart/lib/src/next.dart From dcb542ee12eec0de2bac90288990d93df4690914 Mon Sep 17 00:00:00 2001 From: Seven Du <5564821+medz@users.noreply.github.com> Date: Mon, 8 Dec 2025 18:21:55 +0800 Subject: [PATCH 07/19] WIP: Remove legacy next.dart and introduce simplified v3 API --- packages/solidart/lib/src/next.dart | 351 ---------------------------- packages/solidart/lib/src/v3.dart | 104 +++++++++ packages/solidart/pubspec.yaml | 2 +- 3 files changed, 105 insertions(+), 352 deletions(-) delete mode 100644 packages/solidart/lib/src/next.dart create mode 100644 packages/solidart/lib/src/v3.dart diff --git a/packages/solidart/lib/src/next.dart b/packages/solidart/lib/src/next.dart deleted file mode 100644 index a10383bc..00000000 --- a/packages/solidart/lib/src/next.dart +++ /dev/null @@ -1,351 +0,0 @@ -// ignore_for_file: public_member_api_docs, sort_constructors_first -import 'dart:convert' as convert; -import 'dart:developer' as developer; - -import 'package:meta/meta.dart'; -import 'package:solidart/deps/preset.dart' as preset; -import 'package:solidart/deps/system.dart' as system; - -typedef ValueComparator = bool Function(T a, T b); -typedef VoidCallback = void Function(); - -T batch(T Function() callback) { - preset.startBatch(); - try { - return callback(); - } finally { - preset.endBatch(); - } -} - -T untracked(T Function() callback) { - final prevSub = preset.setActiveSub(); - try { - return callback(); - } finally { - preset.setActiveSub(prevSub); - } -} - -sealed class Option { - const Option(); - - T unwrap() => switch (this) { - Some(:final value) => value, - _ => throw StateError('Option is None'), - }; - - T? safeUnwrap() => switch (this) { - Some(:final value) => value, - _ => null, - }; -} - -final class Some extends Option { - final T value; - - const Some(this.value); -} - -final class None extends Option { - const None(); -} - -abstract class SolidartConfig { - static bool equals = false; - static bool autoDispose = true; - static bool devToolsEnabled = false; - static bool trackPreviousValue = true; - static bool useRefreshing = true; - static bool assertSignalBuilderWithoutDependencies = true; - static final observers = []; - static bool detachEffects = false; -} - -abstract class SolidartObserver { - // coverage:ignore-start - const SolidartObserver(); - // coverage:ignore-end - - void didCreateSignal(ReadonlySignal signal); - void didUpdateSignal(ReadonlySignal signal); - void didDisposeSignal(ReadonlySignal signal); -} - -mixin Disposable { - bool _disposed = false; - final List _onDisposeCallbacks = []; - - bool get disposed => _disposed; - - @mustCallSuper - void dispose() { - if (disposed) return; - try { - for (final callback in _onDisposeCallbacks) { - callback(); - } - } finally { - _disposed = true; - _onDisposeCallbacks.clear(); - } - } - - void onDispose(VoidCallback callback) => _onDisposeCallbacks.add(callback); -} - -abstract interface class ReadonlySignal implements Disposable { - String? get name; - bool get equals; - ValueComparator get comparator; - bool get autoDispose; - bool get trackInDevTools; - bool get trackPreviousValue; - int get listenerCount; - - T get value; - set value(T newValue); - bool get hasValue; - T get untrackedValue; - - T? get previousValue; - T? get untrackedPreviousValue; - bool get hasPreviousValue; - - T call(); -} - -class Signal extends preset.SignalNode> - with Disposable - implements ReadonlySignal { - @override - final bool autoDispose; - - @override - final ValueComparator comparator; - - @override - final bool equals; - - @override - final String? name; - - @override - final bool trackInDevTools; - - @override - final bool trackPreviousValue; - - @override - bool hasValue; - - @override - bool hasPreviousValue = false; - - T? _untrackedPreviousValue; - - Signal( - T initialValue, { - ValueComparator? comparator, - String? name, - bool? autoDispose, - bool? equals, - bool? trackInDevTools, - bool? trackPreviousValue, - }) : this._internal( - Some(initialValue), - hasValue: true, - comparator: comparator ?? identical, - name: name, - autoDispose: autoDispose, - equals: equals, - trackInDevTools: trackInDevTools, - trackPreviousValue: trackPreviousValue, - ); - - Signal.lazy({ - ValueComparator? comparator, - String? name, - bool? autoDispose, - bool? equals, - bool? trackInDevTools, - bool? trackPreviousValue, - }) : this._internal( - const None(), - hasValue: false, - comparator: comparator ?? identical, - name: name, - autoDispose: autoDispose, - equals: equals, - trackInDevTools: trackInDevTools, - trackPreviousValue: trackPreviousValue, - ); - - Signal._internal( - Option initialValue, { - required this.hasValue, - required this.comparator, - this.name, - bool? autoDispose, - bool? equals, - bool? trackInDevTools, - bool? trackPreviousValue, - }) : autoDispose = autoDispose ?? SolidartConfig.autoDispose, - equals = equals ?? SolidartConfig.equals, - trackInDevTools = trackInDevTools ?? SolidartConfig.devToolsEnabled, - trackPreviousValue = - trackPreviousValue ?? SolidartConfig.trackPreviousValue, - super( - flags: system.ReactiveFlags.mutable, - currentValue: initialValue, - pendingValue: initialValue, - ); - - @override - T? get untrackedPreviousValue { - if (hasPreviousValue) return _untrackedPreviousValue; - throw StateError('Signal has no previous value'); - } - - @override - T? get previousValue { - if (trackPreviousValue) get(); - return untrackedPreviousValue; - } - - @override - T get value { - if (hasValue) return get().unwrap(); - throw StateError('Signal has no value'); - } - - @override - set value(T newValue) { - final oldValue = pendingValue.safeUnwrap(); - if (!hasValue) { - hasValue = true; - set(Some(newValue)); - SolidartConfig.observers.emit(.created, this); - return; - } - if ((!equals && !comparator(oldValue as T, newValue)) || - (equals && oldValue != newValue)) { - return; - } - - final prevHasPreviousValue = hasPreviousValue; - _untrackedPreviousValue = oldValue; - if (!prevHasPreviousValue) { - hasPreviousValue = true; - SolidartConfig.observers.emit(.created, this); - } - - set(Some(newValue)); - if (prevHasPreviousValue) { - SolidartConfig.observers.emit(.updated, this); - } - } - - @override - T get untrackedValue => currentValue.unwrap(); - - @override - // TODO: implement listenerCount - int get listenerCount => throw UnimplementedError(); - - @override - T call() => value; - - ReadonlySignal toReadonlySignal() => this; -} - -// TODO -class Computed extends preset.ComputedNode - with Disposable - implements ReadonlySignal {} - -// TODO -class Effect extends preset.EffectNode with Disposable { - // 额外需要此设置! - final bool detach; -} - -enum _ObserverEvent { created, updated, disposed } - -final class _DevTools { - const _DevTools(); - - static void emit(_ObserverEvent event, ReadonlySignal signal) { - assert(() { - final kind = 'ext.solidart.signal.${event.name}'; - var value = signal.value; - var previousValue = signal.previousValue; - // TODO: - // if (signal is Resource) { - // value = signal._value.asReady?.value; - // previousValue = signal._previousValue?.asReady?.value; - // } - - developer.postEvent(kind, { - // '_id': signal._id, - 'name': signal.name, - 'value': trySerializeJson(value), - 'previousValue': trySerializeJson(previousValue), - 'hasPreviousValue': signal.hasPreviousValue, - 'type': switch (signal) { - // TODO: - // Resource() => 'Resource', - // ListSignal() => 'ListSignal', - // MapSignal() => 'MapSignal', - // SetSignal() => 'SetSignal', - // Signal() => 'Signal', - // Computed() => 'Computed', - ReadonlySignal() => 'ReadonlySignal', - }, - 'valueType': value.runtimeType.toString(), - if (signal.hasPreviousValue) - 'previousValueType': previousValue.runtimeType.toString(), - 'disposed': signal.disposed, - 'autoDispose': signal.autoDispose, - 'listenerCount': signal.listenerCount, - 'lastUpdate': DateTime.now().toIso8601String(), - }); - return true; - }(), 'Post devtools event assertion failed'); - } - - static Object? trySerializeJson(Object? value) { - try { - return convert.json.encode(value); - } catch (_) { - return switch (value) { - Iterable() => trySerializeJson(value.map(trySerializeJson).toList()), - Map() => trySerializeJson( - value.map( - (key, value) => - MapEntry(trySerializeJson(key), trySerializeJson(value)), - ), - ), - _ => convert.json.encode(value), - }; - } - } -} - -extension on Iterable { - void emit(_ObserverEvent event, ReadonlySignal signal) { - _DevTools.emit(event, signal); - for (final observer in this) { - try { - switch (event) { - case .created: - observer.didCreateSignal(signal); - case .updated: - observer.didUpdateSignal(signal); - case .disposed: - observer.didDisposeSignal(signal); - } - } catch (_) {} - } - } -} diff --git a/packages/solidart/lib/src/v3.dart b/packages/solidart/lib/src/v3.dart new file mode 100644 index 00000000..951988eb --- /dev/null +++ b/packages/solidart/lib/src/v3.dart @@ -0,0 +1,104 @@ +// ignore_for_file: public_member_api_docs + +import 'package:solidart/deps/preset.dart' as preset; +import 'package:solidart/deps/system.dart' as system; + +typedef ValueGetter = T Function(); +typedef VoidCallback = ValueGetter; + +sealed class Option { + const Option(); + + T unwrap() => switch (this) { + Some(:final value) => value, + _ => throw StateError('Option is None'), + }; + + T? safeUnwrap() => switch (this) { + Some(:final value) => value, + _ => null, + }; +} + +final class Some extends Option { + const Some(this.value); + + final T value; +} + +final class None extends Option { + const None(); +} + +/// Maybe rename to `ReadSignal` ? +/// CC @nank1ro +abstract interface class ReadonlySignal implements system.ReactiveNode { + T get value; +} + +class Signal extends preset.SignalNode> + implements ReadonlySignal { + Signal(T initialValue) + : super( + flags: system.ReactiveFlags.mutable, + currentValue: Some(initialValue), + pendingValue: const None(), + ) { + pendingValue = currentValue; + } + + Signal._internal(Option initialValue) + : super( + flags: system.ReactiveFlags.mutable, + currentValue: initialValue, + pendingValue: initialValue, + ); + + factory Signal.lazy() = LazySignal; + + @override + T get value => super.get().unwrap(); + + set value(T newValue) => set(Some(newValue)); +} + +class LazySignal extends Signal { + LazySignal() : super._internal(const None()); + + @override + T get value { + if (currentValue is None) { + throw StateError( + 'LazySignal is not initialized, Please call `.value` first.', + ); + } + + return super.value; + } +} + +class Computed extends preset.ComputedNode implements ReadonlySignal { + Computed(ValueGetter getter) + : super(flags: system.ReactiveFlags.none, getter: (_) => getter()); + + @override + T get value => super.get(); +} + +class Effect extends preset.EffectNode { + Effect(VoidCallback callback) + : super( + fn: callback, + flags: + system.ReactiveFlags.watching | system.ReactiveFlags.recursedCheck, + ) { + final prevSub = preset.setActiveSub(this); + if (prevSub != null) preset.link(this, prevSub, 0); + try { + callback(); + } finally { + preset.activeSub = prevSub; + flags &= ~system.ReactiveFlags.recursedCheck; + } + } +} diff --git a/packages/solidart/pubspec.yaml b/packages/solidart/pubspec.yaml index 6757d795..4e65c5a3 100644 --- a/packages/solidart/pubspec.yaml +++ b/packages/solidart/pubspec.yaml @@ -14,7 +14,7 @@ resolution: workspace dependencies: # we depend on the alien signals reactivity implementation because it's the fastest available right now (30/12/2024) - alien_signals: ^2.0.1 + alien_signals: ^2.1.0 collection: ^1.18.0 meta: ^1.11.0 From 0095e25efbf5a3584ee58f60cef5cf80af3144a0 Mon Sep 17 00:00:00 2001 From: Seven Du <5564821+medz@users.noreply.github.com> Date: Mon, 8 Dec 2025 18:23:19 +0800 Subject: [PATCH 08/19] legacy: Rename update method to didUpdate in internal signals --- packages/solidart/lib/src/core/computed.dart | 2 +- packages/solidart/lib/src/core/read_signal.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/solidart/lib/src/core/computed.dart b/packages/solidart/lib/src/core/computed.dart index c9448176..41afb00e 100644 --- a/packages/solidart/lib/src/core/computed.dart +++ b/packages/solidart/lib/src/core/computed.dart @@ -236,7 +236,7 @@ class Computed extends ReadSignal { /// However, in some cases, you may want to force an update. void run() { if (_disposed) return; - _internalComputed.update(); + _internalComputed.didUpdate(); } @override diff --git a/packages/solidart/lib/src/core/read_signal.dart b/packages/solidart/lib/src/core/read_signal.dart index 43b80201..ac960c77 100644 --- a/packages/solidart/lib/src/core/read_signal.dart +++ b/packages/solidart/lib/src/core/read_signal.dart @@ -344,7 +344,7 @@ class ReadableSignal implements ReadSignal { system.ReactiveFlags.none) { return false; } - return _internalSignal.update(); + return _internalSignal.didUpdate(); } @override From 109ba0d97737cf3357656b76963cf4e5947d2e45 Mon Sep 17 00:00:00 2001 From: Seven Du <5564821+medz@users.noreply.github.com> Date: Mon, 8 Dec 2025 18:48:34 +0800 Subject: [PATCH 09/19] Add Disposable interface and mixin for reactive primitives Introduce a `Disposable` interface and `DisponsableMixin` (note: typo in name) to provide a common disposal mechanism for `Signal`, `Computed`, and `Effect`. This allows registering cleanup callbacks and ensures proper resource disposal. --- packages/solidart/lib/src/v3.dart | 53 +++++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/packages/solidart/lib/src/v3.dart b/packages/solidart/lib/src/v3.dart index 951988eb..c58365fa 100644 --- a/packages/solidart/lib/src/v3.dart +++ b/packages/solidart/lib/src/v3.dart @@ -1,5 +1,6 @@ // ignore_for_file: public_member_api_docs +import 'package:meta/meta.dart'; import 'package:solidart/deps/preset.dart' as preset; import 'package:solidart/deps/system.dart' as system; @@ -30,13 +31,27 @@ final class None extends Option { const None(); } +abstract class Configuration { + String? get name; + bool get autoDispose; +} + +abstract class Disposable { + bool get isDisposed; + + void onDispose(VoidCallback callback); + void dispose(); +} + /// Maybe rename to `ReadSignal` ? /// CC @nank1ro -abstract interface class ReadonlySignal implements system.ReactiveNode { +abstract interface class ReadonlySignal + implements system.ReactiveNode, Disposable { T get value; } class Signal extends preset.SignalNode> + with DisponsableMixin implements ReadonlySignal { Signal(T initialValue) : super( @@ -77,7 +92,9 @@ class LazySignal extends Signal { } } -class Computed extends preset.ComputedNode implements ReadonlySignal { +class Computed extends preset.ComputedNode + with DisponsableMixin + implements ReadonlySignal { Computed(ValueGetter getter) : super(flags: system.ReactiveFlags.none, getter: (_) => getter()); @@ -85,7 +102,9 @@ class Computed extends preset.ComputedNode implements ReadonlySignal { T get value => super.get(); } -class Effect extends preset.EffectNode { +class Effect extends preset.EffectNode + with DisponsableMixin + implements Disposable { Effect(VoidCallback callback) : super( fn: callback, @@ -102,3 +121,31 @@ class Effect extends preset.EffectNode { } } } + +mixin DisponsableMixin implements Disposable { + @internal + late final cleanups = []; + + @override + bool isDisposed = false; + + @mustCallSuper + @override + void onDispose(VoidCallback callback) { + cleanups.add(callback); + } + + @mustCallSuper + @override + void dispose() { + if (isDisposed) return; + isDisposed = true; + try { + for (final callback in cleanups) { + callback(); + } + } finally { + cleanups.clear(); + } + } +} From 76d6bc48f593d344f3a8028a14ca3f3cdf1743d6 Mon Sep 17 00:00:00 2001 From: Seven Du <5564821+medz@users.noreply.github.com> Date: Mon, 8 Dec 2025 18:48:42 +0800 Subject: [PATCH 10/19] Add v3.dart as a re-export entry point --- packages/solidart/lib/v3.dart | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 packages/solidart/lib/v3.dart diff --git a/packages/solidart/lib/v3.dart b/packages/solidart/lib/v3.dart new file mode 100644 index 00000000..470b767a --- /dev/null +++ b/packages/solidart/lib/v3.dart @@ -0,0 +1,3 @@ +// TODO: rename the v3.dart to solidart.dart filename. + +export 'src/v3.dart' show Computed, Effect, LazySignal, ReadonlySignal, Signal; From 7693e13e02a35ed13a25207f8127dbdd52dd45eb Mon Sep 17 00:00:00 2001 From: Seven Du <5564821+medz@users.noreply.github.com> Date: Mon, 8 Dec 2025 18:59:18 +0800 Subject: [PATCH 11/19] Add Configuration interface and global autoDispose setting - Add SolidartConfig with static autoDispose flag - Make ReadonlySignal implement Configuration - Add name and autoDispose parameters to Signal, LazySignal, and Computed - Export Configuration and Disposable from the main library --- packages/solidart/lib/src/v3.dart | 56 ++++++++++++++++++++----------- packages/solidart/lib/v3.dart | 10 +++++- 2 files changed, 46 insertions(+), 20 deletions(-) diff --git a/packages/solidart/lib/src/v3.dart b/packages/solidart/lib/src/v3.dart index c58365fa..b07bc840 100644 --- a/packages/solidart/lib/src/v3.dart +++ b/packages/solidart/lib/src/v3.dart @@ -31,6 +31,12 @@ final class None extends Option { const None(); } +final class SolidartConifg { + const SolidartConifg._(); + + static bool autoDispose = false; +} + abstract class Configuration { String? get name; bool get autoDispose; @@ -46,30 +52,34 @@ abstract class Disposable { /// Maybe rename to `ReadSignal` ? /// CC @nank1ro abstract interface class ReadonlySignal - implements system.ReactiveNode, Disposable { + implements system.ReactiveNode, Disposable, Configuration { T get value; } class Signal extends preset.SignalNode> with DisponsableMixin implements ReadonlySignal { - Signal(T initialValue) - : super( - flags: system.ReactiveFlags.mutable, - currentValue: Some(initialValue), - pendingValue: const None(), - ) { - pendingValue = currentValue; - } + Signal(T initialValue, {String? name, bool? autoDispose}) + : this._internal(Some(initialValue), name: name, autoDispose: autoDispose); + + Signal._internal( + Option initialValue, { + this.name, + bool? autoDispose, + }) : autoDispose = autoDispose ?? SolidartConifg.autoDispose, + super( + flags: system.ReactiveFlags.mutable, + currentValue: initialValue, + pendingValue: initialValue, + ); + + factory Signal.lazy({String? name, bool? autoDispose}) = LazySignal; - Signal._internal(Option initialValue) - : super( - flags: system.ReactiveFlags.mutable, - currentValue: initialValue, - pendingValue: initialValue, - ); + @override + final bool autoDispose; - factory Signal.lazy() = LazySignal; + @override + final String? name; @override T get value => super.get().unwrap(); @@ -78,7 +88,8 @@ class Signal extends preset.SignalNode> } class LazySignal extends Signal { - LazySignal() : super._internal(const None()); + LazySignal({String? name, bool? autoDispose}) + : super._internal(const None(), name: name, autoDispose: autoDispose); @override T get value { @@ -95,8 +106,15 @@ class LazySignal extends Signal { class Computed extends preset.ComputedNode with DisponsableMixin implements ReadonlySignal { - Computed(ValueGetter getter) - : super(flags: system.ReactiveFlags.none, getter: (_) => getter()); + Computed(ValueGetter getter, {this.name, bool? autoDispose}) + : autoDispose = autoDispose ?? SolidartConifg.autoDispose, + super(flags: system.ReactiveFlags.none, getter: (_) => getter()); + + @override + final bool autoDispose; + + @override + final String? name; @override T get value => super.get(); diff --git a/packages/solidart/lib/v3.dart b/packages/solidart/lib/v3.dart index 470b767a..286642cc 100644 --- a/packages/solidart/lib/v3.dart +++ b/packages/solidart/lib/v3.dart @@ -1,3 +1,11 @@ // TODO: rename the v3.dart to solidart.dart filename. -export 'src/v3.dart' show Computed, Effect, LazySignal, ReadonlySignal, Signal; +export 'src/v3.dart' + show + Computed, + Configuration, + Disposable, + Effect, + LazySignal, + ReadonlySignal, + Signal; From 6a71777c7fda428fd3497dbf12bf8c153595c008 Mon Sep 17 00:00:00 2001 From: Seven Du <5564821+medz@users.noreply.github.com> Date: Mon, 8 Dec 2025 19:03:06 +0800 Subject: [PATCH 12/19] Add name and autoDispose parameters to Effect constructor The Effect class now implements Configuration and accepts optional name and autoDispose parameters, defaulting autoDispose to the global SolidartConfig.autoDispose value when not provided. --- packages/solidart/lib/src/v3.dart | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/solidart/lib/src/v3.dart b/packages/solidart/lib/src/v3.dart index b07bc840..3cc35ca0 100644 --- a/packages/solidart/lib/src/v3.dart +++ b/packages/solidart/lib/src/v3.dart @@ -122,9 +122,10 @@ class Computed extends preset.ComputedNode class Effect extends preset.EffectNode with DisponsableMixin - implements Disposable { - Effect(VoidCallback callback) - : super( + implements Disposable, Configuration { + Effect(VoidCallback callback, {this.name, bool? autoDispose}) + : autoDispose = autoDispose ?? SolidartConifg.autoDispose, + super( fn: callback, flags: system.ReactiveFlags.watching | system.ReactiveFlags.recursedCheck, @@ -138,6 +139,12 @@ class Effect extends preset.EffectNode flags &= ~system.ReactiveFlags.recursedCheck; } } + + @override + final bool autoDispose; + + @override + final String? name; } mixin DisponsableMixin implements Disposable { From f151329f53d6b241411c788671ba4efe197f3784 Mon Sep 17 00:00:00 2001 From: Seven Du <5564821+medz@users.noreply.github.com> Date: Mon, 8 Dec 2025 19:10:30 +0800 Subject: [PATCH 13/19] Replace signal name with unique identifier Introduce an `Identifier` class that provides a unique integer value alongside an optional name. This replaces the simple `name` property on signals, computed values, and effects to ensure each reactive primitive has a distinct identifier for debugging and tracking. --- packages/solidart/lib/src/v3.dart | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/packages/solidart/lib/src/v3.dart b/packages/solidart/lib/src/v3.dart index 3cc35ca0..81a9b44f 100644 --- a/packages/solidart/lib/src/v3.dart +++ b/packages/solidart/lib/src/v3.dart @@ -37,8 +37,16 @@ final class SolidartConifg { static bool autoDispose = false; } +class Identifier { + Identifier._(this.name) : value = _counter++; + static int _counter = 0; + + final String? name; + final int value; +} + abstract class Configuration { - String? get name; + Identifier get identifier; bool get autoDispose; } @@ -59,14 +67,15 @@ abstract interface class ReadonlySignal class Signal extends preset.SignalNode> with DisponsableMixin implements ReadonlySignal { - Signal(T initialValue, {String? name, bool? autoDispose}) - : this._internal(Some(initialValue), name: name, autoDispose: autoDispose); + Signal(T initialValue, {bool? autoDispose, String? name}) + : this._internal(Some(initialValue), autoDispose: autoDispose, name: name); Signal._internal( Option initialValue, { - this.name, + String? name, bool? autoDispose, }) : autoDispose = autoDispose ?? SolidartConifg.autoDispose, + identifier = Identifier._(name), super( flags: system.ReactiveFlags.mutable, currentValue: initialValue, @@ -79,7 +88,7 @@ class Signal extends preset.SignalNode> final bool autoDispose; @override - final String? name; + final Identifier identifier; @override T get value => super.get().unwrap(); @@ -106,15 +115,16 @@ class LazySignal extends Signal { class Computed extends preset.ComputedNode with DisponsableMixin implements ReadonlySignal { - Computed(ValueGetter getter, {this.name, bool? autoDispose}) + Computed(ValueGetter getter, {bool? autoDispose, String? name}) : autoDispose = autoDispose ?? SolidartConifg.autoDispose, + identifier = Identifier._(name), super(flags: system.ReactiveFlags.none, getter: (_) => getter()); @override final bool autoDispose; @override - final String? name; + final Identifier identifier; @override T get value => super.get(); @@ -123,8 +133,9 @@ class Computed extends preset.ComputedNode class Effect extends preset.EffectNode with DisponsableMixin implements Disposable, Configuration { - Effect(VoidCallback callback, {this.name, bool? autoDispose}) + Effect(VoidCallback callback, {bool? autoDispose, String? name}) : autoDispose = autoDispose ?? SolidartConifg.autoDispose, + identifier = Identifier._(name), super( fn: callback, flags: @@ -144,7 +155,7 @@ class Effect extends preset.EffectNode final bool autoDispose; @override - final String? name; + final Identifier identifier; } mixin DisponsableMixin implements Disposable { From 915d958a9e6b7b23a3159c7ee6dd18ff58532418 Mon Sep 17 00:00:00 2001 From: Seven Du <5564821+medz@users.noreply.github.com> Date: Mon, 8 Dec 2025 19:10:36 +0800 Subject: [PATCH 14/19] Update import path to use solidart/v3.dart --- packages/solidart/example/main.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/solidart/example/main.dart b/packages/solidart/example/main.dart index 7b76a506..d38e9bb6 100644 --- a/packages/solidart/example/main.dart +++ b/packages/solidart/example/main.dart @@ -1,6 +1,6 @@ // ignore_for_file: avoid_print -import 'package:solidart/solidart.dart'; +import 'package:solidart/v3.dart'; void main() { final count = Signal(0); From 02ac378c8d3c88b0adff1be7636efc9fc3432fb2 Mon Sep 17 00:00:00 2001 From: Seven Du <5564821+medz@users.noreply.github.com> Date: Mon, 8 Dec 2025 19:14:08 +0800 Subject: [PATCH 15/19] Export SolidartConifg and remove unused exports from v3 --- packages/solidart/lib/v3.dart | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/packages/solidart/lib/v3.dart b/packages/solidart/lib/v3.dart index 286642cc..1a95b7dd 100644 --- a/packages/solidart/lib/v3.dart +++ b/packages/solidart/lib/v3.dart @@ -1,11 +1,4 @@ // TODO: rename the v3.dart to solidart.dart filename. export 'src/v3.dart' - show - Computed, - Configuration, - Disposable, - Effect, - LazySignal, - ReadonlySignal, - Signal; + show Computed, Effect, LazySignal, ReadonlySignal, Signal, SolidartConifg; From bf8c73494dad0b659ee85ae7df796ab03e10e351 Mon Sep 17 00:00:00 2001 From: Seven Du <5564821+medz@users.noreply.github.com> Date: Mon, 8 Dec 2025 19:14:13 +0800 Subject: [PATCH 16/19] Add advanced exports for v3 features --- packages/solidart/lib/advanced.dart | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 packages/solidart/lib/advanced.dart diff --git a/packages/solidart/lib/advanced.dart b/packages/solidart/lib/advanced.dart new file mode 100644 index 00000000..18878c08 --- /dev/null +++ b/packages/solidart/lib/advanced.dart @@ -0,0 +1,9 @@ +export 'src/v3.dart' + show + Configuration, + DisponsableMixin, + Disposable, + Identifier, + None, + Option, + Some; From 48bddcb38058e06596142e8cfdf18d79bdf79a1b Mon Sep 17 00:00:00 2001 From: Seven Du <5564821+medz@users.noreply.github.com> Date: Mon, 8 Dec 2025 19:17:01 +0800 Subject: [PATCH 17/19] Add isInitialized getter to LazySignal Check isInitialized in value getter to throw a clearer error. The error message now instructs to set `.value = `. --- packages/solidart/lib/src/v3.dart | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/solidart/lib/src/v3.dart b/packages/solidart/lib/src/v3.dart index 81a9b44f..4a24afdd 100644 --- a/packages/solidart/lib/src/v3.dart +++ b/packages/solidart/lib/src/v3.dart @@ -100,15 +100,17 @@ class LazySignal extends Signal { LazySignal({String? name, bool? autoDispose}) : super._internal(const None(), name: name, autoDispose: autoDispose); + bool get isInitialized => currentValue is Some; + @override T get value { - if (currentValue is None) { - throw StateError( - 'LazySignal is not initialized, Please call `.value` first.', - ); + if (isInitialized) { + return super.value; } - return super.value; + throw StateError( + 'LazySignal is not initialized, Please call `.value = ` first.', + ); } } From 74920358dba0ac0b736e04d36842b2bfad051cbe Mon Sep 17 00:00:00 2001 From: Seven Du <5564821+medz@users.noreply.github.com> Date: Mon, 8 Dec 2025 19:19:07 +0800 Subject: [PATCH 18/19] Add toReadonly method to Signal class --- packages/solidart/lib/src/v3.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/solidart/lib/src/v3.dart b/packages/solidart/lib/src/v3.dart index 4a24afdd..9820afd8 100644 --- a/packages/solidart/lib/src/v3.dart +++ b/packages/solidart/lib/src/v3.dart @@ -94,6 +94,8 @@ class Signal extends preset.SignalNode> T get value => super.get().unwrap(); set value(T newValue) => set(Some(newValue)); + + ReadonlySignal toReadonly() => this; } class LazySignal extends Signal { From 8290c557bf8dcc620cc34eb785034aa691d0216d Mon Sep 17 00:00:00 2001 From: Seven Du <5564821+medz@users.noreply.github.com> Date: Mon, 8 Dec 2025 19:26:19 +0800 Subject: [PATCH 19/19] Update TODO comments for naming discussions --- packages/solidart/lib/src/v3.dart | 5 +++-- packages/solidart/lib/v3.dart | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/solidart/lib/src/v3.dart b/packages/solidart/lib/src/v3.dart index 9820afd8..2104b1ab 100644 --- a/packages/solidart/lib/src/v3.dart +++ b/packages/solidart/lib/src/v3.dart @@ -57,8 +57,7 @@ abstract class Disposable { void dispose(); } -/// Maybe rename to `ReadSignal` ? -/// CC @nank1ro +// TODO(nank1ro): Maybe rename to `ReadSignal`? medz: I still recommend `ReadonlySignal` because it is semantically clearer., https://github.com/nank1ro/solidart/pull/166#issuecomment-3623175977 abstract interface class ReadonlySignal implements system.ReactiveNode, Disposable, Configuration { T get value; @@ -95,6 +94,8 @@ class Signal extends preset.SignalNode> set value(T newValue) => set(Some(newValue)); + // TODO(nank1ro): See ReadonlySignal TODO, If `ReadonlySignal` rename + // to `ReadSignal`, the `.toReadonly` method should be rename? ReadonlySignal toReadonly() => this; } diff --git a/packages/solidart/lib/v3.dart b/packages/solidart/lib/v3.dart index 1a95b7dd..66233ebb 100644 --- a/packages/solidart/lib/v3.dart +++ b/packages/solidart/lib/v3.dart @@ -1,4 +1,4 @@ -// TODO: rename the v3.dart to solidart.dart filename. +// TODO(medz): rename the v3.dart to solidart.dart filename. export 'src/v3.dart' show Computed, Effect, LazySignal, ReadonlySignal, Signal, SolidartConifg;