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.
- package/dist/components/derive.d.ts +7 -6
- package/dist/components/derive.d.ts.map +1 -1
- package/dist/components/derive.js +9 -10
- package/dist/components/derive.js.map +1 -1
- package/dist/components/errorBoundary.d.ts +1 -1
- package/dist/components/errorBoundary.d.ts.map +1 -1
- package/dist/components/errorBoundary.js.map +1 -1
- package/dist/components/transition.d.ts +1 -2
- package/dist/components/transition.d.ts.map +1 -1
- package/dist/components/transition.js +26 -15
- package/dist/components/transition.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/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.js +13 -8
- 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 +12 -54
- package/dist/signals/base.js.map +1 -1
- package/dist/signals/computed.d.ts.map +1 -1
- package/dist/signals/computed.js +5 -11
- 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 -4
- 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 +3 -3
- 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/package.json +1 -13
- package/src/components/derive.ts +29 -36
- package/src/components/errorBoundary.ts +3 -1
- package/src/components/transition.ts +29 -16
- package/src/error.ts +11 -19
- package/src/headlessRender.ts +5 -5
- package/src/hmr.ts +13 -3
- package/src/index.ts +1 -1
- package/src/renderToString.ts +1 -1
- package/src/resource.ts +207 -0
- package/src/scheduler.ts +14 -8
- package/src/signals/base.ts +18 -57
- package/src/signals/computed.ts +4 -8
- package/src/signals/globals.ts +0 -3
- package/src/signals/types.ts +1 -4
- package/src/ssr/server.ts +18 -11
- package/src/types.ts +6 -6
- package/src/utils/index.ts +2 -1
- package/src/utils/stream.ts +17 -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/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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
}
|
package/src/signals/base.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
47
|
-
//
|
|
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
|
-
|
|
125
|
-
if (__DEV__ && !subs) {
|
|
121
|
+
if (__DEV__ && tgt.$isDisposed) {
|
|
126
122
|
const name = tgt.displayName ?? tgt.$id
|
|
127
|
-
let message = `
|
|
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
|
|
138
|
-
return () => this.$subs
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
184
|
+
signal.$subs.clear()
|
|
224
185
|
}
|
|
225
186
|
}
|
|
226
187
|
|
package/src/signals/computed.ts
CHANGED
|
@@ -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
|
|
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]
|
|
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
|
-
|
|
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 (
|
|
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()
|
package/src/signals/globals.ts
CHANGED
package/src/signals/types.ts
CHANGED
|
@@ -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.
|
|
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
|
|
22
|
+
export interface ReadableStreamRenderResult {
|
|
24
23
|
immediate: string
|
|
25
|
-
stream:
|
|
26
|
-
}
|
|
27
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
65
|
+
headlessRender(ctx, rootNode)
|
|
59
66
|
renderMode.current = prev
|
|
60
67
|
|
|
61
68
|
if (pendingWritePromises.length > 0) {
|
|
62
69
|
Promise.all(pendingWritePromises).then(() => {
|
|
63
|
-
|
|
64
|
-
|
|
70
|
+
controller.enqueue(STREAMED_DATA_SETUP)
|
|
71
|
+
controller.close()
|
|
65
72
|
})
|
|
66
73
|
} else {
|
|
67
|
-
|
|
74
|
+
controller.close()
|
|
68
75
|
}
|
|
69
76
|
|
|
70
77
|
return { immediate, stream }
|
package/src/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
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
|
-
(
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
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>
|
|
157
|
+
type Signal<T> = SignalClass<T>
|
|
158
158
|
|
|
159
159
|
type ExoticSymbol =
|
|
160
160
|
| typeof $FRAGMENT
|
package/src/utils/index.ts
CHANGED
|
@@ -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"}
|