kiru 0.54.0-preview.1 → 0.54.1

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 (204) hide show
  1. package/dist/components/derive.d.ts +1 -1
  2. package/dist/components/derive.d.ts.map +1 -1
  3. package/dist/components/derive.js +2 -3
  4. package/dist/components/derive.js.map +1 -1
  5. package/dist/components/memo.d.ts +1 -3
  6. package/dist/components/memo.d.ts.map +1 -1
  7. package/dist/components/memo.js +2 -2
  8. package/dist/components/memo.js.map +1 -1
  9. package/dist/context.d.ts.map +1 -1
  10. package/dist/context.js +1 -23
  11. package/dist/context.js.map +1 -1
  12. package/dist/dom.d.ts.map +1 -1
  13. package/dist/dom.js +111 -78
  14. package/dist/dom.js.map +1 -1
  15. package/dist/error.d.ts.map +1 -1
  16. package/dist/error.js +2 -4
  17. package/dist/error.js.map +1 -1
  18. package/dist/form/index.d.ts.map +1 -1
  19. package/dist/form/index.js +6 -10
  20. package/dist/form/index.js.map +1 -1
  21. package/dist/globals.d.ts +1 -1
  22. package/dist/globals.d.ts.map +1 -1
  23. package/dist/globals.js.map +1 -1
  24. package/dist/hmr.d.ts +1 -0
  25. package/dist/hmr.d.ts.map +1 -1
  26. package/dist/hmr.js +11 -3
  27. package/dist/hmr.js.map +1 -1
  28. package/dist/hooks/useEffectEvent.d.ts.map +1 -1
  29. package/dist/hooks/useEffectEvent.js.map +1 -1
  30. package/dist/hooks/usePromise.d.ts +1 -2
  31. package/dist/hooks/usePromise.d.ts.map +1 -1
  32. package/dist/hooks/usePromise.js +62 -31
  33. package/dist/hooks/usePromise.js.map +1 -1
  34. package/dist/hooks/utils.d.ts.map +1 -1
  35. package/dist/hooks/utils.js +10 -10
  36. package/dist/hooks/utils.js.map +1 -1
  37. package/dist/hydration.d.ts +6 -13
  38. package/dist/hydration.d.ts.map +1 -1
  39. package/dist/hydration.js +20 -50
  40. package/dist/hydration.js.map +1 -1
  41. package/dist/index.js +2 -1
  42. package/dist/index.js.map +1 -1
  43. package/dist/reconciler.d.ts.map +1 -1
  44. package/dist/reconciler.js +3 -6
  45. package/dist/reconciler.js.map +1 -1
  46. package/dist/recursiveRender.d.ts.map +1 -1
  47. package/dist/recursiveRender.js +9 -8
  48. package/dist/recursiveRender.js.map +1 -1
  49. package/dist/renderToString.d.ts.map +1 -1
  50. package/dist/renderToString.js.map +1 -1
  51. package/dist/router/client/index.d.ts +2 -4
  52. package/dist/router/client/index.d.ts.map +1 -1
  53. package/dist/router/client/index.js +13 -59
  54. package/dist/router/client/index.js.map +1 -1
  55. package/dist/router/context.d.ts +5 -2
  56. package/dist/router/context.d.ts.map +1 -1
  57. package/dist/router/context.js +1 -5
  58. package/dist/router/context.js.map +1 -1
  59. package/dist/router/fileRouter.d.ts.map +1 -1
  60. package/dist/router/fileRouter.js +2 -4
  61. package/dist/router/fileRouter.js.map +1 -1
  62. package/dist/router/fileRouterController.d.ts +2 -2
  63. package/dist/router/fileRouterController.d.ts.map +1 -1
  64. package/dist/router/fileRouterController.js +135 -214
  65. package/dist/router/fileRouterController.js.map +1 -1
  66. package/dist/router/globals.d.ts +0 -3
  67. package/dist/router/globals.d.ts.map +1 -1
  68. package/dist/router/globals.js +0 -3
  69. package/dist/router/globals.js.map +1 -1
  70. package/dist/router/head.d.ts.map +1 -1
  71. package/dist/router/head.js +7 -5
  72. package/dist/router/head.js.map +1 -1
  73. package/dist/router/index.d.ts +1 -2
  74. package/dist/router/index.d.ts.map +1 -1
  75. package/dist/router/index.js +1 -2
  76. package/dist/router/index.js.map +1 -1
  77. package/dist/router/link.js +3 -3
  78. package/dist/router/link.js.map +1 -1
  79. package/dist/router/{ssg → server}/index.d.ts +4 -4
  80. package/dist/router/server/index.d.ts.map +1 -0
  81. package/dist/router/{ssg → server}/index.js +7 -9
  82. package/dist/router/server/index.js.map +1 -0
  83. package/dist/router/types.d.ts +16 -42
  84. package/dist/router/types.d.ts.map +1 -1
  85. package/dist/router/types.internal.d.ts +0 -4
  86. package/dist/router/types.internal.d.ts.map +1 -1
  87. package/dist/router/utils/index.d.ts +3 -8
  88. package/dist/router/utils/index.d.ts.map +1 -1
  89. package/dist/router/utils/index.js +8 -40
  90. package/dist/router/utils/index.js.map +1 -1
  91. package/dist/scheduler.d.ts +3 -14
  92. package/dist/scheduler.d.ts.map +1 -1
  93. package/dist/scheduler.js +64 -56
  94. package/dist/scheduler.js.map +1 -1
  95. package/dist/signals/base.d.ts +0 -2
  96. package/dist/signals/base.d.ts.map +1 -1
  97. package/dist/signals/base.js +0 -6
  98. package/dist/signals/base.js.map +1 -1
  99. package/dist/signals/computed.d.ts +3 -0
  100. package/dist/signals/computed.d.ts.map +1 -1
  101. package/dist/signals/computed.js +29 -20
  102. package/dist/signals/computed.js.map +1 -1
  103. package/dist/signals/for.d.ts +3 -3
  104. package/dist/signals/for.d.ts.map +1 -1
  105. package/dist/signals/for.js +2 -1
  106. package/dist/signals/for.js.map +1 -1
  107. package/dist/signals/utils.d.ts.map +1 -1
  108. package/dist/signals/utils.js +2 -1
  109. package/dist/signals/utils.js.map +1 -1
  110. package/dist/signals/watch.d.ts.map +1 -1
  111. package/dist/signals/watch.js +18 -22
  112. package/dist/signals/watch.js.map +1 -1
  113. package/dist/ssr/client.d.ts +1 -1
  114. package/dist/ssr/client.d.ts.map +1 -1
  115. package/dist/ssr/client.js +0 -2
  116. package/dist/ssr/client.js.map +1 -1
  117. package/dist/ssr/server.d.ts +3 -9
  118. package/dist/ssr/server.d.ts.map +1 -1
  119. package/dist/ssr/server.js +30 -37
  120. package/dist/ssr/server.js.map +1 -1
  121. package/dist/types.d.ts +0 -7
  122. package/dist/types.d.ts.map +1 -1
  123. package/dist/types.dom.d.ts +3 -3
  124. package/dist/types.dom.d.ts.map +1 -1
  125. package/dist/utils/format.d.ts +1 -2
  126. package/dist/utils/format.d.ts.map +1 -1
  127. package/dist/utils/format.js +1 -4
  128. package/dist/utils/format.js.map +1 -1
  129. package/dist/utils/index.d.ts +1 -1
  130. package/dist/utils/index.d.ts.map +1 -1
  131. package/dist/utils/index.js +1 -1
  132. package/dist/utils/index.js.map +1 -1
  133. package/dist/utils/promise.d.ts +0 -2
  134. package/dist/utils/promise.d.ts.map +1 -1
  135. package/dist/utils/promise.js +1 -45
  136. package/dist/utils/promise.js.map +1 -1
  137. package/dist/utils/runtime.d.ts +3 -2
  138. package/dist/utils/runtime.d.ts.map +1 -1
  139. package/dist/utils/runtime.js +5 -2
  140. package/dist/utils/runtime.js.map +1 -1
  141. package/dist/utils/vdom.d.ts.map +1 -1
  142. package/dist/utils/vdom.js +2 -2
  143. package/dist/utils/vdom.js.map +1 -1
  144. package/package.json +4 -8
  145. package/src/components/derive.ts +3 -5
  146. package/src/components/memo.ts +3 -11
  147. package/src/context.ts +1 -24
  148. package/src/dom.ts +146 -101
  149. package/src/error.ts +2 -4
  150. package/src/form/index.ts +6 -9
  151. package/src/globals.ts +1 -1
  152. package/src/hmr.ts +14 -5
  153. package/src/hooks/useEffectEvent.ts +0 -1
  154. package/src/hooks/usePromise.ts +77 -58
  155. package/src/hooks/utils.ts +12 -12
  156. package/src/hydration.ts +21 -57
  157. package/src/index.ts +1 -1
  158. package/src/reconciler.ts +2 -6
  159. package/src/recursiveRender.ts +10 -9
  160. package/src/renderToString.ts +0 -1
  161. package/src/router/client/index.ts +16 -114
  162. package/src/router/context.ts +6 -7
  163. package/src/router/fileRouter.ts +2 -6
  164. package/src/router/fileRouterController.ts +161 -324
  165. package/src/router/globals.ts +0 -4
  166. package/src/router/head.ts +7 -5
  167. package/src/router/index.ts +1 -12
  168. package/src/router/link.ts +3 -3
  169. package/src/router/{ssg → server}/index.ts +13 -18
  170. package/src/router/types.internal.ts +0 -5
  171. package/src/router/types.ts +16 -53
  172. package/src/router/utils/index.ts +16 -79
  173. package/src/scheduler.ts +85 -89
  174. package/src/signals/base.ts +0 -8
  175. package/src/signals/computed.ts +30 -18
  176. package/src/signals/for.ts +15 -10
  177. package/src/signals/utils.ts +2 -1
  178. package/src/signals/watch.ts +27 -22
  179. package/src/ssr/client.ts +1 -4
  180. package/src/ssr/server.ts +34 -59
  181. package/src/types.dom.ts +4 -5
  182. package/src/types.ts +0 -10
  183. package/src/utils/format.ts +0 -5
  184. package/src/utils/index.ts +1 -1
  185. package/src/utils/promise.ts +1 -70
  186. package/src/utils/runtime.ts +6 -2
  187. package/src/utils/vdom.ts +2 -7
  188. package/dist/router/constants.d.ts +0 -2
  189. package/dist/router/constants.d.ts.map +0 -1
  190. package/dist/router/constants.js +0 -2
  191. package/dist/router/constants.js.map +0 -1
  192. package/dist/router/guard.d.ts +0 -17
  193. package/dist/router/guard.d.ts.map +0 -1
  194. package/dist/router/guard.js +0 -45
  195. package/dist/router/guard.js.map +0 -1
  196. package/dist/router/ssg/index.d.ts.map +0 -1
  197. package/dist/router/ssg/index.js.map +0 -1
  198. package/dist/router/ssr/index.d.ts +0 -20
  199. package/dist/router/ssr/index.d.ts.map +0 -1
  200. package/dist/router/ssr/index.js +0 -163
  201. package/dist/router/ssr/index.js.map +0 -1
  202. package/src/router/constants.ts +0 -1
  203. package/src/router/guard.ts +0 -72
  204. package/src/router/ssr/index.ts +0 -252
package/src/dom.ts CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  propToHtmlAttr,
6
6
  getVNodeAppContext,
7
7
  setRef,
8
+ isValidTextChild,
8
9
  } from "./utils/index.js"
9
10
  import {
10
11
  booleanAttributes,
@@ -87,43 +88,35 @@ function createDom(vNode: DomVNode): SomeDom {
87
88
  }
88
89
  function createTextNode(vNode: VNode): Text {
89
90
  const { nodeValue } = vNode.props
90
- if (!Signal.isSignal(nodeValue)) {
91
- return document.createTextNode(nodeValue)
91
+ if (Signal.isSignal(nodeValue)) {
92
+ return createSignalTextNode(vNode, nodeValue)
92
93
  }
93
94
 
95
+ return document.createTextNode(nodeValue)
96
+ }
97
+
98
+ function createSignalTextNode(vNode: VNode, nodeValue: Signal<string>): Text {
94
99
  const value = nodeValue.peek() ?? ""
95
100
  const textNode = document.createTextNode(value)
96
101
  subTextNode(vNode, textNode, nodeValue)
97
102
  return textNode
98
103
  }
99
104
 
100
- function wrapFocusEventHandler(
101
- vNode: VNode,
102
- evtName: "focus" | "blur",
103
- callback: (event: FocusEvent) => void
104
- ) {
105
- const wrappedHandlers = vNodeToWrappedFocusEventHandlersMap.get(vNode) ?? {}
106
- const handler = (wrappedHandlers[evtName] = (event: FocusEvent) => {
105
+ function wrapFocusEventHandler(callback: (event: FocusEvent) => void) {
106
+ return (event: FocusEvent) => {
107
107
  if (persistingFocus) {
108
108
  event.preventDefault()
109
109
  event.stopPropagation()
110
110
  return
111
111
  }
112
112
  callback(event)
113
- })
114
- vNodeToWrappedFocusEventHandlersMap.set(vNode, wrappedHandlers)
115
- return handler
113
+ }
116
114
  }
117
115
 
118
- type WrappedFocusEventMap = {
119
- focus?: (event: FocusEvent) => void
120
- blur?: (event: FocusEvent) => void
116
+ interface VNodeEventListenerObjects {
117
+ [key: string]: EventListenerObject
121
118
  }
122
-
123
- const vNodeToWrappedFocusEventHandlersMap = new WeakMap<
124
- VNode,
125
- WrappedFocusEventMap
126
- >()
119
+ const eventListenerObjects = new WeakMap<VNode, VNodeEventListenerObjects>()
127
120
 
128
121
  function updateDom(vNode: DomVNode) {
129
122
  const { dom, prev, props, cleanups } = vNode
@@ -146,30 +139,39 @@ function updateDom(vNode: DomVNode) {
146
139
  if (!(k in prevProps)) keys.push(k)
147
140
  }
148
141
 
142
+ let events: VNodeEventListenerObjects | undefined
149
143
  for (let i = 0; i < keys.length; i++) {
150
144
  const key = keys[i]
151
145
  const prevVal = prevProps[key]
152
146
  const nextVal = nextProps[key]
153
147
 
154
148
  if (propFilters.isEvent(key)) {
149
+ events ??= eventListenerObjects.get(vNode)
150
+ if (!events) eventListenerObjects.set(vNode, (events = {}))
151
+
155
152
  if (prevVal !== nextVal || isHydration) {
156
153
  const evtName = key.replace(EVENT_PREFIX_REGEX, "")
157
- const isFocus = evtName === "focus" || evtName === "blur"
158
- const wrappedMap = vNodeToWrappedFocusEventHandlersMap.get(vNode)
159
-
160
- if (key in prevProps) {
161
- dom.removeEventListener(
162
- evtName,
163
- isFocus ? wrappedMap?.[evtName] : prevVal
164
- )
154
+ const evtListenerObj = events[evtName]
155
+
156
+ if (!nextVal) {
157
+ if (evtListenerObj) {
158
+ dom.removeEventListener(evtName, evtListenerObj)
159
+ delete events[evtName]
160
+ }
161
+ continue
162
+ }
163
+
164
+ let handleEvent = nextVal.bind(void 0)
165
+ if (evtName === "focus" || evtName === "blur") {
166
+ handleEvent = wrapFocusEventHandler(handleEvent)
165
167
  }
166
168
 
167
- if (key in nextProps) {
168
- dom.addEventListener(
169
- evtName,
170
- isFocus ? wrapFocusEventHandler(vNode, evtName, nextVal) : nextVal
171
- )
169
+ if (evtListenerObj) {
170
+ evtListenerObj.handleEvent = handleEvent
171
+ continue
172
172
  }
173
+
174
+ dom.addEventListener(evtName, (events[evtName] = { handleEvent }))
173
175
  }
174
176
  continue
175
177
  }
@@ -206,7 +208,7 @@ function updateDom(vNode: DomVNode) {
206
208
  }
207
209
  }
208
210
 
209
- function deriveSelectElementValue(dom: HTMLSelectElement) {
211
+ function getSelectElementValue(dom: HTMLSelectElement) {
210
212
  if (dom.multiple) {
211
213
  return Array.from(dom.selectedOptions).map((option) => option.value)
212
214
  }
@@ -231,7 +233,7 @@ const bindAttrToEventMap: Record<string, string> = {
231
233
  playbackRate: "ratechange",
232
234
  currentTime: "timeupdate",
233
235
  }
234
- const numericValueElements = ["progress", "meter", "number", "range"]
236
+ const numericValueInputTypes = new Set(["progress", "meter", "number", "range"])
235
237
 
236
238
  function setSignalProp(
237
239
  vNode: VNode,
@@ -240,71 +242,80 @@ function setSignalProp(
240
242
  signal: Signal<any>,
241
243
  prevValue: unknown
242
244
  ) {
243
- const cleanups = (vNode.cleanups ??= {})
244
245
  const [modifier, attr] = key.split(":")
246
+ const cleanups = (vNode.cleanups ??= {})
245
247
  if (modifier !== "bind") {
246
248
  cleanups[key] = signal.subscribe((value, prev) => {
247
249
  if (value === prev) return
248
250
  setProp(dom, key, value, prev)
249
251
  if (__DEV__) {
250
- window.__kiru.profilingContext?.emit(
251
- "signalAttrUpdate",
252
- getVNodeAppContext(vNode)!
253
- )
252
+ emitSignalAttrUpdate(vNode)
254
253
  }
255
254
  })
255
+ } else {
256
+ const evtName = bindAttrToEventMap[attr]
257
+ if (!evtName) {
258
+ if (__DEV__) {
259
+ console.error(
260
+ `[kiru]: ${attr} is not a valid element binding attribute.`
261
+ )
262
+ }
263
+ return
264
+ }
265
+ cleanups[key] = bindElementProp(vNode, dom, attr, evtName, signal)
266
+ }
256
267
 
257
- const value = signal.peek()
258
- const prev = unwrap(prevValue)
259
- if (value === prev) return
260
- setProp(dom, key, value, prev)
261
- return
268
+ const value = signal.peek()
269
+ const prev = unwrap(prevValue)
270
+ if (value !== prev) {
271
+ setProp(dom, attr ?? modifier, value, prev)
262
272
  }
273
+ }
263
274
 
264
- const evtName = bindAttrToEventMap[attr]
265
- if (!evtName) {
266
- if (__DEV__) {
267
- console.error(`[kiru]: ${attr} is not a valid element binding attribute.`)
268
- }
269
- return
275
+ function createElementValueReader(
276
+ dom: Exclude<SomeDom, Text>,
277
+ signal: Signal<any>
278
+ ) {
279
+ if (dom instanceof HTMLInputElement) {
280
+ return createInputValueReader(dom, signal)
270
281
  }
282
+ if (dom instanceof HTMLSelectElement) {
283
+ return () => getSelectElementValue(dom)
284
+ }
285
+ return () => (dom as any).value
286
+ }
271
287
 
272
- const isSelect = dom instanceof HTMLSelectElement
273
- const setAttr = isSelect
274
- ? (value: any) => setSelectElementValue(dom, value)
275
- : (value: any) => ((dom as any)[attr] = value)
288
+ function bindElementProp(
289
+ vNode: VNode,
290
+ dom: Exclude<SomeDom, Text>,
291
+ attr: string,
292
+ evtName: string,
293
+ signal: Signal<any>
294
+ ): () => void {
295
+ const writeToSignal = (val: any) => {
296
+ signal.sneak(val)
297
+ signal.notify((sub) => sub !== updateFromSignal)
298
+ }
299
+
300
+ const writeToElement =
301
+ dom instanceof HTMLSelectElement && attr === "value"
302
+ ? (value: any) => setSelectElementValue(dom, value)
303
+ : (value: any) => ((dom as any)[attr] = value)
276
304
 
277
- const signalUpdateCallback = (value: any) => {
278
- setAttr(value)
305
+ const updateFromSignal = (value: any) => {
306
+ writeToElement(value)
279
307
  if (__DEV__) {
280
- window.__kiru.profilingContext?.emit(
281
- "signalAttrUpdate",
282
- getVNodeAppContext(vNode)!
283
- )
308
+ emitSignalAttrUpdate(vNode)
284
309
  }
285
310
  }
286
311
 
287
- const setSigFromElement = (val: any) => {
288
- signal.sneak(val)
289
- signal.notify((sub) => sub !== signalUpdateCallback)
290
- }
291
-
292
- let evtHandler: (evt: Event) => void
312
+ let evtHandler: EventListener
293
313
  if (attr === "value") {
294
- const useNumericValue =
295
- numericValueElements.indexOf((dom as HTMLInputElement).type) !== -1
296
- evtHandler = () => {
297
- let val: any = (dom as HTMLInputElement | HTMLSelectElement).value
298
- if (isSelect) {
299
- val = deriveSelectElementValue(dom)
300
- } else if (typeof signal.peek() === "number" && useNumericValue) {
301
- val = (dom as HTMLInputElement).valueAsNumber
302
- }
303
- setSigFromElement(val)
304
- }
314
+ const readValue = createElementValueReader(dom, signal)
315
+ evtHandler = () => writeToSignal(readValue())
305
316
  } else {
306
- evtHandler = (e: Event) => {
307
- const val = (e.target as any)[attr]
317
+ evtHandler = () => {
318
+ const val = (dom as any)[attr]
308
319
  /**
309
320
  * the 'timeupdate' event is fired when the currentTime property is
310
321
  * set (from code OR playback), so we need to prevent unnecessary
@@ -312,22 +323,42 @@ function setSignalProp(
312
323
  * elements with the same signal bound to 'currentTime'
313
324
  */
314
325
  if (attr === "currentTime" && signal.peek() === val) return
315
- setSigFromElement(val)
326
+ writeToSignal(val)
316
327
  }
317
328
  }
318
329
 
319
330
  dom.addEventListener(evtName, evtHandler)
320
- const unsub = signal.subscribe(signalUpdateCallback)
331
+ const unsub = signal.subscribe(updateFromSignal)
321
332
 
322
- cleanups[key] = () => {
333
+ return () => {
323
334
  dom.removeEventListener(evtName, evtHandler)
324
335
  unsub()
325
336
  }
337
+ }
326
338
 
327
- const value = signal.peek()
328
- const prev = unwrap(prevValue)
329
- if (value === prev) return
330
- setProp(dom, attr, value, prev)
339
+ function createInputValueReader(
340
+ dom: HTMLInputElement,
341
+ signal: Signal<any>
342
+ ): () => any {
343
+ const t = dom.type
344
+ const v = signal.peek()
345
+
346
+ if (t === "date" && v instanceof Date) {
347
+ return () => dom.valueAsDate
348
+ }
349
+
350
+ if (numericValueInputTypes.has(t) && typeof v === "number") {
351
+ return () => dom.valueAsNumber
352
+ }
353
+
354
+ return () => dom.value
355
+ }
356
+
357
+ function emitSignalAttrUpdate(vNode: VNode) {
358
+ window.__kiru.profilingContext?.emit(
359
+ "signalAttrUpdate",
360
+ getVNodeAppContext(vNode)!
361
+ )
331
362
  }
332
363
 
333
364
  function subTextNode(vNode: VNode, textNode: Text, signal: Signal<string>) {
@@ -343,21 +374,39 @@ function subTextNode(vNode: VNode, textNode: Text, signal: Signal<string>) {
343
374
  })
344
375
  }
345
376
 
346
- function tryHydrateNullableSignalChild(vNode: VNode): MaybeDom {
347
- if (vNode.type !== "#text" || !Signal.isSignal(vNode.props.nodeValue)) {
348
- return
377
+ /**
378
+ * Creates and inserts an empty signal-bound text node into
379
+ * the dom tree if the signal value is null or undefined.
380
+ */
381
+ function getOrCreateTextNode(vNode: VNode): MaybeDom {
382
+ const sig = vNode.props.nodeValue
383
+ if (!Signal.isSignal(sig)) {
384
+ return hydrationStack.getCurrentChild()
349
385
  }
350
- const value = unwrap(vNode.props.nodeValue)
351
- if (value !== null && value !== undefined) {
352
- return
386
+
387
+ const value = sig.peek()
388
+ if (isValidTextChild(value)) {
389
+ return hydrationStack.getCurrentChild()
390
+ }
391
+
392
+ const dom = createSignalTextNode(vNode, sig)
393
+ const currentChild = hydrationStack.getCurrentChild()
394
+
395
+ if (!currentChild) {
396
+ return hydrationStack.getCurrentParent().appendChild(dom)
353
397
  }
354
- const dom = createTextNode(vNode)
355
- hydrationStack.parent().appendChild(dom)
398
+
399
+ currentChild.before(dom)
356
400
  return dom
357
401
  }
358
402
 
359
403
  function hydrateDom(vNode: VNode) {
360
- const dom = hydrationStack.nextChild() ?? tryHydrateNullableSignalChild(vNode)
404
+ const dom =
405
+ vNode.type === "#text"
406
+ ? getOrCreateTextNode(vNode)
407
+ : hydrationStack.getCurrentChild()
408
+
409
+ hydrationStack.bumpChildIndex()
361
410
 
362
411
  if (!dom) {
363
412
  throw new KiruError({
@@ -699,11 +748,7 @@ function commitDeletion(vNode: VNode) {
699
748
 
700
749
  subs?.forEach((unsub) => unsub())
701
750
  if (cleanups) Object.values(cleanups).forEach((c) => c())
702
- while (hooks?.length) {
703
- try {
704
- hooks.pop()!.cleanup?.()
705
- } catch {}
706
- }
751
+ while (hooks?.length) hooks.pop()!.cleanup?.()
707
752
 
708
753
  if (__DEV__) {
709
754
  window.__kiru.profilingContext?.emit("removeNode", ctx)
package/src/error.ts CHANGED
@@ -25,10 +25,8 @@ export class KiruError extends Error {
25
25
  : optionsOrMessage.message
26
26
  super(message)
27
27
  if (typeof optionsOrMessage !== "string") {
28
- if (__DEV__) {
29
- if (optionsOrMessage?.vNode) {
30
- this.customNodeStack = captureErrorStack(optionsOrMessage.vNode)
31
- }
28
+ if (__DEV__ && optionsOrMessage?.vNode) {
29
+ this.customNodeStack = captureErrorStack(optionsOrMessage.vNode)
32
30
  }
33
31
  this.fatal = optionsOrMessage?.fatal
34
32
  }
package/src/form/index.ts CHANGED
@@ -4,6 +4,7 @@ import {
4
4
  safeStringify,
5
5
  shallowCompare,
6
6
  generateRandomID,
7
+ call,
7
8
  } from "../utils/index.js"
8
9
  import { useEffect } from "../hooks/useEffect.js"
9
10
  import { useMemo } from "../hooks/useMemo.js"
@@ -260,7 +261,7 @@ function createFormController<T extends Record<string, unknown>>(
260
261
  }
261
262
  }
262
263
 
263
- formFieldUpdaters.get(name)?.forEach((update) => update())
264
+ formFieldUpdaters.get(name)?.forEach(call)
264
265
  }
265
266
 
266
267
  const setFieldValue = <K extends RecordKey<T>>(
@@ -410,9 +411,7 @@ function createFormController<T extends Record<string, unknown>>(
410
411
  delete formFieldErrors[fieldName as RecordKey<T>]
411
412
  }
412
413
  updateSubscribers()
413
- formFieldUpdaters.forEach((updaters) => {
414
- updaters.forEach((update) => update())
415
- })
414
+ formFieldUpdaters.forEach((updaters) => updaters.forEach(call))
416
415
  }
417
416
 
418
417
  const validateForm = async () => {
@@ -634,11 +633,9 @@ export function useForm<T extends Record<string, unknown> = {}>(
634
633
  "useFormSubscription",
635
634
  { sub: null! as FormStateSubscriber<T> },
636
635
  ({ hook, isInit, isHMR, update }) => {
637
- if (__DEV__) {
638
- if (isHMR) {
639
- isInit = true
640
- hook.cleanup?.()
641
- }
636
+ if (__DEV__ && isHMR) {
637
+ isInit = true
638
+ hook.cleanup?.()
642
639
  }
643
640
  if (isInit) {
644
641
  hook.sub = {
package/src/globals.ts CHANGED
@@ -13,5 +13,5 @@ const renderMode = {
13
13
  }
14
14
 
15
15
  const hydrationMode = {
16
- current: "dynamic" as Kiru.HydrationMode,
16
+ current: "dynamic" as "static" | "dynamic",
17
17
  }
package/src/hmr.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import { $HMR_ACCEPT, $DEV_FILE_LINK } from "./constants.js"
2
- import { __DEV__ } from "./env.js"
3
2
  import { traverseApply } from "./utils/index.js"
4
- import { requestUpdate } from "./scheduler.js"
3
+ import { flushSync, requestUpdate } from "./scheduler.js"
5
4
  import { Signal } from "./signals/base.js"
6
5
  import type { WatchEffect } from "./signals/watch.js"
7
6
  import type { Store } from "./store.js"
@@ -25,6 +24,11 @@ type HotVarDesc = {
25
24
  link: string
26
25
  }
27
26
 
27
+ let _isHmrUpdate = false
28
+ export function isHmrUpdate() {
29
+ return _isHmrUpdate
30
+ }
31
+
28
32
  export function isGenericHmrAcceptor(
29
33
  thing: unknown
30
34
  ): thing is GenericHMRAcceptor<any> {
@@ -128,15 +132,20 @@ export function createHMRContext() {
128
132
  if (vNode.type === oldEntry.value) {
129
133
  vNode.type = newEntry.value as any
130
134
  dirtiedApps.add(ctx)
131
- vNode.hmrUpdated = true
132
135
  }
133
136
  })
134
137
  })
135
138
  }
136
139
  }
137
- dirtiedApps.forEach((ctx) => ctx.rootNode && requestUpdate(ctx.rootNode))
138
- isModuleReplacementExecution = false
139
140
 
141
+ if (dirtiedApps.size) {
142
+ _isHmrUpdate = true
143
+ dirtiedApps.forEach((ctx) => requestUpdate(ctx.rootNode))
144
+ flushSync()
145
+ _isHmrUpdate = false
146
+ }
147
+
148
+ isModuleReplacementExecution = false
140
149
  currentModuleMemory = null
141
150
  currentModuleFilePath = null
142
151
  }
@@ -1,6 +1,5 @@
1
1
  import { sideEffectsEnabled } from "../utils/index.js"
2
2
  import { node } from "../globals.js"
3
- import { __DEV__ } from "../env.js"
4
3
  import { useHook } from "./utils.js"
5
4
 
6
5
  /**
@@ -1,21 +1,15 @@
1
- import { __DEV__ } from "../env.js"
1
+ import { STREAMED_DATA_EVENT } from "../constants.js"
2
2
  import { hydrationMode, renderMode } from "../globals.js"
3
3
  import { Signal, useSignal } from "../signals/base.js"
4
- import { depsRequireChange, useHook } from "./utils.js"
4
+ import { cleanupHook, depsRequireChange, useHook } from "./utils.js"
5
5
  import { useId } from "./useId.js"
6
- import {
7
- createStatefulPromise,
8
- resolveStreamedPromise,
9
- } from "../utils/promise.js"
10
6
 
11
7
  export { usePromise }
12
8
 
13
9
  const nodeToPromiseIndex = new WeakMap<Kiru.VNode, number>()
14
10
 
15
- type PromiseMutator = (signal: AbortSignal) => unknown | Promise<unknown>
16
-
17
11
  type UsePromiseResult<T> = Kiru.StatefulPromise<T> & {
18
- refresh: (mutator?: PromiseMutator) => void
12
+ refresh: () => void
19
13
  isPending: Signal<boolean>
20
14
  }
21
15
 
@@ -23,7 +17,7 @@ function usePromise<T>(
23
17
  callback: (signal: AbortSignal) => Promise<T>,
24
18
  deps: unknown[]
25
19
  ): UsePromiseResult<T> {
26
- const vNodeId = useId()
20
+ const id = useId()
27
21
  const isPending = useSignal(true)
28
22
 
29
23
  return useHook(
@@ -32,54 +26,24 @@ function usePromise<T>(
32
26
  promise: UsePromiseResult<T>
33
27
  abortController?: AbortController
34
28
  deps?: unknown[]
35
- refresh: (mutator?: PromiseMutator) => void
36
- promiseId: string
37
- epoch: number
38
29
  },
39
30
  ({ hook, isInit, vNode, update }) => {
40
- if (isInit) {
41
- hook.epoch = 0
42
- hook.cleanup = () => hook.abortController?.abort("aborted")
43
-
44
- const index = nodeToPromiseIndex.get(vNode) ?? 0
45
- nodeToPromiseIndex.set(vNode, index + 1)
46
- const promiseId = (hook.promiseId = `${vNodeId}:data:${index}`)
47
-
48
- const refresh = (hook.refresh = (mutator?: PromiseMutator) => {
49
- if (typeof mutator !== "function") {
50
- delete hook.deps
51
- return update()
52
- }
53
-
54
- hook.cleanup!()
55
- const signal = (hook.abortController = new AbortController()).signal
56
- const promise = Promise.try(mutator, signal).then(() =>
57
- callback(signal)
58
- )
59
- const epoch = ++hook.epoch
60
- hook.promise = createStatefulPromise(
61
- promiseId,
62
- promise,
63
- { isPending, refresh },
64
- () => epoch === hook.epoch && (isPending.value = false)
65
- )
66
-
67
- isPending.value = true
68
- update()
69
- })
70
- }
71
-
72
31
  if (isInit || depsRequireChange(deps, hook.deps)) {
73
32
  isPending.value = true
74
33
  hook.deps = deps
75
- hook.cleanup!()
34
+ cleanupHook(hook)
76
35
 
77
- const signal = (hook.abortController = new AbortController()).signal
78
- const { promiseId, refresh } = hook
36
+ const controller = (hook.abortController = new AbortController())
37
+ hook.cleanup = () => controller.abort()
38
+
39
+ const index = nodeToPromiseIndex.get(vNode) ?? 0
40
+ nodeToPromiseIndex.set(vNode, index + 1)
41
+
42
+ const promiseId = `${id}:data:${index}`
79
43
 
80
44
  let promise: Promise<T>
81
45
  if (renderMode.current === "string") {
82
- // if we're rendering to string, there's no need to fire the callback
46
+ // if we're rendering to a string, there's no need to fire the callback
83
47
  promise = Promise.resolve() as Promise<T>
84
48
  } else if (
85
49
  renderMode.current === "hydrate" &&
@@ -87,21 +51,76 @@ function usePromise<T>(
87
51
  ) {
88
52
  // if we're hydrating and the hydration mode is not static,
89
53
  // we need to resolve the promise from cache/event
90
- promise = resolveStreamedPromise<T>(promiseId, signal)
54
+ promise = resolveDeferredPromise<T>(promiseId, controller.signal)
91
55
  } else {
92
56
  // dom / stream / (hydrate + static)
93
- promise = callback(signal)
57
+ promise = callback(controller.signal)
94
58
  }
95
59
 
96
- const epoch = ++hook.epoch
97
- hook.promise = createStatefulPromise(
98
- promiseId,
99
- promise,
100
- { isPending, refresh },
101
- () => epoch === hook.epoch && (isPending.value = false)
102
- )
60
+ const state: Kiru.PromiseState<T> = { id: promiseId, state: "pending" }
61
+ const statefulPromise: Kiru.StatefulPromise<T> = (hook.promise =
62
+ Object.assign(promise, state, {
63
+ isPending,
64
+ refresh: () => {
65
+ hook.deps = undefined
66
+ update()
67
+ },
68
+ }))
69
+
70
+ statefulPromise
71
+ .then((value) => {
72
+ statefulPromise.state = "fulfilled"
73
+ statefulPromise.value = value
74
+ isPending.value = false
75
+ })
76
+ .catch((error) => {
77
+ statefulPromise.state = "rejected"
78
+ statefulPromise.error =
79
+ error instanceof Error ? error : new Error(error)
80
+ })
103
81
  }
104
82
  return hook.promise
105
83
  }
106
84
  )
107
85
  }
86
+
87
+ interface DeferredPromiseEventDetail<T> {
88
+ id: string
89
+ data?: T
90
+ error?: string
91
+ }
92
+
93
+ function resolveDeferredPromise<T>(
94
+ id: string,
95
+ signal: AbortSignal
96
+ ): Promise<T> {
97
+ return new Promise<T>((resolve, reject) => {
98
+ const deferralCache: Map<string, { data?: T; error?: string }> = // @ts-ignore
99
+ (window[STREAMED_DATA_EVENT] ??= new Map())
100
+
101
+ const existing = deferralCache.get(id)
102
+ if (existing) {
103
+ const { data, error } = existing
104
+ deferralCache.delete(id)
105
+ if (error) return reject(error)
106
+ return resolve(data!)
107
+ }
108
+
109
+ const onDataEvent = (event: Event) => {
110
+ const { detail } = event as CustomEvent<DeferredPromiseEventDetail<T>>
111
+ if (detail.id === id) {
112
+ deferralCache.delete(id)
113
+ window.removeEventListener(STREAMED_DATA_EVENT, onDataEvent)
114
+ const { data, error } = detail
115
+ if (error) return reject(error)
116
+ resolve(data!)
117
+ }
118
+ }
119
+
120
+ window.addEventListener(STREAMED_DATA_EVENT, onDataEvent)
121
+ signal.addEventListener("abort", () => {
122
+ window.removeEventListener(STREAMED_DATA_EVENT, onDataEvent)
123
+ reject()
124
+ })
125
+ })
126
+ }