@fictjs/runtime 0.0.9 → 0.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/signal.ts CHANGED
@@ -96,6 +96,10 @@ export interface EffectNode extends BaseNode {
96
96
  deps: Link | undefined
97
97
  /** Last dependency link */
98
98
  depsTail: Link | undefined
99
+ /** Optional cleanup runner to be called before checkDirty */
100
+ runCleanup?: () => void
101
+ /** Devtools ID */
102
+ __id?: number | undefined
99
103
  }
100
104
 
101
105
  /**
@@ -202,6 +206,8 @@ const enqueueMicrotask =
202
206
  : (fn: () => void) => {
203
207
  Promise.resolve().then(fn)
204
208
  }
209
+ // Flag to indicate cleanup is running - signal reads should return currentValue without updating
210
+ let inCleanup = false
205
211
  export const ReactiveFlags = {
206
212
  None: 0,
207
213
  Mutable,
@@ -735,7 +741,16 @@ function updateComputed<T>(c: ComputedNode<T>): boolean {
735
741
  */
736
742
  function runEffect(e: EffectNode): void {
737
743
  const flags = e.flags
738
- if (flags & Dirty || (flags & Pending && e.deps && checkDirty(e.deps, e))) {
744
+ // Run cleanup BEFORE checkDirty so cleanup sees previous signal values
745
+ if (flags & Dirty) {
746
+ if (e.runCleanup) {
747
+ inCleanup = true
748
+ try {
749
+ e.runCleanup()
750
+ } finally {
751
+ inCleanup = false
752
+ }
753
+ }
739
754
  ++cycle
740
755
  effectRunDevtools(e)
741
756
  e.depsTail = undefined
@@ -752,6 +767,36 @@ function runEffect(e: EffectNode): void {
752
767
  e.flags = Watching
753
768
  throw err
754
769
  }
770
+ } else if (flags & Pending && e.deps) {
771
+ // Run cleanup before checkDirty which commits signal values
772
+ if (e.runCleanup) {
773
+ inCleanup = true
774
+ try {
775
+ e.runCleanup()
776
+ } finally {
777
+ inCleanup = false
778
+ }
779
+ }
780
+ if (checkDirty(e.deps, e)) {
781
+ ++cycle
782
+ effectRunDevtools(e)
783
+ e.depsTail = undefined
784
+ e.flags = WatchingRunning
785
+ const prevSub = activeSub
786
+ activeSub = e
787
+ try {
788
+ e.fn()
789
+ activeSub = prevSub
790
+ e.flags = Watching
791
+ purgeDeps(e)
792
+ } catch (err) {
793
+ activeSub = prevSub
794
+ e.flags = Watching
795
+ throw err
796
+ }
797
+ } else {
798
+ e.flags = Watching
799
+ }
755
800
  } else {
756
801
  e.flags = Watching
757
802
  }
@@ -870,7 +915,8 @@ function signalOper<T>(this: SignalNode<T>, value?: T): T | void {
870
915
  }
871
916
 
872
917
  const flags = this.flags
873
- if (flags & Dirty) {
918
+ // During cleanup, don't update signal - return currentValue as-is
919
+ if (flags & Dirty && !inCleanup) {
874
920
  if (updateSignal(this)) {
875
921
  const subs = this.subs
876
922
  if (subs !== undefined) shallowPropagate(subs)
@@ -976,6 +1022,44 @@ export function effect(fn: () => void): EffectDisposer {
976
1022
 
977
1023
  return effectOper.bind(e) as EffectDisposer
978
1024
  }
1025
+
1026
+ /**
1027
+ * Create a reactive effect with a custom cleanup runner
1028
+ * The cleanup runner is called BEFORE signal values are committed, allowing
1029
+ * cleanup functions to access the previous values of signals.
1030
+ * @param fn - The effect function
1031
+ * @param cleanupRunner - Function to run cleanups before signal value commit
1032
+ * @returns An effect disposer function
1033
+ */
1034
+ export function effectWithCleanup(fn: () => void, cleanupRunner: () => void): EffectDisposer {
1035
+ const e: EffectNode = {
1036
+ fn,
1037
+ subs: undefined,
1038
+ subsTail: undefined,
1039
+ deps: undefined,
1040
+ depsTail: undefined,
1041
+ flags: WatchingRunning,
1042
+ runCleanup: cleanupRunner,
1043
+ __id: undefined as number | undefined,
1044
+ }
1045
+
1046
+ registerEffectDevtools(e)
1047
+
1048
+ const prevSub = activeSub
1049
+ if (prevSub !== undefined) link(e, prevSub, 0)
1050
+ activeSub = e
1051
+
1052
+ try {
1053
+ effectRunDevtools(e)
1054
+ fn()
1055
+ } finally {
1056
+ activeSub = prevSub
1057
+ e.flags &= ~Running
1058
+ }
1059
+
1060
+ return effectOper.bind(e) as EffectDisposer
1061
+ }
1062
+
979
1063
  function effectOper(this: EffectNode): void {
980
1064
  disposeNode(this)
981
1065
  }
@@ -1057,10 +1141,20 @@ export function endBatch(): void {
1057
1141
  */
1058
1142
  export function batch<T>(fn: () => T): T {
1059
1143
  ++batchDepth
1144
+ let _error: unknown
1145
+ let hasError = false
1060
1146
  try {
1061
1147
  return fn()
1148
+ } catch (e) {
1149
+ _error = e
1150
+ hasError = true
1151
+ throw e
1062
1152
  } finally {
1063
- if (--batchDepth === 0) flush()
1153
+ --batchDepth
1154
+ // Only flush if no error occurred to avoid interfering with error propagation
1155
+ if (!hasError && batchDepth === 0) {
1156
+ flush()
1157
+ }
1064
1158
  }
1065
1159
  }
1066
1160
  /**
@@ -1253,7 +1347,7 @@ export function createSelector<T>(
1253
1347
  let current = source()
1254
1348
  const observers = new Map<T, SignalAccessor<boolean>>()
1255
1349
 
1256
- effect(() => {
1350
+ const dispose = effect(() => {
1257
1351
  const next = source()
1258
1352
  if (equalityFn(current, next)) return
1259
1353
 
@@ -1265,6 +1359,10 @@ export function createSelector<T>(
1265
1359
 
1266
1360
  current = next
1267
1361
  })
1362
+ registerRootCleanup(() => {
1363
+ dispose()
1364
+ observers.clear()
1365
+ })
1268
1366
 
1269
1367
  return (key: T) => {
1270
1368
  let sig = observers.get(key)