@fictjs/runtime 0.1.0 → 0.2.0
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/advanced.cjs +8 -8
- package/dist/advanced.d.cts +3 -3
- package/dist/advanced.d.ts +3 -3
- package/dist/advanced.js +3 -3
- package/dist/{chunk-Q4EN6BXV.cjs → chunk-527QSKFM.cjs} +16 -16
- package/dist/{chunk-Q4EN6BXV.cjs.map → chunk-527QSKFM.cjs.map} +1 -1
- package/dist/{chunk-BWZFJXUI.js → chunk-5KXEEQUO.js} +84 -10
- package/dist/chunk-5KXEEQUO.js.map +1 -0
- package/dist/{chunk-YQ4IB7NC.cjs → chunk-BSUMPMKX.cjs} +7 -7
- package/dist/{chunk-YQ4IB7NC.cjs.map → chunk-BSUMPMKX.cjs.map} +1 -1
- package/dist/{chunk-V62XZLDU.js → chunk-FG3M7EBL.js} +2 -2
- package/dist/{chunk-7WAGAQLT.cjs → chunk-J74L7UYP.cjs} +84 -10
- package/dist/chunk-J74L7UYP.cjs.map +1 -0
- package/dist/{chunk-CF3OHML2.js → chunk-QV5GOCR5.js} +2 -2
- package/dist/{context-B7UYnfzM.d.ts → context-4woHo7-L.d.ts} +1 -1
- package/dist/{context-UXySaqI_.d.cts → context-9gFXOdJl.d.cts} +1 -1
- package/dist/{effect-Auji1rz9.d.cts → effect-ClARNUCc.d.cts} +23 -2
- package/dist/{effect-Auji1rz9.d.ts → effect-ClARNUCc.d.ts} +23 -2
- package/dist/index.cjs +51 -54
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.dev.js +66 -19
- package/dist/index.dev.js.map +1 -1
- package/dist/index.js +10 -13
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +34 -34
- package/dist/internal.d.cts +4 -4
- package/dist/internal.d.ts +4 -4
- package/dist/internal.js +2 -2
- package/dist/jsx-runtime.d.cts +671 -0
- package/dist/jsx-runtime.d.ts +671 -0
- package/dist/{props-BfmSLuyp.d.cts → props-CBwuh35e.d.cts} +4 -4
- package/dist/{props-BBi8Tkks.d.ts → props-DAyeRPwH.d.ts} +4 -4
- package/dist/{scope-S6eAzBJZ.d.ts → scope-DvgMquEy.d.ts} +1 -1
- package/dist/{scope-DKYzWfTn.d.cts → scope-xmdo6lVU.d.cts} +1 -1
- package/package.json +1 -1
- package/src/binding.ts +58 -5
- package/src/effect.ts +9 -2
- package/src/lifecycle.ts +13 -3
- package/src/signal.ts +43 -4
- package/src/suspense.ts +17 -13
- package/dist/chunk-7WAGAQLT.cjs.map +0 -1
- package/dist/chunk-BWZFJXUI.js.map +0 -1
- /package/dist/{chunk-V62XZLDU.js.map → chunk-FG3M7EBL.js.map} +0 -0
- /package/dist/{chunk-CF3OHML2.js.map → chunk-QV5GOCR5.js.map} +0 -0
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import { C as Cleanup, F as FictNode, D as DOMElement } from './effect-
|
|
2
|
-
|
|
3
|
-
type Memo<T> = () => T;
|
|
4
|
-
declare function createMemo<T>(fn: () => T): Memo<T>;
|
|
1
|
+
import { C as Cleanup, F as FictNode, D as DOMElement } from './effect-ClARNUCc.js';
|
|
5
2
|
|
|
6
3
|
type LifecycleFn = () => void | Cleanup;
|
|
7
4
|
interface CreateRootOptions {
|
|
@@ -15,6 +12,9 @@ declare function createRoot<T>(fn: () => T, options?: CreateRootOptions): {
|
|
|
15
12
|
value: T;
|
|
16
13
|
};
|
|
17
14
|
|
|
15
|
+
type Memo<T> = () => T;
|
|
16
|
+
declare function createMemo<T>(fn: () => T): Memo<T>;
|
|
17
|
+
|
|
18
18
|
declare const Fragment: unique symbol;
|
|
19
19
|
declare namespace JSX {
|
|
20
20
|
type Element = FictNode;
|
package/package.json
CHANGED
package/src/binding.ts
CHANGED
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
} from './lifecycle'
|
|
37
37
|
import { toNodeArray, removeNodes, insertNodesBefore } from './node-ops'
|
|
38
38
|
import { batch } from './scheduler'
|
|
39
|
-
import { computed, untrack } from './signal'
|
|
39
|
+
import { computed, untrack, isSignal, isComputed, isEffect, isEffectScope } from './signal'
|
|
40
40
|
import type { Cleanup, FictNode } from './types'
|
|
41
41
|
|
|
42
42
|
const isDev =
|
|
@@ -70,11 +70,43 @@ export interface BindingHandle {
|
|
|
70
70
|
// ============================================================================
|
|
71
71
|
|
|
72
72
|
/**
|
|
73
|
-
* Check if a value is reactive (a getter function)
|
|
74
|
-
*
|
|
73
|
+
* Check if a value is reactive (a getter function that returns a value).
|
|
74
|
+
*
|
|
75
|
+
* A value is considered reactive if:
|
|
76
|
+
* 1. It's a signal or computed value created by the runtime (marked with Symbol)
|
|
77
|
+
* 2. It's a zero-argument function (getter pattern used by the compiler)
|
|
78
|
+
*
|
|
79
|
+
* NOT considered reactive:
|
|
80
|
+
* - Event handlers (functions that take arguments)
|
|
81
|
+
* - Effect disposers (zero-arg but for cleanup, not value access)
|
|
82
|
+
* - Effect scopes (zero-arg but for scope management)
|
|
83
|
+
*
|
|
84
|
+
* @param value - The value to check
|
|
85
|
+
* @returns true if the value is a reactive getter
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* ```ts
|
|
89
|
+
* const [count, setCount] = createSignal(0)
|
|
90
|
+
* isReactive(count) // true - signal accessor
|
|
91
|
+
* isReactive(() => 42) // true - getter pattern
|
|
92
|
+
* isReactive((x) => x) // false - takes argument
|
|
93
|
+
* isReactive('hello') // false - not a function
|
|
94
|
+
* isReactive(effectDisposer) // false - effect cleanup function
|
|
95
|
+
* ```
|
|
75
96
|
*/
|
|
76
97
|
export function isReactive(value: unknown): value is () => unknown {
|
|
77
|
-
|
|
98
|
+
if (typeof value !== 'function') return false
|
|
99
|
+
|
|
100
|
+
// Check for runtime-created signals/computed (most reliable)
|
|
101
|
+
if (isSignal(value) || isComputed(value)) return true
|
|
102
|
+
|
|
103
|
+
// Exclude effect disposers and effect scopes - they are zero-arg
|
|
104
|
+
// functions but not reactive getters
|
|
105
|
+
if (isEffect(value) || isEffectScope(value)) return false
|
|
106
|
+
|
|
107
|
+
// Fall back to length check for compiler-generated getters
|
|
108
|
+
// Zero-argument functions are treated as reactive getters
|
|
109
|
+
return value.length === 0
|
|
78
110
|
}
|
|
79
111
|
|
|
80
112
|
/**
|
|
@@ -590,6 +622,7 @@ export function insert(
|
|
|
590
622
|
const root = createRootContext(hostRoot)
|
|
591
623
|
const prev = pushRoot(root)
|
|
592
624
|
let nodes: Node[] = []
|
|
625
|
+
let handledError = false
|
|
593
626
|
try {
|
|
594
627
|
let newNode: Node | Node[]
|
|
595
628
|
|
|
@@ -614,14 +647,34 @@ export function insert(
|
|
|
614
647
|
}
|
|
615
648
|
|
|
616
649
|
nodes = toNodeArray(newNode)
|
|
650
|
+
if (root.suspended) {
|
|
651
|
+
handledError = true
|
|
652
|
+
destroyRoot(root)
|
|
653
|
+
return
|
|
654
|
+
}
|
|
617
655
|
if (parentNode) {
|
|
618
656
|
insertNodesBefore(parentNode, nodes, marker)
|
|
619
657
|
}
|
|
658
|
+
} catch (err) {
|
|
659
|
+
if (handleSuspend(err as any, root)) {
|
|
660
|
+
handledError = true
|
|
661
|
+
destroyRoot(root)
|
|
662
|
+
return
|
|
663
|
+
}
|
|
664
|
+
if (handleError(err, { source: 'renderChild' }, root)) {
|
|
665
|
+
handledError = true
|
|
666
|
+
destroyRoot(root)
|
|
667
|
+
return
|
|
668
|
+
}
|
|
669
|
+
throw err
|
|
620
670
|
} finally {
|
|
621
671
|
popRoot(prev)
|
|
622
|
-
|
|
672
|
+
if (!handledError) {
|
|
673
|
+
flushOnMount(root)
|
|
674
|
+
}
|
|
623
675
|
}
|
|
624
676
|
|
|
677
|
+
// If we reach here, no error was handled (handledError blocks return early)
|
|
625
678
|
currentRoot = root
|
|
626
679
|
currentNodes = nodes
|
|
627
680
|
})
|
package/src/effect.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getCurrentRoot,
|
|
3
3
|
handleError,
|
|
4
|
+
handleSuspend,
|
|
4
5
|
registerRootCleanup,
|
|
5
6
|
runCleanupList,
|
|
6
7
|
withEffectCleanups,
|
|
@@ -34,6 +35,9 @@ export function createEffect(fn: Effect): () => void {
|
|
|
34
35
|
bucket.push(maybeCleanup)
|
|
35
36
|
}
|
|
36
37
|
} catch (err) {
|
|
38
|
+
if (handleSuspend(err as any, rootForError)) {
|
|
39
|
+
return
|
|
40
|
+
}
|
|
37
41
|
if (handleError(err, { source: 'effect' }, rootForError)) {
|
|
38
42
|
return
|
|
39
43
|
}
|
|
@@ -43,7 +47,7 @@ export function createEffect(fn: Effect): () => void {
|
|
|
43
47
|
cleanups = bucket
|
|
44
48
|
}
|
|
45
49
|
|
|
46
|
-
const disposeEffect = effectWithCleanup(run, doCleanup)
|
|
50
|
+
const disposeEffect = effectWithCleanup(run, doCleanup, rootForError)
|
|
47
51
|
const teardown = () => {
|
|
48
52
|
runCleanupList(cleanups)
|
|
49
53
|
disposeEffect()
|
|
@@ -76,6 +80,9 @@ export function createRenderEffect(fn: Effect): () => void {
|
|
|
76
80
|
cleanup = maybeCleanup
|
|
77
81
|
}
|
|
78
82
|
} catch (err) {
|
|
83
|
+
if (handleSuspend(err as any, rootForError)) {
|
|
84
|
+
return
|
|
85
|
+
}
|
|
79
86
|
const handled = handleError(err, { source: 'effect' }, rootForError)
|
|
80
87
|
if (handled) {
|
|
81
88
|
return
|
|
@@ -84,7 +91,7 @@ export function createRenderEffect(fn: Effect): () => void {
|
|
|
84
91
|
}
|
|
85
92
|
}
|
|
86
93
|
|
|
87
|
-
const disposeEffect = effectWithCleanup(run, doCleanup)
|
|
94
|
+
const disposeEffect = effectWithCleanup(run, doCleanup, rootForError)
|
|
88
95
|
const teardown = () => {
|
|
89
96
|
if (cleanup) {
|
|
90
97
|
cleanup()
|
package/src/lifecycle.ts
CHANGED
|
@@ -15,6 +15,7 @@ export interface RootContext {
|
|
|
15
15
|
destroyCallbacks: Cleanup[]
|
|
16
16
|
errorHandlers?: ErrorHandler[]
|
|
17
17
|
suspenseHandlers?: SuspenseHandler[]
|
|
18
|
+
suspended?: boolean
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export interface CreateRootOptions {
|
|
@@ -30,7 +31,7 @@ const globalErrorHandlers = new WeakMap<RootContext, ErrorHandler[]>()
|
|
|
30
31
|
const globalSuspenseHandlers = new WeakMap<RootContext, SuspenseHandler[]>()
|
|
31
32
|
|
|
32
33
|
export function createRootContext(parent?: RootContext): RootContext {
|
|
33
|
-
return { parent, cleanups: [], destroyCallbacks: [] }
|
|
34
|
+
return { parent, cleanups: [], destroyCallbacks: [], suspended: false }
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
export function pushRoot(root: RootContext): RootContext | undefined {
|
|
@@ -267,13 +268,18 @@ export function handleSuspend(
|
|
|
267
268
|
startRoot?: RootContext,
|
|
268
269
|
): boolean {
|
|
269
270
|
let root: RootContext | undefined = startRoot ?? currentRoot
|
|
271
|
+
const originRoot = root // Preserve reference to set suspended flag on success
|
|
270
272
|
while (root) {
|
|
271
273
|
const handlers = root.suspenseHandlers
|
|
272
274
|
if (handlers && handlers.length) {
|
|
273
275
|
for (let i = handlers.length - 1; i >= 0; i--) {
|
|
274
276
|
const handler = handlers[i]!
|
|
275
277
|
const handled = handler(token)
|
|
276
|
-
if (handled !== false)
|
|
278
|
+
if (handled !== false) {
|
|
279
|
+
// Only set suspended = true when a handler actually handles the token
|
|
280
|
+
if (originRoot) originRoot.suspended = true
|
|
281
|
+
return true
|
|
282
|
+
}
|
|
277
283
|
}
|
|
278
284
|
}
|
|
279
285
|
root = root.parent
|
|
@@ -288,7 +294,11 @@ export function handleSuspend(
|
|
|
288
294
|
for (let i = globalForRoot.length - 1; i >= 0; i--) {
|
|
289
295
|
const handler = globalForRoot[i]!
|
|
290
296
|
const handled = handler(token)
|
|
291
|
-
if (handled !== false)
|
|
297
|
+
if (handled !== false) {
|
|
298
|
+
// Only set suspended = true when a handler actually handles the token
|
|
299
|
+
if (originRoot) originRoot.suspended = true
|
|
300
|
+
return true
|
|
301
|
+
}
|
|
292
302
|
}
|
|
293
303
|
}
|
|
294
304
|
return false
|
package/src/signal.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { beginFlushGuard, beforeEffectRunGuard, endFlushGuard } from './cycle-guard'
|
|
2
2
|
import { getDevtoolsHook } from './devtools'
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
getCurrentRoot,
|
|
5
|
+
handleError,
|
|
6
|
+
handleSuspend,
|
|
7
|
+
registerRootCleanup,
|
|
8
|
+
type RootContext,
|
|
9
|
+
} from './lifecycle'
|
|
4
10
|
|
|
5
11
|
const isDev =
|
|
6
12
|
typeof __DEV__ !== 'undefined'
|
|
@@ -103,6 +109,8 @@ export interface EffectNode extends BaseNode {
|
|
|
103
109
|
depsTail: Link | undefined
|
|
104
110
|
/** Optional cleanup runner to be called before checkDirty */
|
|
105
111
|
runCleanup?: () => void
|
|
112
|
+
/** Root context for error/suspense handling */
|
|
113
|
+
root?: RootContext
|
|
106
114
|
/** Devtools ID */
|
|
107
115
|
__id?: number | undefined
|
|
108
116
|
}
|
|
@@ -808,7 +816,25 @@ function runEffect(e: EffectNode): void {
|
|
|
808
816
|
inCleanup = false
|
|
809
817
|
}
|
|
810
818
|
}
|
|
811
|
-
|
|
819
|
+
let isDirty = false
|
|
820
|
+
try {
|
|
821
|
+
isDirty = checkDirty(e.deps, e)
|
|
822
|
+
} catch (err) {
|
|
823
|
+
if (handleSuspend(err as any, e.root)) {
|
|
824
|
+
if (e.flags !== 0) {
|
|
825
|
+
e.flags = Watching
|
|
826
|
+
}
|
|
827
|
+
return
|
|
828
|
+
}
|
|
829
|
+
if (handleError(err, { source: 'effect' }, e.root)) {
|
|
830
|
+
if (e.flags !== 0) {
|
|
831
|
+
e.flags = Watching
|
|
832
|
+
}
|
|
833
|
+
return
|
|
834
|
+
}
|
|
835
|
+
throw err
|
|
836
|
+
}
|
|
837
|
+
if (isDirty) {
|
|
812
838
|
++cycle
|
|
813
839
|
effectRunDevtools(e)
|
|
814
840
|
e.depsTail = undefined
|
|
@@ -1031,7 +1057,7 @@ function computedOper<T>(this: ComputedNode<T>): T {
|
|
|
1031
1057
|
* @returns An effect disposer function
|
|
1032
1058
|
*/
|
|
1033
1059
|
export function effect(fn: () => void): EffectDisposer {
|
|
1034
|
-
const e = {
|
|
1060
|
+
const e: EffectNode = {
|
|
1035
1061
|
fn,
|
|
1036
1062
|
subs: undefined,
|
|
1037
1063
|
subsTail: undefined,
|
|
@@ -1040,6 +1066,10 @@ export function effect(fn: () => void): EffectDisposer {
|
|
|
1040
1066
|
flags: WatchingRunning,
|
|
1041
1067
|
__id: undefined as number | undefined,
|
|
1042
1068
|
}
|
|
1069
|
+
const root = getCurrentRoot()
|
|
1070
|
+
if (root) {
|
|
1071
|
+
e.root = root
|
|
1072
|
+
}
|
|
1043
1073
|
|
|
1044
1074
|
registerEffectDevtools(e)
|
|
1045
1075
|
|
|
@@ -1066,9 +1096,14 @@ export function effect(fn: () => void): EffectDisposer {
|
|
|
1066
1096
|
* cleanup functions to access the previous values of signals.
|
|
1067
1097
|
* @param fn - The effect function
|
|
1068
1098
|
* @param cleanupRunner - Function to run cleanups before signal value commit
|
|
1099
|
+
* @param root - Root context for error/suspense handling (defaults to current root)
|
|
1069
1100
|
* @returns An effect disposer function
|
|
1070
1101
|
*/
|
|
1071
|
-
export function effectWithCleanup(
|
|
1102
|
+
export function effectWithCleanup(
|
|
1103
|
+
fn: () => void,
|
|
1104
|
+
cleanupRunner: () => void,
|
|
1105
|
+
root?: RootContext,
|
|
1106
|
+
): EffectDisposer {
|
|
1072
1107
|
const e: EffectNode = {
|
|
1073
1108
|
fn,
|
|
1074
1109
|
subs: undefined,
|
|
@@ -1079,6 +1114,10 @@ export function effectWithCleanup(fn: () => void, cleanupRunner: () => void): Ef
|
|
|
1079
1114
|
runCleanup: cleanupRunner,
|
|
1080
1115
|
__id: undefined as number | undefined,
|
|
1081
1116
|
}
|
|
1117
|
+
const resolvedRoot = root ?? getCurrentRoot()
|
|
1118
|
+
if (resolvedRoot) {
|
|
1119
|
+
e.root = resolvedRoot
|
|
1120
|
+
}
|
|
1082
1121
|
|
|
1083
1122
|
registerEffectDevtools(e)
|
|
1084
1123
|
|
package/src/suspense.ts
CHANGED
|
@@ -60,11 +60,6 @@ export function Suspense(props: SuspenseProps): FictNode {
|
|
|
60
60
|
? (props.fallback as (e?: unknown) => FictNode)(err)
|
|
61
61
|
: props.fallback
|
|
62
62
|
|
|
63
|
-
const switchView = (view: FictNode | null) => {
|
|
64
|
-
currentView(view)
|
|
65
|
-
renderView(view)
|
|
66
|
-
}
|
|
67
|
-
|
|
68
63
|
const renderView = (view: FictNode | null) => {
|
|
69
64
|
if (cleanup) {
|
|
70
65
|
cleanup()
|
|
@@ -88,8 +83,9 @@ export function Suspense(props: SuspenseProps): FictNode {
|
|
|
88
83
|
// Suspended view: child threw a suspense token and was handled upstream.
|
|
89
84
|
// Avoid replacing existing fallback content; tear down this attempt.
|
|
90
85
|
const suspendedAttempt =
|
|
91
|
-
|
|
92
|
-
nodes.
|
|
86
|
+
root.suspended ||
|
|
87
|
+
(nodes.length > 0 &&
|
|
88
|
+
nodes.every(node => node instanceof Comment && (node as Comment).data === 'fict:suspend'))
|
|
93
89
|
if (suspendedAttempt) {
|
|
94
90
|
popRoot(prev)
|
|
95
91
|
destroyRoot(root)
|
|
@@ -134,7 +130,10 @@ export function Suspense(props: SuspenseProps): FictNode {
|
|
|
134
130
|
registerSuspenseHandler(token => {
|
|
135
131
|
const tokenEpoch = epoch
|
|
136
132
|
pending(pending() + 1)
|
|
137
|
-
switchView
|
|
133
|
+
// Directly render fallback instead of using switchView to avoid
|
|
134
|
+
// triggering the effect which would cause duplicate renders
|
|
135
|
+
currentView(toFallback())
|
|
136
|
+
renderView(toFallback())
|
|
138
137
|
|
|
139
138
|
const thenable = (token as SuspenseToken).then
|
|
140
139
|
? (token as SuspenseToken)
|
|
@@ -157,7 +156,9 @@ export function Suspense(props: SuspenseProps): FictNode {
|
|
|
157
156
|
const newPending = Math.max(0, pending() - 1)
|
|
158
157
|
pending(newPending)
|
|
159
158
|
if (newPending === 0) {
|
|
160
|
-
|
|
159
|
+
// Directly render children instead of using switchView
|
|
160
|
+
currentView(props.children ?? null)
|
|
161
|
+
renderView(props.children ?? null)
|
|
161
162
|
onResolveMaybe()
|
|
162
163
|
}
|
|
163
164
|
},
|
|
@@ -180,9 +181,10 @@ export function Suspense(props: SuspenseProps): FictNode {
|
|
|
180
181
|
return false
|
|
181
182
|
})
|
|
182
183
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
184
|
+
// Initial render - render children directly
|
|
185
|
+
// Note: This will be called synchronously during component creation.
|
|
186
|
+
// If children suspend, the handler above will be called and switch to fallback.
|
|
187
|
+
renderView(props.children ?? null)
|
|
186
188
|
|
|
187
189
|
if (props.resetKeys !== undefined) {
|
|
188
190
|
const isGetter =
|
|
@@ -195,7 +197,9 @@ export function Suspense(props: SuspenseProps): FictNode {
|
|
|
195
197
|
prev = next
|
|
196
198
|
epoch++
|
|
197
199
|
pending(0)
|
|
198
|
-
|
|
200
|
+
// Directly render children instead of using switchView
|
|
201
|
+
currentView(props.children ?? null)
|
|
202
|
+
renderView(props.children ?? null)
|
|
199
203
|
}
|
|
200
204
|
})
|
|
201
205
|
}
|