@fictjs/runtime 0.0.11 → 0.0.12

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
@@ -208,6 +208,11 @@ const enqueueMicrotask =
208
208
  }
209
209
  // Flag to indicate cleanup is running - signal reads should return currentValue without updating
210
210
  let inCleanup = false
211
+ // This ensures type detection works correctly even after minification
212
+ const SIGNAL_MARKER = Symbol.for('fict:signal')
213
+ const COMPUTED_MARKER = Symbol.for('fict:computed')
214
+ const EFFECT_MARKER = Symbol.for('fict:effect')
215
+ const EFFECT_SCOPE_MARKER = Symbol.for('fict:effectScope')
211
216
  export const ReactiveFlags = {
212
217
  None: 0,
213
218
  Mutable,
@@ -318,13 +323,21 @@ export function createReactiveSystem({
318
323
  dirty = true
319
324
  }
320
325
  } else if ((depFlags & MutablePending) === MutablePending) {
321
- if (link.nextSub !== undefined || link.prevSub !== undefined) {
322
- stack = { value: link, prev: stack }
326
+ if (!dep.deps) {
327
+ const nextDep = link.nextDep
328
+ if (nextDep !== undefined) {
329
+ link = nextDep
330
+ continue
331
+ }
332
+ } else {
333
+ if (link.nextSub !== undefined || link.prevSub !== undefined) {
334
+ stack = { value: link, prev: stack }
335
+ }
336
+ link = dep.deps
337
+ sub = dep
338
+ ++checkDepth
339
+ continue
323
340
  }
324
- link = dep.deps!
325
- sub = dep
326
- ++checkDepth
327
- continue
328
341
  }
329
342
 
330
343
  if (!dirty) {
@@ -572,13 +585,22 @@ function checkDirty(firstLink: Link, sub: ReactiveNode): boolean {
572
585
  dirty = true
573
586
  }
574
587
  } else if ((depFlags & MutablePending) === MutablePending) {
575
- if (link.nextSub !== undefined || link.prevSub !== undefined) {
576
- stack = { value: link, prev: stack }
588
+ if (!dep.deps) {
589
+ // No dependencies to check, skip this node
590
+ const nextDep = link.nextDep
591
+ if (nextDep !== undefined) {
592
+ link = nextDep
593
+ continue
594
+ }
595
+ } else {
596
+ if (link.nextSub !== undefined || link.prevSub !== undefined) {
597
+ stack = { value: link, prev: stack }
598
+ }
599
+ link = dep.deps
600
+ sub = dep
601
+ ++checkDepth
602
+ continue
577
603
  }
578
- link = dep.deps!
579
- sub = dep
580
- ++checkDepth
581
- continue
582
604
  }
583
605
 
584
606
  if (!dirty) {
@@ -688,8 +710,12 @@ function disposeNode(node: ReactiveNode): void {
688
710
  node.depsTail = undefined
689
711
  node.flags = 0
690
712
  purgeDeps(node)
691
- const sub = node.subs
692
- if (sub !== undefined) unlink(sub, node)
713
+ let sub = node.subs
714
+ while (sub !== undefined) {
715
+ const next = sub.nextSub
716
+ unlink(sub)
717
+ sub = next
718
+ }
693
719
  }
694
720
  /**
695
721
  * Update a signal node
@@ -897,7 +923,9 @@ export function signal<T>(initialValue: T): SignalAccessor<T> {
897
923
  __id: undefined as number | undefined,
898
924
  }
899
925
  registerSignalDevtools(initialValue, s)
900
- return signalOper.bind(s) as SignalAccessor<T>
926
+ const accessor = signalOper.bind(s) as SignalAccessor<T> & Record<symbol, boolean>
927
+ accessor[SIGNAL_MARKER] = true
928
+ return accessor as SignalAccessor<T>
901
929
  }
902
930
  function signalOper<T>(this: SignalNode<T>, value?: T): T | void {
903
931
  if (arguments.length > 0) {
@@ -953,7 +981,9 @@ export function computed<T>(getter: (oldValue?: T) => T): ComputedAccessor<T> {
953
981
  flags: 0,
954
982
  getter,
955
983
  }
956
- const bound = (computedOper as (this: ComputedNode<T>) => T).bind(c)
984
+ const bound = (computedOper as (this: ComputedNode<T>) => T).bind(c) as ComputedAccessor<T> &
985
+ Record<symbol, boolean>
986
+ bound[COMPUTED_MARKER] = true
957
987
  return bound as ComputedAccessor<T>
958
988
  }
959
989
  function computedOper<T>(this: ComputedNode<T>): T {
@@ -1020,7 +1050,9 @@ export function effect(fn: () => void): EffectDisposer {
1020
1050
  e.flags &= ~Running
1021
1051
  }
1022
1052
 
1023
- return effectOper.bind(e) as EffectDisposer
1053
+ const disposer = effectOper.bind(e) as EffectDisposer & Record<symbol, boolean>
1054
+ disposer[EFFECT_MARKER] = true
1055
+ return disposer as EffectDisposer
1024
1056
  }
1025
1057
 
1026
1058
  /**
@@ -1057,7 +1089,9 @@ export function effectWithCleanup(fn: () => void, cleanupRunner: () => void): Ef
1057
1089
  e.flags &= ~Running
1058
1090
  }
1059
1091
 
1060
- return effectOper.bind(e) as EffectDisposer
1092
+ const disposer = effectOper.bind(e) as EffectDisposer & Record<symbol, boolean>
1093
+ disposer[EFFECT_MARKER] = true
1094
+ return disposer as EffectDisposer
1061
1095
  }
1062
1096
 
1063
1097
  function effectOper(this: EffectNode): void {
@@ -1084,7 +1118,9 @@ export function effectScope(fn: () => void): EffectScopeDisposer {
1084
1118
  activeSub = prevSub
1085
1119
  }
1086
1120
 
1087
- return effectScopeOper.bind(e) as EffectScopeDisposer
1121
+ const disposer = effectScopeOper.bind(e) as EffectScopeDisposer & Record<symbol, boolean>
1122
+ disposer[EFFECT_SCOPE_MARKER] = true
1123
+ return disposer as EffectScopeDisposer
1088
1124
  }
1089
1125
  function effectScopeOper(this: EffectScopeNode): void {
1090
1126
  disposeNode(this)
@@ -1203,14 +1239,16 @@ export function untrack<T>(fn: () => T): T {
1203
1239
  export function peek<T>(accessor: () => T): T {
1204
1240
  return untrack(accessor)
1205
1241
  }
1206
- // Type detection - Fixed: using Function.name
1242
+ // This ensures correct detection even after minification
1207
1243
  /**
1208
1244
  * Check if a function is a signal accessor
1209
1245
  * @param fn - The function to check
1210
1246
  * @returns True if the function is a signal accessor
1211
1247
  */
1212
1248
  export function isSignal(fn: unknown): fn is SignalAccessor<unknown> {
1213
- return typeof fn === 'function' && fn.name === 'bound signalOper'
1249
+ return (
1250
+ typeof fn === 'function' && (fn as unknown as Record<symbol, boolean>)[SIGNAL_MARKER] === true
1251
+ )
1214
1252
  }
1215
1253
  /**
1216
1254
  * Check if a function is a computed accessor
@@ -1218,7 +1256,9 @@ export function isSignal(fn: unknown): fn is SignalAccessor<unknown> {
1218
1256
  * @returns True if the function is a computed accessor
1219
1257
  */
1220
1258
  export function isComputed(fn: unknown): fn is ComputedAccessor<unknown> {
1221
- return typeof fn === 'function' && fn.name === 'bound computedOper'
1259
+ return (
1260
+ typeof fn === 'function' && (fn as unknown as Record<symbol, boolean>)[COMPUTED_MARKER] === true
1261
+ )
1222
1262
  }
1223
1263
  /**
1224
1264
  * Check if a function is an effect disposer
@@ -1226,7 +1266,9 @@ export function isComputed(fn: unknown): fn is ComputedAccessor<unknown> {
1226
1266
  * @returns True if the function is an effect disposer
1227
1267
  */
1228
1268
  export function isEffect(fn: unknown): fn is EffectDisposer {
1229
- return typeof fn === 'function' && fn.name === 'bound effectOper'
1269
+ return (
1270
+ typeof fn === 'function' && (fn as unknown as Record<symbol, boolean>)[EFFECT_MARKER] === true
1271
+ )
1230
1272
  }
1231
1273
  /**
1232
1274
  * Check if a function is an effect scope disposer
@@ -1234,7 +1276,10 @@ export function isEffect(fn: unknown): fn is EffectDisposer {
1234
1276
  * @returns True if the function is an effect scope disposer
1235
1277
  */
1236
1278
  export function isEffectScope(fn: unknown): fn is EffectScopeDisposer {
1237
- return typeof fn === 'function' && fn.name === 'bound effectScopeOper'
1279
+ return (
1280
+ typeof fn === 'function' &&
1281
+ (fn as unknown as Record<symbol, boolean>)[EFFECT_SCOPE_MARKER] === true
1282
+ )
1238
1283
  }
1239
1284
  // ============================================================================
1240
1285
  // Transition Context (for priority scheduling)
package/src/suspense.ts CHANGED
@@ -103,7 +103,9 @@ export function Suspense(props: SuspenseProps): FictNode {
103
103
  popRoot(prev)
104
104
  flushOnMount(root)
105
105
  destroyRoot(root)
106
- handleError(err, { source: 'render' })
106
+ if (!handleError(err, { source: 'render' }, hostRoot)) {
107
+ throw err
108
+ }
107
109
  return
108
110
  }
109
111
  popRoot(prev)
@@ -143,18 +145,33 @@ export function Suspense(props: SuspenseProps): FictNode {
143
145
  if (thenable) {
144
146
  thenable.then(
145
147
  () => {
146
- if (epoch !== tokenEpoch) return
147
- pending(Math.max(0, pending() - 1))
148
- if (pending() === 0) {
148
+ // This prevents stale token resolutions from affecting state after
149
+ // a reset. The order is important: check epoch first, then update state.
150
+ if (epoch !== tokenEpoch) {
151
+ // Token is stale (from before a reset), ignore it completely
152
+ return
153
+ }
154
+ // Use Math.max as a defensive measure - pending should never go below 0,
155
+ // but this protects against edge cases where a token might resolve twice
156
+ // or after the component has been reset.
157
+ const newPending = Math.max(0, pending() - 1)
158
+ pending(newPending)
159
+ if (newPending === 0) {
149
160
  switchView(props.children ?? null)
150
161
  onResolveMaybe()
151
162
  }
152
163
  },
153
164
  err => {
154
- if (epoch !== tokenEpoch) return
155
- pending(Math.max(0, pending() - 1))
165
+ // Same epoch check - ignore stale tokens
166
+ if (epoch !== tokenEpoch) {
167
+ return
168
+ }
169
+ const newPending = Math.max(0, pending() - 1)
170
+ pending(newPending)
156
171
  props.onReject?.(err)
157
- handleError(err, { source: 'render' }, hostRoot)
172
+ if (!handleError(err, { source: 'render' }, hostRoot)) {
173
+ throw err
174
+ }
158
175
  },
159
176
  )
160
177
  return true
package/src/transition.ts CHANGED
@@ -71,8 +71,11 @@ export function useTransition(): [() => boolean, (fn: () => void) => void] {
71
71
  const pending = signal(false)
72
72
 
73
73
  const start = (fn: () => void) => {
74
- pending(true)
74
+ // Both pending(true) and pending(false) are now low-priority updates,
75
+ // preventing the race condition where isPending() could be inconsistent
76
+ // with the actual transition execution state.
75
77
  startTransition(() => {
78
+ pending(true)
76
79
  try {
77
80
  fn()
78
81
  } finally {