@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/dist/index.cjs +2373 -3048
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -141
- package/dist/index.d.ts +11 -141
- package/dist/index.dev.js +2558 -2653
- package/dist/index.dev.js.map +1 -1
- package/dist/index.js +2374 -3042
- package/dist/index.js.map +1 -1
- package/package.json +1 -6
- package/src/binding.ts +28 -423
- package/src/constants.ts +368 -344
- package/src/cycle-guard.ts +124 -97
- package/src/dev.d.ts +5 -0
- package/src/dom.ts +39 -27
- package/src/effect.ts +4 -0
- package/src/error-boundary.ts +11 -2
- package/src/hooks.ts +9 -1
- package/src/index.ts +1 -19
- package/src/lifecycle.ts +17 -3
- package/src/list-helpers.ts +248 -86
- package/src/props.ts +2 -4
- package/src/reconcile.ts +4 -0
- package/src/signal.ts +128 -63
- package/src/suspense.ts +24 -7
- package/src/transition.ts +4 -1
- package/dist/slim.cjs +0 -3668
- package/dist/slim.cjs.map +0 -1
- package/dist/slim.d.cts +0 -504
- package/dist/slim.d.ts +0 -504
- package/dist/slim.js +0 -3616
- package/dist/slim.js.map +0 -1
- package/src/slim.ts +0 -69
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 (
|
|
322
|
-
|
|
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 (
|
|
576
|
-
|
|
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
|
-
|
|
692
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1145
|
-
let
|
|
1185
|
+
let result!: T
|
|
1186
|
+
let error: unknown
|
|
1146
1187
|
try {
|
|
1147
|
-
|
|
1188
|
+
result = fn()
|
|
1148
1189
|
} catch (e) {
|
|
1149
|
-
|
|
1150
|
-
hasError = true
|
|
1151
|
-
throw e
|
|
1190
|
+
error = e
|
|
1152
1191
|
} finally {
|
|
1153
1192
|
--batchDepth
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
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
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
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
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
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
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
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
|
-
|
|
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 {
|