kiru 1.2.0 → 1.3.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/appHandle.d.ts.map +1 -1
- package/dist/appHandle.js +4 -5
- package/dist/appHandle.js.map +1 -1
- package/dist/components/derive.d.ts +8 -7
- package/dist/components/derive.d.ts.map +1 -1
- package/dist/components/derive.js +10 -11
- package/dist/components/derive.js.map +1 -1
- package/dist/components/errorBoundary.d.ts +2 -2
- package/dist/components/errorBoundary.d.ts.map +1 -1
- package/dist/components/errorBoundary.js +1 -1
- package/dist/components/errorBoundary.js.map +1 -1
- package/dist/components/for.d.ts +1 -1
- package/dist/components/for.d.ts.map +1 -1
- package/dist/components/for.js +1 -1
- package/dist/components/lazy.d.ts +1 -1
- package/dist/components/lazy.js +1 -1
- package/dist/components/portal.d.ts +1 -1
- package/dist/components/portal.js +1 -1
- package/dist/components/show.d.ts +1 -1
- package/dist/components/show.js +1 -1
- package/dist/components/transition.d.ts +1 -1
- package/dist/components/transition.d.ts.map +1 -1
- package/dist/components/transition.js +15 -12
- package/dist/components/transition.js.map +1 -1
- package/dist/dom/nodes.d.ts.map +1 -1
- package/dist/dom/nodes.js +5 -1
- package/dist/dom/nodes.js.map +1 -1
- package/dist/dom/props.d.ts.map +1 -1
- package/dist/dom/props.js +23 -4
- package/dist/dom/props.js.map +1 -1
- package/dist/error.d.ts +0 -2
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js +11 -14
- package/dist/error.js.map +1 -1
- package/dist/headlessRender.d.ts +2 -2
- package/dist/headlessRender.d.ts.map +1 -1
- package/dist/headlessRender.js +2 -3
- package/dist/headlessRender.js.map +1 -1
- package/dist/hmr.d.ts +1 -0
- package/dist/hmr.d.ts.map +1 -1
- package/dist/hmr.js +6 -2
- package/dist/hmr.js.map +1 -1
- package/dist/hooks/setup.d.ts +1 -1
- package/dist/hooks/setup.d.ts.map +1 -1
- package/dist/hooks/setup.js +110 -6
- package/dist/hooks/setup.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/renderToString.js +1 -1
- package/dist/renderToString.js.map +1 -1
- package/dist/resource.d.ts +19 -0
- package/dist/resource.d.ts.map +1 -0
- package/dist/resource.js +167 -0
- package/dist/resource.js.map +1 -0
- package/dist/scheduler.d.ts.map +1 -1
- package/dist/scheduler.js +15 -11
- package/dist/scheduler.js.map +1 -1
- package/dist/signals/base.d.ts +3 -5
- package/dist/signals/base.d.ts.map +1 -1
- package/dist/signals/base.js +24 -47
- package/dist/signals/base.js.map +1 -1
- package/dist/signals/computed.d.ts.map +1 -1
- package/dist/signals/computed.js +14 -12
- package/dist/signals/computed.js.map +1 -1
- package/dist/signals/globals.d.ts +0 -2
- package/dist/signals/globals.d.ts.map +1 -1
- package/dist/signals/globals.js +0 -1
- package/dist/signals/globals.js.map +1 -1
- package/dist/signals/types.d.ts +1 -1
- package/dist/signals/types.d.ts.map +1 -1
- package/dist/ssr/server.d.ts +4 -4
- package/dist/ssr/server.d.ts.map +1 -1
- package/dist/ssr/server.js +11 -7
- package/dist/ssr/server.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/index.d.ts +2 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +2 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/stream.d.ts +12 -0
- package/dist/utils/stream.d.ts.map +1 -0
- package/dist/utils/stream.js +8 -0
- package/dist/utils/stream.js.map +1 -0
- package/dist/utils/vdom.d.ts +2 -1
- package/dist/utils/vdom.d.ts.map +1 -1
- package/dist/utils/vdom.js +4 -1
- package/dist/utils/vdom.js.map +1 -1
- package/package.json +1 -13
- package/src/appHandle.ts +6 -7
- package/src/components/derive.ts +34 -40
- package/src/components/errorBoundary.ts +4 -2
- package/src/components/for.ts +34 -34
- package/src/components/lazy.ts +1 -1
- package/src/components/portal.ts +1 -1
- package/src/components/show.ts +32 -32
- package/src/components/transition.ts +16 -11
- package/src/dom/nodes.ts +5 -2
- package/src/dom/props.ts +28 -3
- package/src/error.ts +11 -19
- package/src/headlessRender.ts +5 -5
- package/src/hmr.ts +13 -3
- package/src/hooks/setup.ts +143 -9
- package/src/index.ts +1 -1
- package/src/renderToString.ts +1 -1
- package/src/resource.ts +207 -0
- package/src/scheduler.ts +16 -11
- package/src/signals/base.ts +29 -52
- package/src/signals/computed.ts +13 -9
- package/src/signals/globals.ts +0 -3
- package/src/signals/types.ts +1 -1
- package/src/ssr/server.ts +18 -11
- package/src/types.ts +4 -4
- package/src/utils/index.ts +2 -1
- package/src/utils/stream.ts +17 -0
- package/src/utils/vdom.ts +5 -0
- package/dist/statefulPromise.d.ts +0 -22
- package/dist/statefulPromise.d.ts.map +0 -1
- package/dist/statefulPromise.js +0 -94
- package/dist/statefulPromise.js.map +0 -1
- package/src/statefulPromise.ts +0 -136
package/src/error.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { $KIRU_ERROR } from "./constants.js"
|
|
1
|
+
import { $DEV_FILE_LINK, $KIRU_ERROR } from "./constants.js"
|
|
2
2
|
import { __DEV__ } from "./env.js"
|
|
3
|
-
import { findParent, noop } from "./utils/index.js"
|
|
4
3
|
|
|
5
4
|
type KiruErrorOptions =
|
|
6
5
|
| string
|
|
@@ -16,8 +15,6 @@ export class KiruError extends Error {
|
|
|
16
15
|
[$KIRU_ERROR] = true
|
|
17
16
|
/** Indicates whether the error is fatal and should crash the application */
|
|
18
17
|
fatal?: boolean
|
|
19
|
-
/** Present if vNode is provided */
|
|
20
|
-
customNodeStack?: string
|
|
21
18
|
constructor(optionsOrMessage: KiruErrorOptions) {
|
|
22
19
|
const message =
|
|
23
20
|
typeof optionsOrMessage === "string"
|
|
@@ -26,18 +23,21 @@ export class KiruError extends Error {
|
|
|
26
23
|
super(message)
|
|
27
24
|
if (typeof optionsOrMessage !== "string") {
|
|
28
25
|
if (__DEV__ && optionsOrMessage?.vNode) {
|
|
29
|
-
|
|
26
|
+
const stack = createVNodeStack(optionsOrMessage.vNode)
|
|
27
|
+
this.message = `${message}
|
|
28
|
+
${stack.map((item) => ` at ${item}`).join("\n")}
|
|
29
|
+
`
|
|
30
30
|
}
|
|
31
31
|
this.fatal = optionsOrMessage?.fatal
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
static isKiruError(error: unknown): error is KiruError {
|
|
36
|
-
return error instanceof Error &&
|
|
36
|
+
return error instanceof Error && $KIRU_ERROR in error
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
function
|
|
40
|
+
function createVNodeStack(vNode: Kiru.VNode) {
|
|
41
41
|
let n = vNode
|
|
42
42
|
let componentFns: string[] = []
|
|
43
43
|
while (n) {
|
|
@@ -45,24 +45,16 @@ function captureErrorStack(vNode: Kiru.VNode) {
|
|
|
45
45
|
if (typeof n.type === "function") {
|
|
46
46
|
componentFns.push(getComponentErrorDisplayText(n.type))
|
|
47
47
|
} else if (typeof n.type === "string") {
|
|
48
|
-
componentFns.push(n.type)
|
|
48
|
+
componentFns.push(`<${n.type}>`)
|
|
49
49
|
}
|
|
50
50
|
n = n.parent
|
|
51
51
|
}
|
|
52
|
-
const componentNode = (
|
|
53
|
-
typeof vNode.type === "function"
|
|
54
|
-
? vNode
|
|
55
|
-
: findParent(vNode, (n) => typeof n.type === "function")
|
|
56
|
-
) as (Kiru.VNode & { type: Function }) | null
|
|
57
|
-
return `The above error occurred in the <${getFunctionName(
|
|
58
|
-
componentNode?.type || noop
|
|
59
|
-
)}> component:
|
|
60
52
|
|
|
61
|
-
|
|
53
|
+
return componentFns
|
|
62
54
|
}
|
|
63
55
|
|
|
64
56
|
function getComponentErrorDisplayText(fn: Function) {
|
|
65
|
-
let str = getFunctionName(fn)
|
|
57
|
+
let str = `<${getFunctionName(fn)}>`
|
|
66
58
|
if (__DEV__) {
|
|
67
59
|
const fileLink = getComponentFileLink(fn)
|
|
68
60
|
if (fileLink) {
|
|
@@ -77,5 +69,5 @@ function getFunctionName(fn: Function) {
|
|
|
77
69
|
}
|
|
78
70
|
|
|
79
71
|
function getComponentFileLink(fn: Function) {
|
|
80
|
-
return fn
|
|
72
|
+
return (fn as any)[$DEV_FILE_LINK] ?? null
|
|
81
73
|
}
|
package/src/headlessRender.ts
CHANGED
|
@@ -7,8 +7,8 @@ import {
|
|
|
7
7
|
assertValidElementProps,
|
|
8
8
|
isPrimitiveChild,
|
|
9
9
|
isValidTextChild,
|
|
10
|
+
isStreamDataThrowValue,
|
|
10
11
|
} from "./utils/index.js"
|
|
11
|
-
import { isStreamDataThrowValue } from "./statefulPromise.js"
|
|
12
12
|
import { Signal } from "./signals/base.js"
|
|
13
13
|
import { $ERROR_BOUNDARY, voidElements, $STREAM_DATA } from "./constants.js"
|
|
14
14
|
import { __DEV__ } from "./env.js"
|
|
@@ -16,14 +16,14 @@ import type { ErrorBoundaryNode } from "./types.utils"
|
|
|
16
16
|
|
|
17
17
|
export interface HeadlessRenderContext {
|
|
18
18
|
write(chunk: string): void
|
|
19
|
-
onStreamData?: (data: Kiru.
|
|
19
|
+
onStreamData?: (data: Kiru.StatefulPromise<unknown>[]) => void
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export function headlessRender(
|
|
23
23
|
ctx: HeadlessRenderContext,
|
|
24
24
|
el: unknown,
|
|
25
|
-
parent: Kiru.VNode | null,
|
|
26
|
-
idx: number
|
|
25
|
+
parent: Kiru.VNode | null = null,
|
|
26
|
+
idx: number = 0
|
|
27
27
|
): void {
|
|
28
28
|
if (el === null) return
|
|
29
29
|
if (el === undefined) return
|
|
@@ -66,7 +66,7 @@ export function headlessRender(
|
|
|
66
66
|
if (isExoticType(type)) {
|
|
67
67
|
if (type === $ERROR_BOUNDARY) {
|
|
68
68
|
let boundaryBuffer = ""
|
|
69
|
-
const streamPromises = new Set<Kiru.
|
|
69
|
+
const streamPromises = new Set<Kiru.StatefulPromise<unknown>>()
|
|
70
70
|
const boundaryCtx: HeadlessRenderContext = {
|
|
71
71
|
write(chunk) {
|
|
72
72
|
boundaryBuffer += chunk
|
package/src/hmr.ts
CHANGED
|
@@ -98,6 +98,8 @@ export function createHmrContext() {
|
|
|
98
98
|
if (currentModuleMemory === null)
|
|
99
99
|
throw new Error("[kiru]: HMR could not register: No active module")
|
|
100
100
|
|
|
101
|
+
// TODO: we should call destroy() on unmatched old entries
|
|
102
|
+
|
|
101
103
|
let dirtyNodes = new Set<Kiru.VNode>()
|
|
102
104
|
for (const [name, newEntry] of Object.entries(hotVarRegistrationEntries)) {
|
|
103
105
|
const oldEntry = currentModuleMemory.hotVars.get(name)
|
|
@@ -120,10 +122,10 @@ export function createHmrContext() {
|
|
|
120
122
|
isGenericHmrAcceptor(oldEntry.value) &&
|
|
121
123
|
isGenericHmrAcceptor(newEntry.value)
|
|
122
124
|
) {
|
|
123
|
-
|
|
124
|
-
oldEntry.value[$HMR_ACCEPT]
|
|
125
|
+
performHmrAccept(
|
|
126
|
+
oldEntry.value[$HMR_ACCEPT],
|
|
127
|
+
newEntry.value[$HMR_ACCEPT]
|
|
125
128
|
)
|
|
126
|
-
oldEntry.value[$HMR_ACCEPT].destroy()
|
|
127
129
|
continue
|
|
128
130
|
}
|
|
129
131
|
if (oldEntry.type === "component" && newEntry.type === "component") {
|
|
@@ -192,3 +194,11 @@ export function onHmr(callback: () => void): void {
|
|
|
192
194
|
window.__kiru.HMRContext.onHmr(callback)
|
|
193
195
|
}
|
|
194
196
|
}
|
|
197
|
+
|
|
198
|
+
export function performHmrAccept<T>(
|
|
199
|
+
oldThing: HMRAccept<T>,
|
|
200
|
+
newThing: HMRAccept<T>
|
|
201
|
+
) {
|
|
202
|
+
newThing.inject(oldThing.provide())
|
|
203
|
+
oldThing.destroy()
|
|
204
|
+
}
|
package/src/hooks/setup.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { signal, Signal } from "../signals/base.js"
|
|
2
2
|
import { createVNodeId } from "../utils/vdom.js"
|
|
3
3
|
import { __DEV__ } from "../env.js"
|
|
4
4
|
import { node, setups } from "../globals.js"
|
|
5
|
-
import
|
|
5
|
+
import {
|
|
6
|
+
tracking,
|
|
7
|
+
type TrackingStackObservations,
|
|
8
|
+
} from "../signals/tracking.js"
|
|
9
|
+
import { registerVNodeCleanup } from "../utils/index.js"
|
|
6
10
|
|
|
7
11
|
export interface Setup<Props extends {}> {
|
|
8
12
|
readonly derive: <T>(
|
|
@@ -36,7 +40,6 @@ export function setup<Props extends {}>(): Setup<Props> {
|
|
|
36
40
|
setups.set(vNode, setup)
|
|
37
41
|
return setup
|
|
38
42
|
}
|
|
39
|
-
|
|
40
43
|
function createSetup<Props extends {}>(vNode: Kiru.VNode): Setup<Props> {
|
|
41
44
|
let id: Signal<string>
|
|
42
45
|
|
|
@@ -45,12 +48,89 @@ function createSetup<Props extends {}>(vNode: Kiru.VNode): Setup<Props> {
|
|
|
45
48
|
|
|
46
49
|
let prevIndex = -1
|
|
47
50
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
// Always points at latest props (updated in propSync) so selector and subs see current props
|
|
52
|
+
const currentProps = { current: { ...vNode.props } as InferredProps }
|
|
53
|
+
const deriveCleanups: Array<() => void> = []
|
|
54
|
+
|
|
55
|
+
type DeriveEntry = { run: () => void; accessedPaths: Set<string> }
|
|
56
|
+
const deriveEntries: DeriveEntry[] = []
|
|
57
|
+
|
|
58
|
+
propSyncs.push((p) => {
|
|
59
|
+
const old = currentProps.current as Record<string, unknown>
|
|
60
|
+
const skip = new Set<DeriveEntry>()
|
|
61
|
+
for (const entry of deriveEntries) {
|
|
62
|
+
if (
|
|
63
|
+
entry.accessedPaths.size > 0 &&
|
|
64
|
+
propsUnchangedAtPaths(
|
|
65
|
+
old,
|
|
66
|
+
p as Record<string, unknown>,
|
|
67
|
+
entry.accessedPaths
|
|
68
|
+
)
|
|
69
|
+
) {
|
|
70
|
+
skip.add(entry)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
currentProps.current = p
|
|
74
|
+
for (const entry of deriveEntries) {
|
|
75
|
+
if (!skip.has(entry)) entry.run()
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
registerVNodeCleanup(vNode, "vnode:setup", () => {
|
|
80
|
+
for (const cleanup of deriveCleanups) cleanup()
|
|
81
|
+
setups.delete(vNode)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const setupResult: Setup<Props> = {
|
|
85
|
+
derive<T>(
|
|
86
|
+
selector: (props: Props extends Kiru.FC<infer P> ? P : Props) => T
|
|
87
|
+
) {
|
|
88
|
+
const resultSig = signal(undefined!) as Signal<T>
|
|
89
|
+
const unsubs = new Map<string, () => void>()
|
|
90
|
+
const accessedPaths = new Set<string>()
|
|
91
|
+
|
|
92
|
+
function sync() {
|
|
93
|
+
accessedPaths.clear()
|
|
94
|
+
const propsProxy = createPropsProxy(
|
|
95
|
+
currentProps.current as Record<string, unknown>,
|
|
96
|
+
accessedPaths
|
|
97
|
+
) as InferredProps
|
|
98
|
+
const observations: TrackingStackObservations = new Map()
|
|
99
|
+
tracking.stack.push(observations)
|
|
100
|
+
const value = selector(propsProxy)
|
|
101
|
+
tracking.stack.pop()
|
|
102
|
+
// Always assign and notify so the component re-renders when the derived value changes
|
|
103
|
+
// (e.g. when parent passes a different signal ref like toggle switching count/double).
|
|
104
|
+
resultSig.value = value
|
|
105
|
+
|
|
106
|
+
for (const [sid, unsub] of unsubs) {
|
|
107
|
+
if (!observations.has(sid)) {
|
|
108
|
+
unsub()
|
|
109
|
+
unsubs.delete(sid)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
for (const [sid, observedSig] of observations) {
|
|
113
|
+
if (!unsubs.has(sid)) {
|
|
114
|
+
try {
|
|
115
|
+
unsubs.set(sid, observedSig.subscribe(sync))
|
|
116
|
+
} catch {
|
|
117
|
+
// Signal may be disposed after HMR; skip subscribing
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
sync()
|
|
124
|
+
const entry: DeriveEntry = { run: sync, accessedPaths }
|
|
125
|
+
deriveEntries.push(entry)
|
|
126
|
+
deriveCleanups.push(() => {
|
|
127
|
+
unsubs.forEach((u) => u())
|
|
128
|
+
unsubs.clear()
|
|
129
|
+
const i = deriveEntries.indexOf(entry)
|
|
130
|
+
if (i !== -1) deriveEntries.splice(i, 1)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
return resultSig
|
|
54
134
|
},
|
|
55
135
|
get id() {
|
|
56
136
|
if (!id) {
|
|
@@ -67,4 +147,58 @@ function createSetup<Props extends {}>(vNode: Kiru.VNode): Setup<Props> {
|
|
|
67
147
|
return id
|
|
68
148
|
},
|
|
69
149
|
}
|
|
150
|
+
return setupResult
|
|
151
|
+
}
|
|
152
|
+
function propsUnchangedAtPaths(
|
|
153
|
+
oldProps: Record<string, unknown>,
|
|
154
|
+
newProps: Record<string, unknown>,
|
|
155
|
+
paths: Set<string>
|
|
156
|
+
): boolean {
|
|
157
|
+
for (const path of paths) {
|
|
158
|
+
if (!Object.is(getAtPath(oldProps, path), getAtPath(newProps, path))) {
|
|
159
|
+
return false
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return true
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function getAtPath(obj: Record<string, unknown>, path: string): unknown {
|
|
166
|
+
let cur: unknown = obj
|
|
167
|
+
for (const key of path.split(".")) {
|
|
168
|
+
if (cur == null || typeof cur !== "object") return undefined
|
|
169
|
+
cur = (cur as Record<string, unknown>)[key]
|
|
170
|
+
}
|
|
171
|
+
return cur
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Proxy that records paths and wraps signals. We only add to accessedPaths when
|
|
176
|
+
* we hit a signal (the leaf we subscribe to), so propSync skip only compares
|
|
177
|
+
* signal refs. Container objects (e.g. "data") are new every render and would
|
|
178
|
+
* always fail the skip.
|
|
179
|
+
*/
|
|
180
|
+
function createPropsProxy<P extends Record<string, unknown>>(
|
|
181
|
+
props: P,
|
|
182
|
+
accessedPaths: Set<string>,
|
|
183
|
+
pathPrefix?: string
|
|
184
|
+
): P {
|
|
185
|
+
return new Proxy(props, {
|
|
186
|
+
get(holder, key: string) {
|
|
187
|
+
const path = pathPrefix ? `${pathPrefix}.${key}` : key
|
|
188
|
+
const v = holder[key]
|
|
189
|
+
if (Signal.isSignal(v)) {
|
|
190
|
+
accessedPaths.add(path) // only record path for signal leaves
|
|
191
|
+
return v
|
|
192
|
+
}
|
|
193
|
+
if (v !== null && typeof v === "object" && !Array.isArray(v)) {
|
|
194
|
+
return createPropsProxy(
|
|
195
|
+
v as Record<string, unknown>,
|
|
196
|
+
accessedPaths,
|
|
197
|
+
path
|
|
198
|
+
) as P[keyof P]
|
|
199
|
+
}
|
|
200
|
+
accessedPaths.add(path) // primitive leaf
|
|
201
|
+
return v
|
|
202
|
+
},
|
|
203
|
+
}) as P
|
|
70
204
|
}
|
package/src/index.ts
CHANGED
package/src/renderToString.ts
CHANGED
package/src/resource.ts
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { $HMR_ACCEPT, STREAMED_DATA_EVENT } from "./constants.js"
|
|
2
|
+
import { hydrationMode, node, renderMode } from "./globals.js"
|
|
3
|
+
import { Signal, signal } from "./signals/base.js"
|
|
4
|
+
import { createVNodeId, registerVNodeCleanup } from "./utils/vdom.js"
|
|
5
|
+
import { generateRandomID } from "./utils/generateId.js"
|
|
6
|
+
import { __DEV__, isBrowser } from "./env.js"
|
|
7
|
+
import { GenericHMRAcceptor, performHmrAccept } from "./hmr.js"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Returns true if the value is a {@link Resource}
|
|
11
|
+
*/
|
|
12
|
+
export function isResource(thing: unknown): thing is Resource<unknown> {
|
|
13
|
+
return (
|
|
14
|
+
Signal.isSignal(thing) &&
|
|
15
|
+
"promise" in thing &&
|
|
16
|
+
thing["promise"] instanceof Promise
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const resourceMeta = new WeakMap<Kiru.VNode, { id: string; index: number }>()
|
|
21
|
+
|
|
22
|
+
interface ResourceState<T> {
|
|
23
|
+
error: Signal<Error | null>
|
|
24
|
+
isPending: Signal<boolean>
|
|
25
|
+
promise: Kiru.StatefulPromise<T>
|
|
26
|
+
refetch: () => void
|
|
27
|
+
dispose: () => void
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type Resource<T> = Kiru.Signal<T> & ResourceState<T>
|
|
31
|
+
export interface ResourceLoaderContext {
|
|
32
|
+
signal: AbortSignal
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function resource<T, Source>(
|
|
36
|
+
source: Kiru.Signal<Source>,
|
|
37
|
+
callback: (source: Source, ctx: ResourceLoaderContext) => Promise<T>
|
|
38
|
+
): Resource<T> {
|
|
39
|
+
const data = signal(void 0 as T)
|
|
40
|
+
const error = signal<Error | null>(null)
|
|
41
|
+
const isPending = signal(true)
|
|
42
|
+
|
|
43
|
+
let promiseId = ""
|
|
44
|
+
const vNode = node.current
|
|
45
|
+
if (!vNode) {
|
|
46
|
+
// todo: investigate streaming global resources via SSR
|
|
47
|
+
// likely cooked since we can't ensure modules are loaded in the same order,
|
|
48
|
+
} else if (
|
|
49
|
+
renderMode.current === "hydrate" ||
|
|
50
|
+
renderMode.current === "stream"
|
|
51
|
+
) {
|
|
52
|
+
// hydrate or stream - create a deterministic id + index offset to use for promise hydration
|
|
53
|
+
const { id, index } = resourceMeta.get(vNode) ?? {
|
|
54
|
+
id: createVNodeId(vNode),
|
|
55
|
+
index: 0,
|
|
56
|
+
}
|
|
57
|
+
promiseId = `${id}:resource:${index}`
|
|
58
|
+
resourceMeta.set(vNode, { id, index: index + 1 })
|
|
59
|
+
} else {
|
|
60
|
+
// could be improved. For now, just use a random id to prevent collisions on the cleanups map.
|
|
61
|
+
// in future, we could implement a cached id based on the vNode for use across other modules too.
|
|
62
|
+
promiseId = generateRandomID()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const unsub = source.subscribe((src) => {
|
|
66
|
+
resource.promise = createPromise(src)
|
|
67
|
+
resource.notify()
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
let controller = new AbortController()
|
|
71
|
+
const dispose = () => {
|
|
72
|
+
if (!controller.signal.aborted) controller.abort()
|
|
73
|
+
Signal.dispose(data)
|
|
74
|
+
Signal.dispose(isPending)
|
|
75
|
+
unsub()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (vNode) {
|
|
79
|
+
registerVNodeCleanup(vNode, promiseId, dispose)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const resource: Resource<T> = Object.assign(data, {
|
|
83
|
+
error,
|
|
84
|
+
isPending,
|
|
85
|
+
promise: undefined as unknown as Kiru.StatefulPromise<T>,
|
|
86
|
+
refetch() {
|
|
87
|
+
data.value = void 0 as T
|
|
88
|
+
this.promise = createPromise(source.peek())
|
|
89
|
+
},
|
|
90
|
+
dispose,
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
if (__DEV__) {
|
|
94
|
+
const { inject: baseInject, destroy: baseDestroy } = data[$HMR_ACCEPT]!
|
|
95
|
+
|
|
96
|
+
;(resource as any as GenericHMRAcceptor<Resource<T>>)[$HMR_ACCEPT] = {
|
|
97
|
+
provide: () => {
|
|
98
|
+
return resource
|
|
99
|
+
},
|
|
100
|
+
destroy: () => {
|
|
101
|
+
baseDestroy()
|
|
102
|
+
controller.abort()
|
|
103
|
+
},
|
|
104
|
+
inject: (prev) => {
|
|
105
|
+
baseInject(prev)
|
|
106
|
+
const { isPending: prevPending, error: prevError } = prev
|
|
107
|
+
const { isPending, error } = resource
|
|
108
|
+
performHmrAccept(prevPending[$HMR_ACCEPT]!, isPending[$HMR_ACCEPT]!)
|
|
109
|
+
performHmrAccept(prevError[$HMR_ACCEPT]!, error[$HMR_ACCEPT]!)
|
|
110
|
+
},
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (__DEV__ && isBrowser && window.__kiru.HMRContext?.isReplacement()) {
|
|
115
|
+
queueMicrotask(() => {
|
|
116
|
+
resource.promise = createPromise(source.peek())
|
|
117
|
+
})
|
|
118
|
+
} else {
|
|
119
|
+
resource.promise = createPromise(source.peek())
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function createPromise(source: Source): Kiru.StatefulPromise<T> {
|
|
123
|
+
controller.abort()
|
|
124
|
+
const ctrl = (controller = new AbortController())
|
|
125
|
+
isPending.value = true
|
|
126
|
+
let newPromise: Promise<T>
|
|
127
|
+
if (renderMode.current === "string") {
|
|
128
|
+
// if we're rendering to a string, there's no need to fire the callback
|
|
129
|
+
newPromise = Promise.resolve() as Promise<T>
|
|
130
|
+
} else if (
|
|
131
|
+
renderMode.current === "hydrate" &&
|
|
132
|
+
hydrationMode.current === "dynamic"
|
|
133
|
+
) {
|
|
134
|
+
// if we're hydrating and the hydration mode is not static,
|
|
135
|
+
// we need to resolve the promise from cache/event
|
|
136
|
+
newPromise = resolveDeferredPromise<T>(promiseId, ctrl.signal)
|
|
137
|
+
} else {
|
|
138
|
+
// stream / dom / (hydrate + static)
|
|
139
|
+
newPromise = callback(source, { signal: ctrl.signal })
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const statefulPromise: Kiru.StatefulPromise<T> = Object.assign(newPromise, {
|
|
143
|
+
id: promiseId,
|
|
144
|
+
state: "pending",
|
|
145
|
+
} satisfies Kiru.PromiseState<T>)
|
|
146
|
+
|
|
147
|
+
statefulPromise
|
|
148
|
+
.then((value) => {
|
|
149
|
+
statefulPromise.state = "fulfilled"
|
|
150
|
+
statefulPromise.value = value
|
|
151
|
+
data.value = value
|
|
152
|
+
isPending.value = false
|
|
153
|
+
error.value = null
|
|
154
|
+
})
|
|
155
|
+
.catch((e) => {
|
|
156
|
+
if (ctrl !== controller) return // prevent setting pending=false if new controller was recreated (new promise)
|
|
157
|
+
statefulPromise.state = "rejected"
|
|
158
|
+
statefulPromise.error = e instanceof Error ? e : new Error(e)
|
|
159
|
+
error.value = statefulPromise.error
|
|
160
|
+
isPending.value = false
|
|
161
|
+
})
|
|
162
|
+
return statefulPromise
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return resource
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
interface DeferredPromiseEventDetail<T> {
|
|
169
|
+
id: string
|
|
170
|
+
data?: T
|
|
171
|
+
error?: string
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function resolveDeferredPromise<T>(
|
|
175
|
+
id: string,
|
|
176
|
+
signal: AbortSignal
|
|
177
|
+
): Promise<T> {
|
|
178
|
+
return new Promise<T>((resolve, reject) => {
|
|
179
|
+
const deferralCache: Map<string, { data?: T; error?: string }> = // @ts-ignore
|
|
180
|
+
(window[STREAMED_DATA_EVENT] ??= new Map())
|
|
181
|
+
|
|
182
|
+
const existing = deferralCache.get(id)
|
|
183
|
+
if (existing) {
|
|
184
|
+
const { data, error } = existing
|
|
185
|
+
deferralCache.delete(id)
|
|
186
|
+
if (error) return reject(error)
|
|
187
|
+
return resolve(data!)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const onDataEvent = (event: Event) => {
|
|
191
|
+
const { detail } = event as CustomEvent<DeferredPromiseEventDetail<T>>
|
|
192
|
+
if (detail.id === id) {
|
|
193
|
+
deferralCache.delete(id)
|
|
194
|
+
window.removeEventListener(STREAMED_DATA_EVENT, onDataEvent)
|
|
195
|
+
const { data, error } = detail
|
|
196
|
+
if (error) return reject(error)
|
|
197
|
+
resolve(data!)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
window.addEventListener(STREAMED_DATA_EVENT, onDataEvent)
|
|
202
|
+
signal.addEventListener("abort", () => {
|
|
203
|
+
window.removeEventListener(STREAMED_DATA_EVENT, onDataEvent)
|
|
204
|
+
reject()
|
|
205
|
+
})
|
|
206
|
+
})
|
|
207
|
+
}
|
package/src/scheduler.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
findParentErrorBoundary,
|
|
23
23
|
call,
|
|
24
24
|
propsChanged,
|
|
25
|
+
depthSort,
|
|
25
26
|
} from "./utils/index.js"
|
|
26
27
|
import { __DEV__ } from "./env.js"
|
|
27
28
|
import { KiruError } from "./error.js"
|
|
@@ -129,8 +130,6 @@ function queueDelete(vNode: VNode): void {
|
|
|
129
130
|
deletions.push(vNode)
|
|
130
131
|
}
|
|
131
132
|
|
|
132
|
-
const depthSort = (a: VNode, b: VNode): number => b.depth - a.depth
|
|
133
|
-
|
|
134
133
|
let currentWorkRoot: VNode | null = null
|
|
135
134
|
|
|
136
135
|
function doWork(): void {
|
|
@@ -281,11 +280,6 @@ function updateVNode(vNode: VNode): VNode | null {
|
|
|
281
280
|
}
|
|
282
281
|
|
|
283
282
|
if (KiruError.isKiruError(error)) {
|
|
284
|
-
if (error.customNodeStack) {
|
|
285
|
-
setTimeout(() => {
|
|
286
|
-
throw new Error(error.customNodeStack)
|
|
287
|
-
})
|
|
288
|
-
}
|
|
289
283
|
if (error.fatal) {
|
|
290
284
|
throw error
|
|
291
285
|
}
|
|
@@ -390,7 +384,7 @@ function renderFunctionComponent(
|
|
|
390
384
|
props: Record<string, unknown>,
|
|
391
385
|
shouldSyncProps: boolean
|
|
392
386
|
): unknown {
|
|
393
|
-
|
|
387
|
+
let { render, propSyncs } = vNode
|
|
394
388
|
|
|
395
389
|
if (render) {
|
|
396
390
|
if (shouldSyncProps) {
|
|
@@ -400,9 +394,20 @@ function renderFunctionComponent(
|
|
|
400
394
|
return render(props)
|
|
401
395
|
}
|
|
402
396
|
|
|
403
|
-
|
|
397
|
+
if (__DEV__) {
|
|
398
|
+
type = latest(type)
|
|
399
|
+
if (typeof type !== "function") {
|
|
400
|
+
throw new KiruError({
|
|
401
|
+
message: `received invalid component type: ${type}`,
|
|
402
|
+
fatal: true,
|
|
403
|
+
vNode,
|
|
404
|
+
})
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
let newChild = type(props)
|
|
404
409
|
if (typeof newChild === "function") {
|
|
405
|
-
vNode.subs?.forEach(call) // unsub from signals
|
|
410
|
+
vNode.subs?.forEach(call) // unsub from signals observed during setup
|
|
406
411
|
vNode.render = newChild
|
|
407
412
|
if (shouldSyncProps) {
|
|
408
413
|
const p = { ...props }
|
|
@@ -445,7 +450,7 @@ function updateHostComponent(vNode: DomVNode): VNode | null {
|
|
|
445
450
|
function checkForTooManyConsecutiveDirtyRenders(): void {
|
|
446
451
|
if (consecutiveDirtyCount > CONSECUTIVE_DIRTY_LIMIT) {
|
|
447
452
|
throw new KiruError(
|
|
448
|
-
"Maximum update depth exceeded. This can happen when a component repeatedly
|
|
453
|
+
"Maximum update depth exceeded. This can happen when a component repeatedly updates during render or in onBeforeMount. Kiru limits the number of nested updates to prevent infinite loops."
|
|
449
454
|
)
|
|
450
455
|
}
|
|
451
456
|
}
|