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.
Files changed (123) hide show
  1. package/dist/appHandle.d.ts.map +1 -1
  2. package/dist/appHandle.js +4 -5
  3. package/dist/appHandle.js.map +1 -1
  4. package/dist/components/derive.d.ts +8 -7
  5. package/dist/components/derive.d.ts.map +1 -1
  6. package/dist/components/derive.js +10 -11
  7. package/dist/components/derive.js.map +1 -1
  8. package/dist/components/errorBoundary.d.ts +2 -2
  9. package/dist/components/errorBoundary.d.ts.map +1 -1
  10. package/dist/components/errorBoundary.js +1 -1
  11. package/dist/components/errorBoundary.js.map +1 -1
  12. package/dist/components/for.d.ts +1 -1
  13. package/dist/components/for.d.ts.map +1 -1
  14. package/dist/components/for.js +1 -1
  15. package/dist/components/lazy.d.ts +1 -1
  16. package/dist/components/lazy.js +1 -1
  17. package/dist/components/portal.d.ts +1 -1
  18. package/dist/components/portal.js +1 -1
  19. package/dist/components/show.d.ts +1 -1
  20. package/dist/components/show.js +1 -1
  21. package/dist/components/transition.d.ts +1 -1
  22. package/dist/components/transition.d.ts.map +1 -1
  23. package/dist/components/transition.js +15 -12
  24. package/dist/components/transition.js.map +1 -1
  25. package/dist/dom/nodes.d.ts.map +1 -1
  26. package/dist/dom/nodes.js +5 -1
  27. package/dist/dom/nodes.js.map +1 -1
  28. package/dist/dom/props.d.ts.map +1 -1
  29. package/dist/dom/props.js +23 -4
  30. package/dist/dom/props.js.map +1 -1
  31. package/dist/error.d.ts +0 -2
  32. package/dist/error.d.ts.map +1 -1
  33. package/dist/error.js +11 -14
  34. package/dist/error.js.map +1 -1
  35. package/dist/headlessRender.d.ts +2 -2
  36. package/dist/headlessRender.d.ts.map +1 -1
  37. package/dist/headlessRender.js +2 -3
  38. package/dist/headlessRender.js.map +1 -1
  39. package/dist/hmr.d.ts +1 -0
  40. package/dist/hmr.d.ts.map +1 -1
  41. package/dist/hmr.js +6 -2
  42. package/dist/hmr.js.map +1 -1
  43. package/dist/hooks/setup.d.ts +1 -1
  44. package/dist/hooks/setup.d.ts.map +1 -1
  45. package/dist/hooks/setup.js +110 -6
  46. package/dist/hooks/setup.js.map +1 -1
  47. package/dist/index.d.ts +1 -1
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +1 -1
  50. package/dist/index.js.map +1 -1
  51. package/dist/renderToString.js +1 -1
  52. package/dist/renderToString.js.map +1 -1
  53. package/dist/resource.d.ts +19 -0
  54. package/dist/resource.d.ts.map +1 -0
  55. package/dist/resource.js +167 -0
  56. package/dist/resource.js.map +1 -0
  57. package/dist/scheduler.d.ts.map +1 -1
  58. package/dist/scheduler.js +15 -11
  59. package/dist/scheduler.js.map +1 -1
  60. package/dist/signals/base.d.ts +3 -5
  61. package/dist/signals/base.d.ts.map +1 -1
  62. package/dist/signals/base.js +24 -47
  63. package/dist/signals/base.js.map +1 -1
  64. package/dist/signals/computed.d.ts.map +1 -1
  65. package/dist/signals/computed.js +14 -12
  66. package/dist/signals/computed.js.map +1 -1
  67. package/dist/signals/globals.d.ts +0 -2
  68. package/dist/signals/globals.d.ts.map +1 -1
  69. package/dist/signals/globals.js +0 -1
  70. package/dist/signals/globals.js.map +1 -1
  71. package/dist/signals/types.d.ts +1 -1
  72. package/dist/signals/types.d.ts.map +1 -1
  73. package/dist/ssr/server.d.ts +4 -4
  74. package/dist/ssr/server.d.ts.map +1 -1
  75. package/dist/ssr/server.js +11 -7
  76. package/dist/ssr/server.js.map +1 -1
  77. package/dist/types.d.ts +1 -1
  78. package/dist/types.d.ts.map +1 -1
  79. package/dist/utils/index.d.ts +2 -1
  80. package/dist/utils/index.d.ts.map +1 -1
  81. package/dist/utils/index.js +2 -1
  82. package/dist/utils/index.js.map +1 -1
  83. package/dist/utils/stream.d.ts +12 -0
  84. package/dist/utils/stream.d.ts.map +1 -0
  85. package/dist/utils/stream.js +8 -0
  86. package/dist/utils/stream.js.map +1 -0
  87. package/dist/utils/vdom.d.ts +2 -1
  88. package/dist/utils/vdom.d.ts.map +1 -1
  89. package/dist/utils/vdom.js +4 -1
  90. package/dist/utils/vdom.js.map +1 -1
  91. package/package.json +1 -13
  92. package/src/appHandle.ts +6 -7
  93. package/src/components/derive.ts +34 -40
  94. package/src/components/errorBoundary.ts +4 -2
  95. package/src/components/for.ts +34 -34
  96. package/src/components/lazy.ts +1 -1
  97. package/src/components/portal.ts +1 -1
  98. package/src/components/show.ts +32 -32
  99. package/src/components/transition.ts +16 -11
  100. package/src/dom/nodes.ts +5 -2
  101. package/src/dom/props.ts +28 -3
  102. package/src/error.ts +11 -19
  103. package/src/headlessRender.ts +5 -5
  104. package/src/hmr.ts +13 -3
  105. package/src/hooks/setup.ts +143 -9
  106. package/src/index.ts +1 -1
  107. package/src/renderToString.ts +1 -1
  108. package/src/resource.ts +207 -0
  109. package/src/scheduler.ts +16 -11
  110. package/src/signals/base.ts +29 -52
  111. package/src/signals/computed.ts +13 -9
  112. package/src/signals/globals.ts +0 -3
  113. package/src/signals/types.ts +1 -1
  114. package/src/ssr/server.ts +18 -11
  115. package/src/types.ts +4 -4
  116. package/src/utils/index.ts +2 -1
  117. package/src/utils/stream.ts +17 -0
  118. package/src/utils/vdom.ts +5 -0
  119. package/dist/statefulPromise.d.ts +0 -22
  120. package/dist/statefulPromise.d.ts.map +0 -1
  121. package/dist/statefulPromise.js +0 -94
  122. package/dist/statefulPromise.js.map +0 -1
  123. 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
- this.customNodeStack = captureErrorStack(optionsOrMessage.vNode)
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 && (error as KiruError)[$KIRU_ERROR] === true
36
+ return error instanceof Error && $KIRU_ERROR in error
37
37
  }
38
38
  }
39
39
 
40
- function captureErrorStack(vNode: Kiru.VNode) {
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
- ${componentFns.map((x) => ` at ${x}`).join("\n")}\n`
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.toString().match(/\/\/ \[kiru_devtools\]:(.*)/)?.[1] ?? null
72
+ return (fn as any)[$DEV_FILE_LINK] ?? null
81
73
  }
@@ -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.StatefulPromiseBase<unknown>[]) => void
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.StatefulPromiseBase<unknown>>()
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
- newEntry.value[$HMR_ACCEPT].inject(
124
- oldEntry.value[$HMR_ACCEPT].provide()
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
+ }
@@ -1,8 +1,12 @@
1
- import { computed, signal } from "../signals/index.js"
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 type { Signal } from "../signals/base.js"
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
- return {
49
- derive(selector) {
50
- const props = { ...vNode.props } as InferredProps
51
- const sig = computed(() => selector(props))
52
- propSyncs.push((p) => (sig.value = selector(p)))
53
- return sig
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
@@ -22,7 +22,7 @@ export {
22
22
  requestUpdate,
23
23
  useRequestUpdate,
24
24
  } from "./scheduler.js"
25
- export * from "./statefulPromise.js"
25
+ export * from "./resource.js"
26
26
  export * from "./viewTransitions.js"
27
27
 
28
28
  if (isBrowser) {
@@ -11,7 +11,7 @@ export function renderToString(element: JSX.Element) {
11
11
  result += chunk
12
12
  },
13
13
  }
14
- headlessRender(ctx, Fragment({ children: element }), null, 0)
14
+ headlessRender(ctx, Fragment({ children: element }))
15
15
  renderMode.current = prev
16
16
  return result
17
17
  }
@@ -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
- const { render, propSyncs } = vNode
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
- let newChild = latest(type)(props)
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 observered during setup
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 calls setState during render or in useLayoutEffect. Kiru limits the number of nested updates to prevent infinite loops."
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
  }