@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/dist/index.cjs +243 -45
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.dev.js +258 -43
- package/dist/index.dev.js.map +1 -1
- package/dist/index.js +243 -45
- package/dist/index.js.map +1 -1
- package/dist/slim.cjs +221 -35
- package/dist/slim.cjs.map +1 -1
- package/dist/slim.js +221 -35
- package/dist/slim.js.map +1 -1
- package/package.json +1 -1
- package/src/binding.ts +3 -1
- package/src/dev.d.ts +5 -0
- package/src/dom.ts +20 -2
- package/src/error-boundary.ts +11 -2
- package/src/lifecycle.ts +4 -1
- package/src/list-helpers.ts +243 -22
- package/src/props.ts +2 -4
- package/src/reconcile.ts +4 -0
- package/src/signal.ts +69 -24
- package/src/suspense.ts +24 -7
- package/src/transition.ts +4 -1
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 (
|
|
322
|
-
|
|
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 (
|
|
576
|
-
|
|
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
|
-
|
|
692
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
if (
|
|
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
|
-
|
|
155
|
-
|
|
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 {
|