@fictjs/runtime 0.5.0 → 0.5.2
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 +9 -9
- package/dist/advanced.js +4 -4
- package/dist/{chunk-5AA7HP4S.js → chunk-4NUHM77Z.js} +3 -3
- package/dist/{chunk-BQG7VEBY.js → chunk-D2IWOO4X.js} +2 -2
- package/dist/{chunk-KYLNC4CD.cjs → chunk-KNGHYGK4.cjs} +17 -17
- package/dist/{chunk-KYLNC4CD.cjs.map → chunk-KNGHYGK4.cjs.map} +1 -1
- package/dist/{chunk-FKDMDAUR.js → chunk-LRFMCJY3.js} +119 -19
- package/dist/chunk-LRFMCJY3.js.map +1 -0
- package/dist/{chunk-GHUV2FLD.cjs → chunk-QB2UD62G.cjs} +8 -8
- package/dist/{chunk-GHUV2FLD.cjs.map → chunk-QB2UD62G.cjs.map} +1 -1
- package/dist/{chunk-KKKYW54Z.js → chunk-SLFAEVKJ.js} +3 -3
- package/dist/{chunk-TKWN42TA.cjs → chunk-Z6M3HKLG.cjs} +156 -156
- package/dist/{chunk-TKWN42TA.cjs.map → chunk-Z6M3HKLG.cjs.map} +1 -1
- package/dist/{chunk-6SOPF5LZ.cjs → chunk-ZR435MDC.cjs} +120 -20
- package/dist/chunk-ZR435MDC.cjs.map +1 -0
- package/dist/index.cjs +95 -45
- package/dist/index.cjs.map +1 -1
- package/dist/index.dev.js +120 -25
- package/dist/index.dev.js.map +1 -1
- package/dist/index.js +60 -10
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +64 -42
- package/dist/internal.cjs.map +1 -1
- package/dist/internal.d.cts +12 -3
- package/dist/internal.d.ts +12 -3
- package/dist/internal.js +25 -3
- package/dist/internal.js.map +1 -1
- package/dist/jsx-dev-runtime.d.cts +671 -0
- package/dist/jsx-dev-runtime.d.ts +671 -0
- package/dist/loader.cjs +60 -8
- package/dist/loader.cjs.map +1 -1
- package/dist/loader.d.cts +1 -1
- package/dist/loader.d.ts +1 -1
- package/dist/loader.js +53 -1
- package/dist/loader.js.map +1 -1
- package/dist/{resume-Dx8_l72o.d.ts → resume-CqeQ3v_q.d.ts} +5 -1
- package/dist/{resume-BrAkmSTY.d.cts → resume-i-A3EFox.d.cts} +5 -1
- package/package.json +1 -1
- package/src/cycle-guard.ts +1 -1
- package/src/internal.ts +4 -0
- package/src/list-helpers.ts +19 -4
- package/src/loader.ts +58 -0
- package/src/resume.ts +55 -0
- package/src/signal.ts +47 -22
- package/src/ssr-stream.ts +38 -0
- package/src/suspense.ts +62 -7
- package/dist/chunk-6SOPF5LZ.cjs.map +0 -1
- package/dist/chunk-FKDMDAUR.js.map +0 -1
- /package/dist/{chunk-5AA7HP4S.js.map → chunk-4NUHM77Z.js.map} +0 -0
- /package/dist/{chunk-BQG7VEBY.js.map → chunk-D2IWOO4X.js.map} +0 -0
- /package/dist/{chunk-KKKYW54Z.js.map → chunk-SLFAEVKJ.js.map} +0 -0
package/src/loader.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
__fictEnsureScope,
|
|
5
5
|
__fictGetResume,
|
|
6
6
|
__fictGetSSRScope,
|
|
7
|
+
__fictMergeSSRState,
|
|
7
8
|
__fictSetSSRState,
|
|
8
9
|
__fictUseLexicalScope,
|
|
9
10
|
} from './resume'
|
|
@@ -82,6 +83,8 @@ const hydratedScopes = new Set<string>()
|
|
|
82
83
|
const prefetchedUrls = new Set<string>()
|
|
83
84
|
let prefetchCleanup: (() => void) | null = null
|
|
84
85
|
let eventListenerCleanup: (() => void) | null = null
|
|
86
|
+
let snapshotObserver: MutationObserver | null = null
|
|
87
|
+
const processedSnapshots = new Set<HTMLScriptElement>()
|
|
85
88
|
|
|
86
89
|
/**
|
|
87
90
|
* Reset the hydrated scopes set. Useful for testing.
|
|
@@ -131,6 +134,7 @@ export function installResumableLoader(options: ResumableLoaderOptions = {}): vo
|
|
|
131
134
|
// Reset hydrated scopes for fresh loader installation
|
|
132
135
|
hydratedScopes.clear()
|
|
133
136
|
prefetchedUrls.clear()
|
|
137
|
+
processedSnapshots.clear()
|
|
134
138
|
|
|
135
139
|
// Clean up previous event listeners
|
|
136
140
|
if (eventListenerCleanup) {
|
|
@@ -144,6 +148,11 @@ export function installResumableLoader(options: ResumableLoaderOptions = {}): vo
|
|
|
144
148
|
prefetchCleanup = null
|
|
145
149
|
}
|
|
146
150
|
|
|
151
|
+
if (snapshotObserver) {
|
|
152
|
+
snapshotObserver.disconnect()
|
|
153
|
+
snapshotObserver = null
|
|
154
|
+
}
|
|
155
|
+
|
|
147
156
|
const snapshotEl = doc.getElementById(scriptId)
|
|
148
157
|
if (snapshotEl?.textContent) {
|
|
149
158
|
try {
|
|
@@ -154,6 +163,38 @@ export function installResumableLoader(options: ResumableLoaderOptions = {}): vo
|
|
|
154
163
|
}
|
|
155
164
|
}
|
|
156
165
|
|
|
166
|
+
const snapshotScripts = doc.querySelectorAll(
|
|
167
|
+
'script[type="application/json"][data-fict-snapshot]',
|
|
168
|
+
)
|
|
169
|
+
for (const script of Array.from(snapshotScripts)) {
|
|
170
|
+
parseSnapshotScript(script as HTMLScriptElement)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (typeof MutationObserver !== 'undefined') {
|
|
174
|
+
snapshotObserver = new MutationObserver(mutations => {
|
|
175
|
+
for (const mutation of mutations) {
|
|
176
|
+
for (const node of Array.from(mutation.addedNodes)) {
|
|
177
|
+
if (!(node instanceof Element)) continue
|
|
178
|
+
if (node.tagName === 'SCRIPT') {
|
|
179
|
+
const script = node as HTMLScriptElement
|
|
180
|
+
if (isSnapshotScript(script)) {
|
|
181
|
+
parseSnapshotScript(script)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const nested = node.querySelectorAll?.(
|
|
185
|
+
'script[type="application/json"][data-fict-snapshot]',
|
|
186
|
+
)
|
|
187
|
+
if (nested && nested.length) {
|
|
188
|
+
for (const script of Array.from(nested)) {
|
|
189
|
+
parseSnapshotScript(script as HTMLScriptElement)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
})
|
|
195
|
+
snapshotObserver.observe(doc.documentElement ?? doc, { childList: true, subtree: true })
|
|
196
|
+
}
|
|
197
|
+
|
|
157
198
|
__fictEnableResumable()
|
|
158
199
|
|
|
159
200
|
const events = options.events ?? Array.from(DelegatedEvents)
|
|
@@ -174,6 +215,23 @@ export function installResumableLoader(options: ResumableLoaderOptions = {}): vo
|
|
|
174
215
|
}
|
|
175
216
|
}
|
|
176
217
|
|
|
218
|
+
function isSnapshotScript(script: HTMLScriptElement): boolean {
|
|
219
|
+
return script.type === 'application/json' && script.hasAttribute('data-fict-snapshot')
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function parseSnapshotScript(script: HTMLScriptElement): void {
|
|
223
|
+
if (processedSnapshots.has(script)) return
|
|
224
|
+
processedSnapshots.add(script)
|
|
225
|
+
const text = script.textContent
|
|
226
|
+
if (!text) return
|
|
227
|
+
try {
|
|
228
|
+
const state = JSON.parse(text)
|
|
229
|
+
__fictMergeSSRState(state)
|
|
230
|
+
} catch {
|
|
231
|
+
// Ignore parse errors
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
177
235
|
// ============================================================================
|
|
178
236
|
// Prefetch Implementation
|
|
179
237
|
// ============================================================================
|
package/src/resume.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { HookContext } from './hooks'
|
|
2
2
|
import { createSignal, isSignal } from './signal'
|
|
3
|
+
import { __fictGetCurrentSSRBoundary } from './ssr-stream'
|
|
3
4
|
import { createStore, isStoreProxy, unwrapStore } from './store'
|
|
4
5
|
|
|
5
6
|
// ============================================================================
|
|
@@ -43,6 +44,7 @@ export interface ScopeRecord {
|
|
|
43
44
|
id: string
|
|
44
45
|
ctx: HookContext
|
|
45
46
|
host: Element
|
|
47
|
+
boundaryId?: string
|
|
46
48
|
type?: string
|
|
47
49
|
props?: Record<string, unknown>
|
|
48
50
|
}
|
|
@@ -52,6 +54,7 @@ let resumableEnabled = false
|
|
|
52
54
|
let hydrating = false
|
|
53
55
|
let scopeCounter = 0
|
|
54
56
|
let scopeRegistry = new Map<string, ScopeRecord>()
|
|
57
|
+
let boundaryScopes = new Map<string, Set<string>>()
|
|
55
58
|
let snapshotState: SSRState | null = null
|
|
56
59
|
const resumedScopes = new Map<
|
|
57
60
|
string,
|
|
@@ -62,12 +65,14 @@ export function __fictEnableSSR(): void {
|
|
|
62
65
|
ssrEnabled = true
|
|
63
66
|
scopeCounter = 0
|
|
64
67
|
scopeRegistry = new Map()
|
|
68
|
+
boundaryScopes = new Map()
|
|
65
69
|
resumedScopes.clear()
|
|
66
70
|
snapshotState = null
|
|
67
71
|
}
|
|
68
72
|
|
|
69
73
|
export function __fictDisableSSR(): void {
|
|
70
74
|
ssrEnabled = false
|
|
75
|
+
boundaryScopes = new Map()
|
|
71
76
|
}
|
|
72
77
|
|
|
73
78
|
export function __fictEnableResumable(): void {
|
|
@@ -124,6 +129,16 @@ export function __fictRegisterScope(
|
|
|
124
129
|
if (props !== undefined) {
|
|
125
130
|
record.props = props
|
|
126
131
|
}
|
|
132
|
+
const boundaryId = __fictGetCurrentSSRBoundary()
|
|
133
|
+
if (boundaryId) {
|
|
134
|
+
record.boundaryId = boundaryId
|
|
135
|
+
let scopes = boundaryScopes.get(boundaryId)
|
|
136
|
+
if (!scopes) {
|
|
137
|
+
scopes = new Set()
|
|
138
|
+
boundaryScopes.set(boundaryId, scopes)
|
|
139
|
+
}
|
|
140
|
+
scopes.add(id)
|
|
141
|
+
}
|
|
127
142
|
scopeRegistry.set(id, record)
|
|
128
143
|
return id
|
|
129
144
|
}
|
|
@@ -132,6 +147,12 @@ export function __fictGetScopeRegistry(): Map<string, ScopeRecord> {
|
|
|
132
147
|
return scopeRegistry
|
|
133
148
|
}
|
|
134
149
|
|
|
150
|
+
export function __fictGetScopesForBoundary(boundaryId: string): string[] {
|
|
151
|
+
const scopes = boundaryScopes.get(boundaryId)
|
|
152
|
+
if (!scopes) return []
|
|
153
|
+
return Array.from(scopes)
|
|
154
|
+
}
|
|
155
|
+
|
|
135
156
|
export function __fictSerializeSSRState(): SSRState {
|
|
136
157
|
const scopes: Record<string, ScopeSnapshot> = {}
|
|
137
158
|
|
|
@@ -155,6 +176,31 @@ export function __fictSerializeSSRState(): SSRState {
|
|
|
155
176
|
return { scopes }
|
|
156
177
|
}
|
|
157
178
|
|
|
179
|
+
export function __fictSerializeSSRStateForScopes(scopeIds: Iterable<string>): SSRState {
|
|
180
|
+
const scopes: Record<string, ScopeSnapshot> = {}
|
|
181
|
+
|
|
182
|
+
for (const id of scopeIds) {
|
|
183
|
+
const record = scopeRegistry.get(id)
|
|
184
|
+
if (!record) continue
|
|
185
|
+
const snapshot: ScopeSnapshot = {
|
|
186
|
+
id,
|
|
187
|
+
slots: serializeSlots(record.ctx),
|
|
188
|
+
}
|
|
189
|
+
if (record.type !== undefined) {
|
|
190
|
+
snapshot.t = record.type
|
|
191
|
+
}
|
|
192
|
+
if (record.props !== undefined) {
|
|
193
|
+
snapshot.props = record.props
|
|
194
|
+
}
|
|
195
|
+
if (record.ctx.slotMap !== undefined) {
|
|
196
|
+
snapshot.vars = record.ctx.slotMap
|
|
197
|
+
}
|
|
198
|
+
scopes[id] = snapshot
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return { scopes }
|
|
202
|
+
}
|
|
203
|
+
|
|
158
204
|
export function __fictSetSSRState(state: SSRState | null): void {
|
|
159
205
|
snapshotState = state
|
|
160
206
|
if (!state) {
|
|
@@ -162,6 +208,15 @@ export function __fictSetSSRState(state: SSRState | null): void {
|
|
|
162
208
|
}
|
|
163
209
|
}
|
|
164
210
|
|
|
211
|
+
export function __fictMergeSSRState(state: SSRState | null): void {
|
|
212
|
+
if (!state) return
|
|
213
|
+
if (!snapshotState) {
|
|
214
|
+
snapshotState = { scopes: { ...state.scopes } }
|
|
215
|
+
return
|
|
216
|
+
}
|
|
217
|
+
Object.assign(snapshotState.scopes, state.scopes)
|
|
218
|
+
}
|
|
219
|
+
|
|
165
220
|
export function __fictGetSSRScope(id: string): ScopeSnapshot | undefined {
|
|
166
221
|
return snapshotState?.scopes[id]
|
|
167
222
|
}
|
package/src/signal.ts
CHANGED
|
@@ -103,6 +103,10 @@ export interface SignalNode<T = unknown> extends BaseNode {
|
|
|
103
103
|
currentValue: T
|
|
104
104
|
/** Pending value to be committed */
|
|
105
105
|
pendingValue: T
|
|
106
|
+
/** Previous committed value (for cleanup reads) */
|
|
107
|
+
prevValue?: T
|
|
108
|
+
/** Flush id when prevValue was recorded */
|
|
109
|
+
prevFlushId?: number
|
|
106
110
|
/** Signals don't have dependencies */
|
|
107
111
|
deps?: undefined
|
|
108
112
|
depsTail?: undefined
|
|
@@ -123,6 +127,10 @@ export interface SignalNode<T = unknown> extends BaseNode {
|
|
|
123
127
|
export interface ComputedNode<T = unknown> extends BaseNode {
|
|
124
128
|
/** Current computed value */
|
|
125
129
|
value: T
|
|
130
|
+
/** Previous computed value (for cleanup reads) */
|
|
131
|
+
prevValue?: T
|
|
132
|
+
/** Flush id when prevValue was recorded */
|
|
133
|
+
prevFlushId?: number
|
|
126
134
|
/** First dependency link */
|
|
127
135
|
deps: Link | undefined
|
|
128
136
|
/** Last dependency link */
|
|
@@ -251,6 +259,8 @@ let cycle = 0
|
|
|
251
259
|
let batchDepth = 0
|
|
252
260
|
let activeSub: ReactiveNode | undefined
|
|
253
261
|
let flushScheduled = false
|
|
262
|
+
let currentFlushId = 0
|
|
263
|
+
let activeCleanupFlushId = 0
|
|
254
264
|
// Dual-priority queue for scheduler
|
|
255
265
|
const highPriorityQueue: EffectNode[] = []
|
|
256
266
|
const lowPriorityQueue: EffectNode[] = []
|
|
@@ -798,6 +808,8 @@ function updateSignal(s: SignalNode): boolean {
|
|
|
798
808
|
const current = s.currentValue
|
|
799
809
|
const pending = s.pendingValue
|
|
800
810
|
if (valuesDiffer(s, current, pending)) {
|
|
811
|
+
s.prevValue = current
|
|
812
|
+
s.prevFlushId = currentFlushId
|
|
801
813
|
s.currentValue = pending
|
|
802
814
|
return true
|
|
803
815
|
}
|
|
@@ -822,6 +834,8 @@ function updateComputed<T>(c: ComputedNode<T>): boolean {
|
|
|
822
834
|
c.flags &= ~Running
|
|
823
835
|
purgeDeps(c)
|
|
824
836
|
if (valuesDiffer(c, oldValue, newValue)) {
|
|
837
|
+
c.prevValue = oldValue
|
|
838
|
+
c.prevFlushId = currentFlushId
|
|
825
839
|
c.value = newValue
|
|
826
840
|
if (isDev) updateComputedDevtools(c, newValue)
|
|
827
841
|
return true
|
|
@@ -839,16 +853,20 @@ function updateComputed<T>(c: ComputedNode<T>): boolean {
|
|
|
839
853
|
*/
|
|
840
854
|
function runEffect(e: EffectNode): void {
|
|
841
855
|
const flags = e.flags
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
856
|
+
const runCleanup = () => {
|
|
857
|
+
if (!e.runCleanup) return
|
|
858
|
+
inCleanup = true
|
|
859
|
+
activeCleanupFlushId = currentFlushId
|
|
860
|
+
try {
|
|
861
|
+
e.runCleanup()
|
|
862
|
+
} finally {
|
|
863
|
+
activeCleanupFlushId = 0
|
|
864
|
+
inCleanup = false
|
|
851
865
|
}
|
|
866
|
+
}
|
|
867
|
+
if (flags & Dirty) {
|
|
868
|
+
// Run cleanup before re-run; values are still the previous commit.
|
|
869
|
+
runCleanup()
|
|
852
870
|
++cycle
|
|
853
871
|
if (isDev) effectRunDevtools(e)
|
|
854
872
|
e.depsTail = undefined
|
|
@@ -866,15 +884,6 @@ function runEffect(e: EffectNode): void {
|
|
|
866
884
|
throw err
|
|
867
885
|
}
|
|
868
886
|
} else if (flags & Pending && e.deps) {
|
|
869
|
-
// Run cleanup before checkDirty which commits signal values
|
|
870
|
-
if (e.runCleanup) {
|
|
871
|
-
inCleanup = true
|
|
872
|
-
try {
|
|
873
|
-
e.runCleanup()
|
|
874
|
-
} finally {
|
|
875
|
-
inCleanup = false
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
887
|
let isDirty = false
|
|
879
888
|
try {
|
|
880
889
|
isDirty = checkDirty(e.deps, e)
|
|
@@ -894,6 +903,9 @@ function runEffect(e: EffectNode): void {
|
|
|
894
903
|
throw err
|
|
895
904
|
}
|
|
896
905
|
if (isDirty) {
|
|
906
|
+
// Only run cleanup if the effect will actually re-run.
|
|
907
|
+
// Cleanup reads should observe previous values for this flush.
|
|
908
|
+
runCleanup()
|
|
897
909
|
++cycle
|
|
898
910
|
if (isDev) effectRunDevtools(e)
|
|
899
911
|
e.depsTail = undefined
|
|
@@ -947,6 +959,7 @@ function flush(): void {
|
|
|
947
959
|
endFlushGuard()
|
|
948
960
|
return
|
|
949
961
|
}
|
|
962
|
+
currentFlushId++
|
|
950
963
|
flushScheduled = false
|
|
951
964
|
|
|
952
965
|
// 1. Process all high-priority effects first
|
|
@@ -1071,6 +1084,12 @@ function signalOper<T>(this: SignalNode<T>, value?: T): T | void {
|
|
|
1071
1084
|
if (subs !== undefined) shallowPropagate(subs)
|
|
1072
1085
|
}
|
|
1073
1086
|
}
|
|
1087
|
+
if (inCleanup) {
|
|
1088
|
+
if (this.prevFlushId === activeCleanupFlushId) {
|
|
1089
|
+
return this.prevValue as T
|
|
1090
|
+
}
|
|
1091
|
+
return this.currentValue
|
|
1092
|
+
}
|
|
1074
1093
|
|
|
1075
1094
|
let sub = activeSub
|
|
1076
1095
|
while (sub !== undefined) {
|
|
@@ -1118,10 +1137,14 @@ export function computed<T>(
|
|
|
1118
1137
|
return bound as ComputedAccessor<T>
|
|
1119
1138
|
}
|
|
1120
1139
|
function computedOper<T>(this: ComputedNode<T>): T {
|
|
1121
|
-
// fix: During cleanup, return
|
|
1122
|
-
// This ensures cleanup functions
|
|
1123
|
-
|
|
1124
|
-
|
|
1140
|
+
// fix: During cleanup, return previous value for this flush without triggering updates.
|
|
1141
|
+
// This ensures cleanup functions observe the pre-commit state for this effect.
|
|
1142
|
+
if (inCleanup) {
|
|
1143
|
+
if (this.prevFlushId === activeCleanupFlushId) {
|
|
1144
|
+
return this.prevValue as T
|
|
1145
|
+
}
|
|
1146
|
+
return this.value
|
|
1147
|
+
}
|
|
1125
1148
|
|
|
1126
1149
|
const flags = this.flags
|
|
1127
1150
|
|
|
@@ -1388,6 +1411,8 @@ export function __resetReactiveState(): void {
|
|
|
1388
1411
|
isInTransition = false
|
|
1389
1412
|
inCleanup = false
|
|
1390
1413
|
cycle = 0
|
|
1414
|
+
currentFlushId = 0
|
|
1415
|
+
activeCleanupFlushId = 0
|
|
1391
1416
|
}
|
|
1392
1417
|
/**
|
|
1393
1418
|
* Execute a function without tracking dependencies
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface SSRStreamHooks {
|
|
2
|
+
registerBoundary?: (start: Comment, end: Comment) => string | null
|
|
3
|
+
boundaryPending?: (id: string) => void
|
|
4
|
+
boundaryResolved?: (id: string) => void
|
|
5
|
+
onError?: (error: unknown, boundaryId?: string) => void
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
let ssrStreamHooks: SSRStreamHooks | null = null
|
|
9
|
+
const boundaryStack: string[] = []
|
|
10
|
+
|
|
11
|
+
export function __fictSetSSRStreamHooks(hooks: SSRStreamHooks | null): void {
|
|
12
|
+
ssrStreamHooks = hooks
|
|
13
|
+
if (!hooks) {
|
|
14
|
+
boundaryStack.length = 0
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function __fictGetSSRStreamHooks(): SSRStreamHooks | null {
|
|
19
|
+
return ssrStreamHooks
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function __fictPushSSRBoundary(id: string): void {
|
|
23
|
+
boundaryStack.push(id)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function __fictPopSSRBoundary(expected?: string): void {
|
|
27
|
+
if (boundaryStack.length === 0) return
|
|
28
|
+
const top = boundaryStack[boundaryStack.length - 1]
|
|
29
|
+
if (expected && top !== expected) {
|
|
30
|
+
boundaryStack.pop()
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
boundaryStack.pop()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function __fictGetCurrentSSRBoundary(): string | null {
|
|
37
|
+
return boundaryStack.length > 0 ? boundaryStack[boundaryStack.length - 1]! : null
|
|
38
|
+
}
|
package/src/suspense.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from './lifecycle'
|
|
13
13
|
import { insertNodesBefore, removeNodes, toNodeArray } from './node-ops'
|
|
14
14
|
import { createSignal } from './signal'
|
|
15
|
+
import { __fictGetSSRStreamHooks, __fictPopSSRBoundary, __fictPushSSRBoundary } from './ssr-stream'
|
|
15
16
|
import type { BaseProps, FictNode, SuspenseToken } from './types'
|
|
16
17
|
|
|
17
18
|
export interface SuspenseProps extends BaseProps {
|
|
@@ -49,6 +50,7 @@ const isThenable = (value: unknown): value is PromiseLike<unknown> =>
|
|
|
49
50
|
typeof (value as PromiseLike<unknown>).then === 'function'
|
|
50
51
|
|
|
51
52
|
export function Suspense(props: SuspenseProps): FictNode {
|
|
53
|
+
const streamHooks = __fictGetSSRStreamHooks()
|
|
52
54
|
const pending = createSignal(0)
|
|
53
55
|
let resolvedOnce = false
|
|
54
56
|
let epoch = 0
|
|
@@ -76,7 +78,12 @@ export function Suspense(props: SuspenseProps): FictNode {
|
|
|
76
78
|
const root = createRootContext(hostRoot)
|
|
77
79
|
const prev = pushRoot(root)
|
|
78
80
|
let nodes: Node[] = []
|
|
81
|
+
let boundaryPushed = false
|
|
79
82
|
try {
|
|
83
|
+
if (streamBoundaryId) {
|
|
84
|
+
__fictPushSSRBoundary(streamBoundaryId)
|
|
85
|
+
boundaryPushed = true
|
|
86
|
+
}
|
|
80
87
|
const output = createElement(view)
|
|
81
88
|
nodes = toNodeArray(output)
|
|
82
89
|
// Suspended view: child threw a suspense token and was handled upstream.
|
|
@@ -90,9 +97,9 @@ export function Suspense(props: SuspenseProps): FictNode {
|
|
|
90
97
|
destroyRoot(root)
|
|
91
98
|
return
|
|
92
99
|
}
|
|
93
|
-
const parentNode =
|
|
100
|
+
const parentNode = endMarker.parentNode as (ParentNode & Node) | null
|
|
94
101
|
if (parentNode) {
|
|
95
|
-
insertNodesBefore(parentNode, nodes,
|
|
102
|
+
insertNodesBefore(parentNode, nodes, endMarker)
|
|
96
103
|
}
|
|
97
104
|
} catch (err) {
|
|
98
105
|
popRoot(prev)
|
|
@@ -101,6 +108,10 @@ export function Suspense(props: SuspenseProps): FictNode {
|
|
|
101
108
|
throw err
|
|
102
109
|
}
|
|
103
110
|
return
|
|
111
|
+
} finally {
|
|
112
|
+
if (boundaryPushed) {
|
|
113
|
+
__fictPopSSRBoundary(streamBoundaryId ?? undefined)
|
|
114
|
+
}
|
|
104
115
|
}
|
|
105
116
|
popRoot(prev)
|
|
106
117
|
flushOnMount(root)
|
|
@@ -113,10 +124,22 @@ export function Suspense(props: SuspenseProps): FictNode {
|
|
|
113
124
|
}
|
|
114
125
|
|
|
115
126
|
const fragment = document.createDocumentFragment()
|
|
116
|
-
const
|
|
117
|
-
|
|
127
|
+
const startMarker = document.createComment('fict:suspense-start')
|
|
128
|
+
const endMarker = document.createComment('fict:suspense-end')
|
|
129
|
+
fragment.appendChild(startMarker)
|
|
130
|
+
fragment.appendChild(endMarker)
|
|
118
131
|
let cleanup: (() => void) | undefined
|
|
119
132
|
let activeNodes: Node[] = []
|
|
133
|
+
let streamBoundaryId: string | null = null
|
|
134
|
+
let streamPending = false
|
|
135
|
+
|
|
136
|
+
if (streamHooks?.registerBoundary) {
|
|
137
|
+
streamBoundaryId = streamHooks.registerBoundary(startMarker, endMarker) ?? null
|
|
138
|
+
if (streamBoundaryId) {
|
|
139
|
+
startMarker.data = `fict:suspense-start:${streamBoundaryId}`
|
|
140
|
+
endMarker.data = `fict:suspense-end:${streamBoundaryId}`
|
|
141
|
+
}
|
|
142
|
+
}
|
|
120
143
|
|
|
121
144
|
const onResolveMaybe = () => {
|
|
122
145
|
if (!resolvedOnce) {
|
|
@@ -127,6 +150,10 @@ export function Suspense(props: SuspenseProps): FictNode {
|
|
|
127
150
|
|
|
128
151
|
registerSuspenseHandler(token => {
|
|
129
152
|
const tokenEpoch = epoch
|
|
153
|
+
if (!streamPending && streamBoundaryId && streamHooks?.boundaryPending) {
|
|
154
|
+
streamPending = true
|
|
155
|
+
streamHooks.boundaryPending(streamBoundaryId)
|
|
156
|
+
}
|
|
130
157
|
pending(pending() + 1)
|
|
131
158
|
// Directly render fallback instead of using switchView to avoid
|
|
132
159
|
// triggering the effect which would cause duplicate renders
|
|
@@ -155,6 +182,10 @@ export function Suspense(props: SuspenseProps): FictNode {
|
|
|
155
182
|
if (newPending === 0) {
|
|
156
183
|
// Directly render children instead of using switchView
|
|
157
184
|
renderView(props.children ?? null)
|
|
185
|
+
if (streamPending && streamBoundaryId && streamHooks?.boundaryResolved) {
|
|
186
|
+
streamPending = false
|
|
187
|
+
streamHooks.boundaryResolved(streamBoundaryId)
|
|
188
|
+
}
|
|
158
189
|
onResolveMaybe()
|
|
159
190
|
}
|
|
160
191
|
},
|
|
@@ -165,9 +196,29 @@ export function Suspense(props: SuspenseProps): FictNode {
|
|
|
165
196
|
}
|
|
166
197
|
const newPending = Math.max(0, pending() - 1)
|
|
167
198
|
pending(newPending)
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
199
|
+
let rejectionError = err
|
|
200
|
+
try {
|
|
201
|
+
props.onReject?.(err)
|
|
202
|
+
} catch (callbackError) {
|
|
203
|
+
rejectionError = callbackError
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const handled = handleError(rejectionError, { source: 'render' }, hostRoot)
|
|
207
|
+
if (!handled) {
|
|
208
|
+
if (streamHooks?.onError) {
|
|
209
|
+
streamHooks.onError(rejectionError, streamBoundaryId ?? undefined)
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
throw rejectionError
|
|
213
|
+
}
|
|
214
|
+
if (
|
|
215
|
+
newPending === 0 &&
|
|
216
|
+
streamPending &&
|
|
217
|
+
streamBoundaryId &&
|
|
218
|
+
streamHooks?.boundaryResolved
|
|
219
|
+
) {
|
|
220
|
+
streamPending = false
|
|
221
|
+
streamHooks.boundaryResolved(streamBoundaryId)
|
|
171
222
|
}
|
|
172
223
|
},
|
|
173
224
|
)
|
|
@@ -195,6 +246,10 @@ export function Suspense(props: SuspenseProps): FictNode {
|
|
|
195
246
|
pending(0)
|
|
196
247
|
// Directly render children instead of using switchView
|
|
197
248
|
renderView(props.children ?? null)
|
|
249
|
+
if (streamPending && streamBoundaryId && streamHooks?.boundaryResolved) {
|
|
250
|
+
streamPending = false
|
|
251
|
+
streamHooks.boundaryResolved(streamBoundaryId)
|
|
252
|
+
}
|
|
198
253
|
}
|
|
199
254
|
})
|
|
200
255
|
}
|