@fictjs/runtime 0.0.11 → 0.0.13

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
@@ -2,6 +2,11 @@ import { beginFlushGuard, beforeEffectRunGuard, endFlushGuard } from './cycle-gu
2
2
  import { getDevtoolsHook } from './devtools'
3
3
  import { registerRootCleanup } from './lifecycle'
4
4
 
5
+ const isDev =
6
+ typeof __DEV__ !== 'undefined'
7
+ ? __DEV__
8
+ : typeof process === 'undefined' || process.env?.NODE_ENV !== 'production'
9
+
5
10
  // ============================================================================
6
11
  // Type Definitions
7
12
  // ============================================================================
@@ -208,6 +213,11 @@ const enqueueMicrotask =
208
213
  }
209
214
  // Flag to indicate cleanup is running - signal reads should return currentValue without updating
210
215
  let inCleanup = false
216
+ // This ensures type detection works correctly even after minification
217
+ const SIGNAL_MARKER = Symbol.for('fict:signal')
218
+ const COMPUTED_MARKER = Symbol.for('fict:computed')
219
+ const EFFECT_MARKER = Symbol.for('fict:effect')
220
+ const EFFECT_SCOPE_MARKER = Symbol.for('fict:effectScope')
211
221
  export const ReactiveFlags = {
212
222
  None: 0,
213
223
  Mutable,
@@ -318,13 +328,21 @@ export function createReactiveSystem({
318
328
  dirty = true
319
329
  }
320
330
  } else if ((depFlags & MutablePending) === MutablePending) {
321
- if (link.nextSub !== undefined || link.prevSub !== undefined) {
322
- stack = { value: link, prev: stack }
331
+ if (!dep.deps) {
332
+ const nextDep = link.nextDep
333
+ if (nextDep !== undefined) {
334
+ link = nextDep
335
+ continue
336
+ }
337
+ } else {
338
+ if (link.nextSub !== undefined || link.prevSub !== undefined) {
339
+ stack = { value: link, prev: stack }
340
+ }
341
+ link = dep.deps
342
+ sub = dep
343
+ ++checkDepth
344
+ continue
323
345
  }
324
- link = dep.deps!
325
- sub = dep
326
- ++checkDepth
327
- continue
328
346
  }
329
347
 
330
348
  if (!dirty) {
@@ -572,13 +590,22 @@ function checkDirty(firstLink: Link, sub: ReactiveNode): boolean {
572
590
  dirty = true
573
591
  }
574
592
  } else if ((depFlags & MutablePending) === MutablePending) {
575
- if (link.nextSub !== undefined || link.prevSub !== undefined) {
576
- stack = { value: link, prev: stack }
593
+ if (!dep.deps) {
594
+ // No dependencies to check, skip this node
595
+ const nextDep = link.nextDep
596
+ if (nextDep !== undefined) {
597
+ link = nextDep
598
+ continue
599
+ }
600
+ } else {
601
+ if (link.nextSub !== undefined || link.prevSub !== undefined) {
602
+ stack = { value: link, prev: stack }
603
+ }
604
+ link = dep.deps
605
+ sub = dep
606
+ ++checkDepth
607
+ continue
577
608
  }
578
- link = dep.deps!
579
- sub = dep
580
- ++checkDepth
581
- continue
582
609
  }
583
610
 
584
611
  if (!dirty) {
@@ -688,8 +715,12 @@ function disposeNode(node: ReactiveNode): void {
688
715
  node.depsTail = undefined
689
716
  node.flags = 0
690
717
  purgeDeps(node)
691
- const sub = node.subs
692
- if (sub !== undefined) unlink(sub, node)
718
+ let sub = node.subs
719
+ while (sub !== undefined) {
720
+ const next = sub.nextSub
721
+ unlink(sub)
722
+ sub = next
723
+ }
693
724
  }
694
725
  /**
695
726
  * Update a signal node
@@ -897,7 +928,9 @@ export function signal<T>(initialValue: T): SignalAccessor<T> {
897
928
  __id: undefined as number | undefined,
898
929
  }
899
930
  registerSignalDevtools(initialValue, s)
900
- return signalOper.bind(s) as SignalAccessor<T>
931
+ const accessor = signalOper.bind(s) as SignalAccessor<T> & Record<symbol, boolean>
932
+ accessor[SIGNAL_MARKER] = true
933
+ return accessor as SignalAccessor<T>
901
934
  }
902
935
  function signalOper<T>(this: SignalNode<T>, value?: T): T | void {
903
936
  if (arguments.length > 0) {
@@ -953,7 +986,9 @@ export function computed<T>(getter: (oldValue?: T) => T): ComputedAccessor<T> {
953
986
  flags: 0,
954
987
  getter,
955
988
  }
956
- const bound = (computedOper as (this: ComputedNode<T>) => T).bind(c)
989
+ const bound = (computedOper as (this: ComputedNode<T>) => T).bind(c) as ComputedAccessor<T> &
990
+ Record<symbol, boolean>
991
+ bound[COMPUTED_MARKER] = true
957
992
  return bound as ComputedAccessor<T>
958
993
  }
959
994
  function computedOper<T>(this: ComputedNode<T>): T {
@@ -1020,7 +1055,9 @@ export function effect(fn: () => void): EffectDisposer {
1020
1055
  e.flags &= ~Running
1021
1056
  }
1022
1057
 
1023
- return effectOper.bind(e) as EffectDisposer
1058
+ const disposer = effectOper.bind(e) as EffectDisposer & Record<symbol, boolean>
1059
+ disposer[EFFECT_MARKER] = true
1060
+ return disposer as EffectDisposer
1024
1061
  }
1025
1062
 
1026
1063
  /**
@@ -1057,7 +1094,9 @@ export function effectWithCleanup(fn: () => void, cleanupRunner: () => void): Ef
1057
1094
  e.flags &= ~Running
1058
1095
  }
1059
1096
 
1060
- return effectOper.bind(e) as EffectDisposer
1097
+ const disposer = effectOper.bind(e) as EffectDisposer & Record<symbol, boolean>
1098
+ disposer[EFFECT_MARKER] = true
1099
+ return disposer as EffectDisposer
1061
1100
  }
1062
1101
 
1063
1102
  function effectOper(this: EffectNode): void {
@@ -1084,7 +1123,9 @@ export function effectScope(fn: () => void): EffectScopeDisposer {
1084
1123
  activeSub = prevSub
1085
1124
  }
1086
1125
 
1087
- return effectScopeOper.bind(e) as EffectScopeDisposer
1126
+ const disposer = effectScopeOper.bind(e) as EffectScopeDisposer & Record<symbol, boolean>
1127
+ disposer[EFFECT_SCOPE_MARKER] = true
1128
+ return disposer as EffectScopeDisposer
1088
1129
  }
1089
1130
  function effectScopeOper(this: EffectScopeNode): void {
1090
1131
  disposeNode(this)
@@ -1141,21 +1182,28 @@ export function endBatch(): void {
1141
1182
  */
1142
1183
  export function batch<T>(fn: () => T): T {
1143
1184
  ++batchDepth
1144
- let _error: unknown
1145
- let hasError = false
1185
+ let result!: T
1186
+ let error: unknown
1146
1187
  try {
1147
- return fn()
1188
+ result = fn()
1148
1189
  } catch (e) {
1149
- _error = e
1150
- hasError = true
1151
- throw e
1190
+ error = e
1152
1191
  } finally {
1153
1192
  --batchDepth
1154
- // Only flush if no error occurred to avoid interfering with error propagation
1155
- if (!hasError && batchDepth === 0) {
1156
- flush()
1193
+ if (batchDepth === 0) {
1194
+ try {
1195
+ flush()
1196
+ } catch (flushErr) {
1197
+ if (error === undefined) {
1198
+ error = flushErr
1199
+ }
1200
+ }
1157
1201
  }
1158
1202
  }
1203
+ if (error !== undefined) {
1204
+ throw error
1205
+ }
1206
+ return result
1159
1207
  }
1160
1208
  /**
1161
1209
  * Get the current active subscriber
@@ -1203,14 +1251,16 @@ export function untrack<T>(fn: () => T): T {
1203
1251
  export function peek<T>(accessor: () => T): T {
1204
1252
  return untrack(accessor)
1205
1253
  }
1206
- // Type detection - Fixed: using Function.name
1254
+ // This ensures correct detection even after minification
1207
1255
  /**
1208
1256
  * Check if a function is a signal accessor
1209
1257
  * @param fn - The function to check
1210
1258
  * @returns True if the function is a signal accessor
1211
1259
  */
1212
1260
  export function isSignal(fn: unknown): fn is SignalAccessor<unknown> {
1213
- return typeof fn === 'function' && fn.name === 'bound signalOper'
1261
+ return (
1262
+ typeof fn === 'function' && (fn as unknown as Record<symbol, boolean>)[SIGNAL_MARKER] === true
1263
+ )
1214
1264
  }
1215
1265
  /**
1216
1266
  * Check if a function is a computed accessor
@@ -1218,7 +1268,9 @@ export function isSignal(fn: unknown): fn is SignalAccessor<unknown> {
1218
1268
  * @returns True if the function is a computed accessor
1219
1269
  */
1220
1270
  export function isComputed(fn: unknown): fn is ComputedAccessor<unknown> {
1221
- return typeof fn === 'function' && fn.name === 'bound computedOper'
1271
+ return (
1272
+ typeof fn === 'function' && (fn as unknown as Record<symbol, boolean>)[COMPUTED_MARKER] === true
1273
+ )
1222
1274
  }
1223
1275
  /**
1224
1276
  * Check if a function is an effect disposer
@@ -1226,7 +1278,9 @@ export function isComputed(fn: unknown): fn is ComputedAccessor<unknown> {
1226
1278
  * @returns True if the function is an effect disposer
1227
1279
  */
1228
1280
  export function isEffect(fn: unknown): fn is EffectDisposer {
1229
- return typeof fn === 'function' && fn.name === 'bound effectOper'
1281
+ return (
1282
+ typeof fn === 'function' && (fn as unknown as Record<symbol, boolean>)[EFFECT_MARKER] === true
1283
+ )
1230
1284
  }
1231
1285
  /**
1232
1286
  * Check if a function is an effect scope disposer
@@ -1234,7 +1288,10 @@ export function isEffect(fn: unknown): fn is EffectDisposer {
1234
1288
  * @returns True if the function is an effect scope disposer
1235
1289
  */
1236
1290
  export function isEffectScope(fn: unknown): fn is EffectScopeDisposer {
1237
- return typeof fn === 'function' && fn.name === 'bound effectScopeOper'
1291
+ return (
1292
+ typeof fn === 'function' &&
1293
+ (fn as unknown as Record<symbol, boolean>)[EFFECT_SCOPE_MARKER] === true
1294
+ )
1238
1295
  }
1239
1296
  // ============================================================================
1240
1297
  // Transition Context (for priority scheduling)
@@ -1290,43 +1347,51 @@ export default {
1290
1347
  }
1291
1348
  export const $state = signal as <T>(value: T) => T
1292
1349
 
1293
- let devtoolsSignalId = 0
1294
- let devtoolsEffectId = 0
1295
-
1296
1350
  interface DevtoolsIdentifiable {
1297
1351
  __id?: number
1298
1352
  }
1299
1353
 
1300
- function registerSignalDevtools(value: unknown, node: SignalNode): number | undefined {
1301
- const hook = getDevtoolsHook()
1302
- if (!hook) return undefined
1303
- const id = ++devtoolsSignalId
1304
- hook.registerSignal(id, value)
1305
- ;(node as SignalNode & DevtoolsIdentifiable).__id = id
1306
- return id
1307
- }
1354
+ let registerSignalDevtools: (value: unknown, node: SignalNode) => number | undefined = () =>
1355
+ undefined
1356
+ let updateSignalDevtools: (node: SignalNode, value: unknown) => void = () => {}
1357
+ let registerEffectDevtools: (node: EffectNode) => number | undefined = () => undefined
1358
+ let effectRunDevtools: (node: EffectNode) => void = () => {}
1359
+
1360
+ if (isDev) {
1361
+ let devtoolsSignalId = 0
1362
+ let devtoolsEffectId = 0
1363
+
1364
+ registerSignalDevtools = (value, node) => {
1365
+ const hook = getDevtoolsHook()
1366
+ if (!hook) return undefined
1367
+ const id = ++devtoolsSignalId
1368
+ hook.registerSignal(id, value)
1369
+ ;(node as SignalNode & DevtoolsIdentifiable).__id = id
1370
+ return id
1371
+ }
1308
1372
 
1309
- function updateSignalDevtools(node: SignalNode, value: unknown): void {
1310
- const hook = getDevtoolsHook()
1311
- if (!hook) return
1312
- const id = (node as SignalNode & DevtoolsIdentifiable).__id
1313
- if (id) hook.updateSignal(id, value)
1314
- }
1373
+ updateSignalDevtools = (node, value) => {
1374
+ const hook = getDevtoolsHook()
1375
+ if (!hook) return
1376
+ const id = (node as SignalNode & DevtoolsIdentifiable).__id
1377
+ if (id) hook.updateSignal(id, value)
1378
+ }
1315
1379
 
1316
- function registerEffectDevtools(node: EffectNode): number | undefined {
1317
- const hook = getDevtoolsHook()
1318
- if (!hook) return undefined
1319
- const id = ++devtoolsEffectId
1320
- hook.registerEffect(id)
1321
- ;(node as EffectNode & DevtoolsIdentifiable).__id = id
1322
- return id
1323
- }
1380
+ registerEffectDevtools = node => {
1381
+ const hook = getDevtoolsHook()
1382
+ if (!hook) return undefined
1383
+ const id = ++devtoolsEffectId
1384
+ hook.registerEffect(id)
1385
+ ;(node as EffectNode & DevtoolsIdentifiable).__id = id
1386
+ return id
1387
+ }
1324
1388
 
1325
- function effectRunDevtools(node: EffectNode): void {
1326
- const hook = getDevtoolsHook()
1327
- if (!hook) return
1328
- const id = (node as EffectNode & DevtoolsIdentifiable).__id
1329
- if (id) hook.effectRun(id)
1389
+ effectRunDevtools = node => {
1390
+ const hook = getDevtoolsHook()
1391
+ if (!hook) return
1392
+ const id = (node as EffectNode & DevtoolsIdentifiable).__id
1393
+ if (id) hook.effectRun(id)
1394
+ }
1330
1395
  }
1331
1396
 
1332
1397
  // ============================================================================
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 {