kiru 1.2.1 → 1.3.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 (86) hide show
  1. package/dist/components/derive.d.ts +7 -6
  2. package/dist/components/derive.d.ts.map +1 -1
  3. package/dist/components/derive.js +9 -10
  4. package/dist/components/derive.js.map +1 -1
  5. package/dist/components/errorBoundary.d.ts +1 -1
  6. package/dist/components/errorBoundary.d.ts.map +1 -1
  7. package/dist/components/errorBoundary.js.map +1 -1
  8. package/dist/components/transition.d.ts +1 -2
  9. package/dist/components/transition.d.ts.map +1 -1
  10. package/dist/components/transition.js +26 -15
  11. package/dist/components/transition.js.map +1 -1
  12. package/dist/error.d.ts +0 -2
  13. package/dist/error.d.ts.map +1 -1
  14. package/dist/error.js +11 -14
  15. package/dist/error.js.map +1 -1
  16. package/dist/headlessRender.d.ts +2 -2
  17. package/dist/headlessRender.d.ts.map +1 -1
  18. package/dist/headlessRender.js +2 -3
  19. package/dist/headlessRender.js.map +1 -1
  20. package/dist/hmr.d.ts +1 -0
  21. package/dist/hmr.d.ts.map +1 -1
  22. package/dist/hmr.js +6 -2
  23. package/dist/hmr.js.map +1 -1
  24. package/dist/index.d.ts +1 -1
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +1 -1
  27. package/dist/index.js.map +1 -1
  28. package/dist/renderToString.js +1 -1
  29. package/dist/renderToString.js.map +1 -1
  30. package/dist/resource.d.ts +19 -0
  31. package/dist/resource.d.ts.map +1 -0
  32. package/dist/resource.js +167 -0
  33. package/dist/resource.js.map +1 -0
  34. package/dist/scheduler.js +13 -8
  35. package/dist/scheduler.js.map +1 -1
  36. package/dist/signals/base.d.ts +3 -5
  37. package/dist/signals/base.d.ts.map +1 -1
  38. package/dist/signals/base.js +12 -54
  39. package/dist/signals/base.js.map +1 -1
  40. package/dist/signals/computed.d.ts.map +1 -1
  41. package/dist/signals/computed.js +5 -11
  42. package/dist/signals/computed.js.map +1 -1
  43. package/dist/signals/globals.d.ts +0 -2
  44. package/dist/signals/globals.d.ts.map +1 -1
  45. package/dist/signals/globals.js +0 -1
  46. package/dist/signals/globals.js.map +1 -1
  47. package/dist/signals/types.d.ts +1 -4
  48. package/dist/signals/types.d.ts.map +1 -1
  49. package/dist/ssr/server.d.ts +4 -4
  50. package/dist/ssr/server.d.ts.map +1 -1
  51. package/dist/ssr/server.js +11 -7
  52. package/dist/ssr/server.js.map +1 -1
  53. package/dist/types.d.ts +3 -3
  54. package/dist/types.d.ts.map +1 -1
  55. package/dist/utils/index.d.ts +2 -1
  56. package/dist/utils/index.d.ts.map +1 -1
  57. package/dist/utils/index.js +2 -1
  58. package/dist/utils/index.js.map +1 -1
  59. package/dist/utils/stream.d.ts +12 -0
  60. package/dist/utils/stream.d.ts.map +1 -0
  61. package/dist/utils/stream.js +8 -0
  62. package/dist/utils/stream.js.map +1 -0
  63. package/package.json +1 -13
  64. package/src/components/derive.ts +29 -36
  65. package/src/components/errorBoundary.ts +3 -1
  66. package/src/components/transition.ts +29 -16
  67. package/src/error.ts +11 -19
  68. package/src/headlessRender.ts +5 -5
  69. package/src/hmr.ts +13 -3
  70. package/src/index.ts +1 -1
  71. package/src/renderToString.ts +1 -1
  72. package/src/resource.ts +207 -0
  73. package/src/scheduler.ts +14 -8
  74. package/src/signals/base.ts +18 -57
  75. package/src/signals/computed.ts +4 -8
  76. package/src/signals/globals.ts +0 -3
  77. package/src/signals/types.ts +1 -4
  78. package/src/ssr/server.ts +18 -11
  79. package/src/types.ts +6 -6
  80. package/src/utils/index.ts +2 -1
  81. package/src/utils/stream.ts +17 -0
  82. package/dist/statefulPromise.d.ts +0 -22
  83. package/dist/statefulPromise.d.ts.map +0 -1
  84. package/dist/statefulPromise.js +0 -94
  85. package/dist/statefulPromise.js.map +0 -1
  86. package/src/statefulPromise.ts +0 -136
@@ -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
@@ -280,11 +280,6 @@ function updateVNode(vNode: VNode): VNode | null {
280
280
  }
281
281
 
282
282
  if (KiruError.isKiruError(error)) {
283
- if (error.customNodeStack) {
284
- setTimeout(() => {
285
- throw new Error(error.customNodeStack)
286
- })
287
- }
288
283
  if (error.fatal) {
289
284
  throw error
290
285
  }
@@ -389,7 +384,7 @@ function renderFunctionComponent(
389
384
  props: Record<string, unknown>,
390
385
  shouldSyncProps: boolean
391
386
  ): unknown {
392
- const { render, propSyncs } = vNode
387
+ let { render, propSyncs } = vNode
393
388
 
394
389
  if (render) {
395
390
  if (shouldSyncProps) {
@@ -399,7 +394,18 @@ function renderFunctionComponent(
399
394
  return render(props)
400
395
  }
401
396
 
402
- 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)
403
409
  if (typeof newChild === "function") {
404
410
  vNode.subs?.forEach(call) // unsub from signals observed during setup
405
411
  vNode.render = newChild
@@ -444,7 +450,7 @@ function updateHostComponent(vNode: DomVNode): VNode | null {
444
450
  function checkForTooManyConsecutiveDirtyRenders(): void {
445
451
  if (consecutiveDirtyCount > CONSECUTIVE_DIRTY_LIMIT) {
446
452
  throw new KiruError(
447
- "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."
448
454
  )
449
455
  }
450
456
  }
@@ -9,16 +9,15 @@ import { $DEV_FILE_LINK, $HMR_ACCEPT, $SIGNAL } from "../constants.js"
9
9
  import { __DEV__, isBrowser } from "../env.js"
10
10
  import { node } from "../globals.js"
11
11
  import { requestUpdate } from "../scheduler.js"
12
- import { signalSubsMap } from "./globals.js"
13
12
  import { tracking } from "./tracking.js"
14
- import type { SignalSubscriber, ReadonlySignal } from "./types.js"
13
+ import type { SignalSubscriber } from "./types.js"
15
14
  import type { HMRAccept } from "../hmr.js"
16
15
 
17
16
  export class Signal<T> {
18
17
  [$SIGNAL] = true;
19
18
  [$HMR_ACCEPT]?: HMRAccept<Signal<any>>
20
19
  displayName?: string
21
- protected $subs?: Set<SignalSubscriber<any>>
20
+ protected $subs: Set<SignalSubscriber<any>>
22
21
  protected $id: string
23
22
  protected $value: T
24
23
  protected $prevValue?: T
@@ -29,10 +28,10 @@ export class Signal<T> {
29
28
  constructor(initial: T, displayName?: string) {
30
29
  this.$id = generateRandomID()
31
30
  this.$value = initial
31
+ this.$subs = new Set()
32
32
  if (displayName) this.displayName = displayName
33
33
 
34
34
  if (__DEV__) {
35
- signalSubsMap.set(this.$id, new Set())
36
35
  this.$initialValue = safeStringify(initial)
37
36
  this[$HMR_ACCEPT] = {
38
37
  provide: () => {
@@ -40,11 +39,11 @@ export class Signal<T> {
40
39
  },
41
40
  inject: (prev) => {
42
41
  if (isBrowser) window.__kiru.devtools?.untrack(prev)
43
- signalSubsMap.get(this.$id)?.clear?.()
44
- signalSubsMap.delete(this.$id)
45
42
  this.$id = prev.$id
46
- // @ts-ignore - this handles scenarios where a reference to the prev has been encapsulated
47
- // and we need to be able to refer to the latest version of the signal.
43
+ this.$subs = prev.$subs
44
+ // this is a nice-to-have so that implementations of signal-on-signal don't need to do it themselves.
45
+ // eg. Object.assign(signal, { nestedSignal })
46
+ // it's only done by our HMR pass for top-level signals.
48
47
  prev.__next = this
49
48
 
50
49
  if (this.$initialValue === prev.$initialValue) {
@@ -55,8 +54,6 @@ export class Signal<T> {
55
54
  },
56
55
  destroy: () => {},
57
56
  } satisfies HMRAccept<Signal<any>>
58
- } else {
59
- this.$subs = new Set()
60
57
  }
61
58
 
62
59
  const n = node.current
@@ -121,33 +118,30 @@ export class Signal<T> {
121
118
  subscribe(cb: (state: T, prevState?: T) => void): () => void {
122
119
  if (__DEV__) {
123
120
  const tgt = latest(this)
124
- const subs = signalSubsMap.get(tgt.$id)!
125
- if (__DEV__ && !subs) {
121
+ if (__DEV__ && tgt.$isDisposed) {
126
122
  const name = tgt.displayName ?? tgt.$id
127
- let message = `Attempting to subscribe to a signal that has been disposed: ${name}`
123
+ let message = `Attempted to subscribe to a signal that has been disposed: ${name}`
128
124
  if ($DEV_FILE_LINK in tgt) {
129
125
  message += `\nFile: ${tgt[$DEV_FILE_LINK]}`
130
126
  }
131
127
  message += `\nInitial value: ${tgt.$initialValue}`
132
128
  throw new Error(message)
133
129
  }
134
- subs.add(cb)
135
- return () => subs.delete(cb)
136
130
  }
137
- this.$subs!.add(cb)
138
- return () => this.$subs!.delete(cb)
131
+ this.$subs.add(cb)
132
+ return () => this.$subs.delete(cb)
139
133
  }
140
134
 
141
135
  notify(filter?: (sub: SignalSubscriber) => boolean) {
142
136
  if (__DEV__) {
143
137
  const tgt = latest(this)
144
- return signalSubsMap.get(tgt.$id)?.forEach((sub) => {
138
+ return tgt.$subs.forEach((sub) => {
145
139
  if (filter && !filter(sub)) return
146
140
  const { $value, $prevValue } = latest(this)
147
141
  return sub($value, $prevValue)
148
142
  })
149
143
  }
150
- this.$subs!.forEach((sub) => {
144
+ this.$subs.forEach((sub) => {
151
145
  if (filter && !filter(sub)) return
152
146
  return sub(this.$value, this.$prevValue)
153
147
  })
@@ -158,43 +152,9 @@ export class Signal<T> {
158
152
  }
159
153
 
160
154
  static subscribers(signal: Signal<any>) {
161
- if (__DEV__) {
162
- signal = latest(signal)
163
- return signalSubsMap.get(signal.$id)!
164
- }
165
155
  return signal.$subs
166
156
  }
167
157
 
168
- static makeReadonly<T>(signal: Signal<T>): ReadonlySignal<T> {
169
- if (__DEV__) signal = latest(signal)
170
- const desc = Object.getOwnPropertyDescriptor(signal, "value")
171
- if (desc && !desc.writable) return signal
172
- return Object.defineProperty(signal, "value", {
173
- get: function (this: Signal<T>) {
174
- Signal.entangle(this)
175
- return this.$value
176
- },
177
- configurable: true,
178
- })
179
- }
180
-
181
- static makeWritable<T>(signal: Signal<T>): Signal<T> {
182
- if (__DEV__) signal = latest(signal)
183
- const desc = Object.getOwnPropertyDescriptor(signal, "value")
184
- if (desc && desc.writable) return signal
185
- return Object.defineProperty(signal, "value", {
186
- get: function (this: Signal<T>) {
187
- Signal.entangle(this)
188
- return this.$value
189
- },
190
- set: function (this: Signal<T>, value) {
191
- this.$value = value
192
- this.notify()
193
- },
194
- configurable: true,
195
- })
196
- }
197
-
198
158
  static entangle<T>(signal: Signal<T>) {
199
159
  if (tracking.enabled === false) return
200
160
  if (__DEV__) signal = latest(signal)
@@ -202,6 +162,7 @@ export class Signal<T> {
202
162
  const vNode = node.current
203
163
  const trackedSignalObservations = tracking.current()
204
164
  if (trackedSignalObservations) {
165
+ // track non-rendering access, only track rendering access if renderMode is DOM/hydrate
205
166
  if (!vNode || (vNode && sideEffectsEnabled())) {
206
167
  trackedSignalObservations.set(signal.$id, signal)
207
168
  }
@@ -213,14 +174,14 @@ export class Signal<T> {
213
174
  }
214
175
 
215
176
  static dispose(signal: Signal<any>) {
177
+ if (signal.$isDisposed) return
178
+
216
179
  signal.$isDisposed = true
217
180
  if (__DEV__) {
218
- signal = latest(signal)
219
- signalSubsMap.delete(signal.$id)
220
- if (isBrowser) window.__kiru.devtools?.untrack(signal)
181
+ if (isBrowser) window.__kiru.devtools?.untrack(latest(signal))
221
182
  return
222
183
  }
223
- signal.$subs!.clear()
184
+ signal.$subs.clear()
224
185
  }
225
186
  }
226
187
 
@@ -1,7 +1,7 @@
1
1
  import { __DEV__ } from "../env.js"
2
2
  import { $HMR_ACCEPT } from "../constants.js"
3
3
  import { call, latest } from "../utils/index.js"
4
- import { effectQueue, signalSubsMap } from "./globals.js"
4
+ import { effectQueue } from "./globals.js"
5
5
  import { executeWithTracking } from "./tracking.js"
6
6
  import { Signal } from "./base.js"
7
7
  import type { HMRAccept } from "../hmr.js"
@@ -17,14 +17,14 @@ export class ComputedSignal<T> extends Signal<T> {
17
17
  this.$isDirty = true
18
18
 
19
19
  if (__DEV__) {
20
- const inject = this[$HMR_ACCEPT]!.inject!
20
+ const { inject: baseInject } = this[$HMR_ACCEPT]!
21
21
  // @ts-expect-error this is fine 😅
22
22
  this[$HMR_ACCEPT] = {
23
23
  provide: () => {
24
24
  return this
25
25
  },
26
26
  inject: (prev) => {
27
- inject(prev)
27
+ baseInject(prev)
28
28
  // Stop any pending reactions on the previous instance and mark
29
29
  // this computed as dirty so it will recompute with the latest
30
30
  // dependencies after HMR.
@@ -96,11 +96,7 @@ export class ComputedSignal<T> extends Signal<T> {
96
96
  fn: () => $getter(computed.$value),
97
97
  onDepChanged: () => {
98
98
  computed.$isDirty = true
99
- if (__DEV__) {
100
- if (!signalSubsMap?.get(id)?.size) return
101
- } else {
102
- if (!computed.$subs!.size) return
103
- }
99
+ if (!computed.$subs.size) return
104
100
  ComputedSignal.run(computed)
105
101
  if (Object.is(computed.$value, computed.$prevValue)) return
106
102
  computed.notify()
@@ -1,4 +1 @@
1
- import type { SignalSubscriber } from "./types.js"
2
-
3
1
  export const effectQueue = new Map<string, Function>()
4
- export const signalSubsMap: Map<string, Set<SignalSubscriber<any>>> = new Map()
@@ -1,13 +1,10 @@
1
1
  import type { Signal } from "./base.js"
2
2
 
3
- export type ReadonlySignal<T> = Signal<T> & {
4
- readonly value: T
5
- }
6
3
  export type SignalSubscriber<T = unknown> = (value: T, prevValue?: T) => void
7
4
 
8
5
  export type SignalValues<T extends readonly Signal<unknown>[]> = {
9
6
  [I in keyof T]: T[I] extends Signal<infer V>
10
- ? V extends Kiru.StatefulPromiseBase<infer P>
7
+ ? V extends Kiru.StatefulPromise<infer P>
11
8
  ? P
12
9
  : V
13
10
  : never
package/src/ssr/server.ts CHANGED
@@ -1,4 +1,3 @@
1
- import { Readable } from "node:stream"
2
1
  import { Fragment } from "../element.js"
3
2
  import { renderMode } from "../globals.js"
4
3
  import { STREAMED_DATA_EVENT } from "../constants.js"
@@ -20,13 +19,21 @@ d.currentScript.remove()
20
19
  </script>
21
20
  `
22
21
 
23
- export function renderToReadableStream(element: JSX.Element): {
22
+ export interface ReadableStreamRenderResult {
24
23
  immediate: string
25
- stream: Readable
26
- } {
27
- const stream = new Readable({ read() {} })
24
+ stream: ReadableStream
25
+ }
26
+
27
+ export function renderToReadableStream(element: JSX.Element): ReadableStreamRenderResult {
28
+ let controller!: ReadableStreamDefaultController<string>
29
+ const stream = new ReadableStream<string>({
30
+ start(c) {
31
+ controller = c
32
+ },
33
+ })
34
+
28
35
  const rootNode = Fragment({ children: element })
29
- const streamPromises = new Set<Kiru.StatefulPromiseBase<unknown>>()
36
+ const streamPromises = new Set<Kiru.StatefulPromise<unknown>>()
30
37
  const pendingWritePromises: Promise<void>[] = []
31
38
 
32
39
  let immediate = ""
@@ -43,7 +50,7 @@ export function renderToReadableStream(element: JSX.Element): {
43
50
  .catch(() => ({ error: promise.error?.message }))
44
51
  .then((value) => {
45
52
  const content = JSON.stringify(value)
46
- stream.push(
53
+ controller.enqueue(
47
54
  `<script id="${promise.id}" k-data type="application/json">${content}</script>`
48
55
  )
49
56
  })
@@ -55,16 +62,16 @@ export function renderToReadableStream(element: JSX.Element): {
55
62
 
56
63
  const prev = renderMode.current
57
64
  renderMode.current = "stream"
58
- headlessRender(ctx, rootNode, null, 0)
65
+ headlessRender(ctx, rootNode)
59
66
  renderMode.current = prev
60
67
 
61
68
  if (pendingWritePromises.length > 0) {
62
69
  Promise.all(pendingWritePromises).then(() => {
63
- stream.push(STREAMED_DATA_SETUP)
64
- stream.push(null)
70
+ controller.enqueue(STREAMED_DATA_SETUP)
71
+ controller.close()
65
72
  })
66
73
  } else {
67
- stream.push(null)
74
+ controller.close()
68
75
  }
69
76
 
70
77
  return { immediate, stream }
package/src/types.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ReadonlySignal, Signal as SignalClass } from "./signals"
1
+ import type { Signal as SignalClass } from "./signals"
2
2
  import type { $CONTEXT, $ERROR_BOUNDARY, $FRAGMENT } from "./constants"
3
3
  import type { KiruGlobalContext } from "./globalContext"
4
4
  import type {
@@ -123,9 +123,9 @@ declare global {
123
123
  }
124
124
 
125
125
  export interface FC<T = {}> {
126
- (props: T):
127
- | Exclude<JSX.Element, Kiru.FC<any>>
128
- | ((props: T) => JSX.Element)
126
+ (
127
+ props: T
128
+ ): Exclude<JSX.Element, Kiru.FC<any>> | ((props: T) => JSX.Element)
129
129
  /** Used to display the name of the component in devtools */
130
130
  displayName?: string
131
131
  }
@@ -148,13 +148,13 @@ declare global {
148
148
  error?: Error
149
149
  }
150
150
 
151
- interface StatefulPromiseBase<T> extends Promise<T>, PromiseState<T> {}
151
+ interface StatefulPromise<T> extends Promise<T>, PromiseState<T> {}
152
152
 
153
153
  type RenderMode = "dom" | "hydrate" | "string" | "stream"
154
154
 
155
155
  type StateSetter<T> = T | ((prev: T) => T)
156
156
 
157
- type Signal<T> = SignalClass<T> | ReadonlySignal<T>
157
+ type Signal<T> = SignalClass<T>
158
158
 
159
159
  type ExoticSymbol =
160
160
  | typeof $FRAGMENT
@@ -1,6 +1,7 @@
1
1
  export * from "./compare.js"
2
2
  export * from "./dom.js"
3
3
  export * from "./format.js"
4
+ export * from "./generateId.js"
4
5
  export * from "./runtime.js"
6
+ export * from "./stream.js"
5
7
  export * from "./vdom.js"
6
- export * from "./generateId.js"
@@ -0,0 +1,17 @@
1
+ import { $STREAM_DATA } from "../constants.js"
2
+
3
+ export interface StreamDataThrowValue {
4
+ [$STREAM_DATA]: {
5
+ fallback?: JSX.Element
6
+ data: Kiru.StatefulPromise<unknown>[]
7
+ }
8
+ }
9
+
10
+ /**
11
+ * Returns true if the value is a {@link StreamDataThrowValue}
12
+ */
13
+ export function isStreamDataThrowValue(
14
+ value: unknown
15
+ ): value is StreamDataThrowValue {
16
+ return typeof value === "object" && !!value && $STREAM_DATA in value
17
+ }
@@ -1,22 +0,0 @@
1
- import { $STREAM_DATA } from "./constants.js";
2
- import { Signal } from "./signals/base.js";
3
- export interface StreamDataThrowValue {
4
- [$STREAM_DATA]: {
5
- fallback?: JSX.Element;
6
- data: Kiru.StatefulPromiseBase<unknown>[];
7
- };
8
- }
9
- /**
10
- * Returns true if the value is a {@link StreamDataThrowValue}
11
- */
12
- export declare function isStreamDataThrowValue(value: unknown): value is StreamDataThrowValue;
13
- /**
14
- * Returns true if the value is a {@link Kiru.StatefulPromiseBase}
15
- */
16
- export declare function isStatefulPromise(thing: unknown): thing is Kiru.StatefulPromiseBase<unknown>;
17
- type StatefulPromise<T> = Kiru.StatefulPromiseBase<T> & {
18
- isPending: Signal<boolean>;
19
- };
20
- export declare function statefulPromise<T>(callback: (signal: AbortSignal) => Promise<T>): StatefulPromise<T>;
21
- export {};
22
- //# sourceMappingURL=statefulPromise.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"statefulPromise.d.ts","sourceRoot":"","sources":["../src/statefulPromise.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,gBAAgB,CAAA;AAElE,OAAO,EAAE,MAAM,EAAU,MAAM,mBAAmB,CAAA;AAIlD,MAAM,WAAW,oBAAoB;IACnC,CAAC,YAAY,CAAC,EAAE;QACd,QAAQ,CAAC,EAAE,GAAG,CAAC,OAAO,CAAA;QACtB,IAAI,EAAE,IAAI,CAAC,mBAAmB,CAAC,OAAO,CAAC,EAAE,CAAA;KAC1C,CAAA;CACF;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,OAAO,GACb,KAAK,IAAI,oBAAoB,CAE/B;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,OAAO,GACb,KAAK,IAAI,IAAI,CAAC,mBAAmB,CAAC,OAAO,CAAC,CAE5C;AAID,KAAK,eAAe,CAAC,CAAC,IAAI,IAAI,CAAC,mBAAmB,CAAC,CAAC,CAAC,GAAG;IACtD,SAAS,EAAE,MAAM,CAAC,OAAO,CAAC,CAAA;CAC3B,CAAA;AAED,wBAAgB,eAAe,CAAC,CAAC,EAC/B,QAAQ,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC,GAC5C,eAAe,CAAC,CAAC,CAAC,CAuDpB"}