kiru 1.4.1 → 1.5.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 (110) hide show
  1. package/dist/components/derive.d.ts +1 -1
  2. package/dist/components/derive.d.ts.map +1 -1
  3. package/dist/constants.d.ts +2 -1
  4. package/dist/constants.d.ts.map +1 -1
  5. package/dist/constants.js +2 -1
  6. package/dist/constants.js.map +1 -1
  7. package/dist/devtools.d.ts +1 -1
  8. package/dist/devtools.d.ts.map +1 -1
  9. package/dist/dom/nodes.d.ts +1 -1
  10. package/dist/dom/nodes.d.ts.map +1 -1
  11. package/dist/dom/nodes.js.map +1 -1
  12. package/dist/dom/props.js.map +1 -1
  13. package/dist/globalContext.d.ts +2 -2
  14. package/dist/globalContext.d.ts.map +1 -1
  15. package/dist/headlessRender.d.ts.map +1 -1
  16. package/dist/headlessRender.js +3 -0
  17. package/dist/headlessRender.js.map +1 -1
  18. package/dist/hooks/onCleanup.d.ts.map +1 -1
  19. package/dist/hooks/onCleanup.js +3 -1
  20. package/dist/hooks/onCleanup.js.map +1 -1
  21. package/dist/hooks/setup.d.ts.map +1 -1
  22. package/dist/hooks/setup.js +11 -28
  23. package/dist/hooks/setup.js.map +1 -1
  24. package/dist/hooks/utils.d.ts.map +1 -1
  25. package/dist/hooks/utils.js +2 -1
  26. package/dist/hooks/utils.js.map +1 -1
  27. package/dist/hydration.d.ts +1 -1
  28. package/dist/hydration.d.ts.map +1 -1
  29. package/dist/index.d.ts +1 -1
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/profiling.d.ts +1 -1
  32. package/dist/profiling.d.ts.map +1 -1
  33. package/dist/reconciler.d.ts.map +1 -1
  34. package/dist/reconciler.js +38 -6
  35. package/dist/reconciler.js.map +1 -1
  36. package/dist/resource.d.ts +11 -5
  37. package/dist/resource.d.ts.map +1 -1
  38. package/dist/resource.js +83 -39
  39. package/dist/resource.js.map +1 -1
  40. package/dist/router/client/index.d.ts +1 -1
  41. package/dist/router/client/index.d.ts.map +1 -1
  42. package/dist/router/globals.d.ts +2 -2
  43. package/dist/router/globals.d.ts.map +1 -1
  44. package/dist/router/link.d.ts +1 -1
  45. package/dist/router/link.d.ts.map +1 -1
  46. package/dist/router/pageConfig.d.ts +1 -1
  47. package/dist/router/pageConfig.d.ts.map +1 -1
  48. package/dist/router/types.d.ts +3 -3
  49. package/dist/router/types.d.ts.map +1 -1
  50. package/dist/router/types.internal.d.ts +2 -2
  51. package/dist/router/types.internal.d.ts.map +1 -1
  52. package/dist/router/utils/index.d.ts +2 -2
  53. package/dist/router/utils/index.d.ts.map +1 -1
  54. package/dist/scheduler.d.ts.map +1 -1
  55. package/dist/scheduler.js +15 -2
  56. package/dist/scheduler.js.map +1 -1
  57. package/dist/signals/base.d.ts +1 -0
  58. package/dist/signals/base.d.ts.map +1 -1
  59. package/dist/signals/base.js +14 -2
  60. package/dist/signals/base.js.map +1 -1
  61. package/dist/signals/effect.d.ts.map +1 -1
  62. package/dist/signals/effect.js +8 -0
  63. package/dist/signals/effect.js.map +1 -1
  64. package/dist/signals/tracking.d.ts.map +1 -1
  65. package/dist/signals/tracking.js +13 -14
  66. package/dist/signals/tracking.js.map +1 -1
  67. package/dist/ssr/client.d.ts +1 -1
  68. package/dist/ssr/client.d.ts.map +1 -1
  69. package/dist/types.d.ts +31 -7
  70. package/dist/types.d.ts.map +1 -1
  71. package/dist/types.dom.d.ts +190 -2
  72. package/dist/types.dom.d.ts.map +1 -1
  73. package/dist/types.utils.d.ts +28 -21
  74. package/dist/types.utils.d.ts.map +1 -1
  75. package/dist/utils/vdom.d.ts.map +1 -1
  76. package/dist/utils/vdom.js +5 -2
  77. package/dist/utils/vdom.js.map +1 -1
  78. package/package.json +1 -1
  79. package/src/components/derive.ts +1 -1
  80. package/src/constants.ts +2 -0
  81. package/src/devtools.ts +1 -1
  82. package/src/dom/commit.ts +1 -1
  83. package/src/dom/nodes.ts +8 -3
  84. package/src/dom/props.ts +6 -6
  85. package/src/globalContext.ts +2 -2
  86. package/src/headlessRender.ts +4 -1
  87. package/src/hooks/onCleanup.ts +3 -1
  88. package/src/hooks/setup.ts +17 -31
  89. package/src/hooks/utils.ts +2 -1
  90. package/src/hydration.ts +1 -1
  91. package/src/index.ts +1 -1
  92. package/src/profiling.ts +1 -1
  93. package/src/reconciler.ts +51 -9
  94. package/src/resource.ts +112 -44
  95. package/src/router/client/index.ts +2 -2
  96. package/src/router/globals.ts +2 -2
  97. package/src/router/link.ts +1 -1
  98. package/src/router/pageConfig.ts +1 -1
  99. package/src/router/types.internal.ts +2 -2
  100. package/src/router/types.ts +3 -3
  101. package/src/router/utils/index.ts +1 -1
  102. package/src/scheduler.ts +20 -3
  103. package/src/signals/base.ts +20 -2
  104. package/src/signals/effect.ts +8 -0
  105. package/src/signals/tracking.ts +17 -15
  106. package/src/ssr/client.ts +1 -1
  107. package/src/types.dom.ts +270 -53
  108. package/src/types.ts +36 -32
  109. package/src/types.utils.ts +56 -22
  110. package/src/utils/vdom.ts +7 -1
package/src/reconciler.ts CHANGED
@@ -1,4 +1,9 @@
1
- import { $FRAGMENT, FLAG_PLACEMENT, FLAG_UPDATE } from "./constants.js"
1
+ import {
2
+ $FRAGMENT,
3
+ $INLINE_FN,
4
+ FLAG_PLACEMENT,
5
+ FLAG_UPDATE,
6
+ } from "./constants.js"
2
7
  import {
3
8
  getVNodeApp,
4
9
  isElement,
@@ -189,6 +194,10 @@ function updateSlot(
189
194
  }
190
195
  return updateFragment(parent, oldChild, child)
191
196
  }
197
+ if (typeof child === "function") {
198
+ if (key !== null) return null
199
+ return updateInlineFnChild(parent, oldChild, child)
200
+ }
192
201
  return null
193
202
  }
194
203
 
@@ -263,6 +272,23 @@ function updateFragment(
263
272
  return oldChild
264
273
  }
265
274
 
275
+ function updateInlineFnChild(
276
+ parent: VNode,
277
+ oldChild: VNode | null,
278
+ expr: Function
279
+ ) {
280
+ if (oldChild === null || oldChild.type !== $INLINE_FN) {
281
+ return createVNode(parent, $INLINE_FN, { expr })
282
+ }
283
+ if (__DEV__) {
284
+ dev_emitUpdateNode()
285
+ }
286
+ oldChild.props = { expr }
287
+ oldChild.flags |= FLAG_UPDATE
288
+ oldChild.sibling = null
289
+ return oldChild
290
+ }
291
+
266
292
  function createChild(parent: VNode, child: unknown): VNode | null {
267
293
  if (isValidTextChild(child)) {
268
294
  return createVNode(parent, "#text", { nodeValue: "" + child })
@@ -283,6 +309,10 @@ function createChild(parent: VNode, child: unknown): VNode | null {
283
309
  return createVNode(parent, $FRAGMENT, { children: child })
284
310
  }
285
311
 
312
+ if (typeof child === "function") {
313
+ return createVNode(parent, $INLINE_FN, { expr: child })
314
+ }
315
+
286
316
  return null
287
317
  }
288
318
 
@@ -316,14 +346,11 @@ function updateFromMap(
316
346
  const isSig = Signal.isSignal(child)
317
347
  if (isSig || isValidTextChild(child)) {
318
348
  const oldChild = existingChildren.get(index)
319
- if (oldChild) {
349
+ if (oldChild?.type === "#text") {
320
350
  if (oldChild.props.nodeValue === child) {
321
351
  return oldChild
322
352
  }
323
- if (
324
- oldChild.type === "#text" &&
325
- Signal.isSignal(oldChild.props.nodeValue)
326
- ) {
353
+ if (Signal.isSignal(oldChild.props.nodeValue)) {
327
354
  oldChild.cleanups?.["nodeValue"]?.()
328
355
  }
329
356
  }
@@ -356,11 +383,11 @@ function updateFromMap(
356
383
 
357
384
  if (Array.isArray(child)) {
358
385
  const props = { children: child }
359
- const oldChild = existingChildren.get(index)
360
386
  if (__DEV__) {
361
387
  markListChild(child)
362
388
  }
363
- if (oldChild) {
389
+ const oldChild = existingChildren.get(index)
390
+ if (oldChild?.type === $FRAGMENT) {
364
391
  if (__DEV__) {
365
392
  dev_emitUpdateNode()
366
393
  }
@@ -372,6 +399,21 @@ function updateFromMap(
372
399
  return createVNode(parent, $FRAGMENT, props, null, index)
373
400
  }
374
401
 
402
+ if (typeof child === "function") {
403
+ const props = { expr: child }
404
+ const oldChild = existingChildren.get(index)
405
+ if (oldChild?.type === $INLINE_FN) {
406
+ if (__DEV__) {
407
+ dev_emitUpdateNode()
408
+ }
409
+ oldChild.flags |= FLAG_UPDATE
410
+ oldChild.props = props
411
+ return oldChild
412
+ }
413
+
414
+ return createVNode(parent, $INLINE_FN, props, null, index)
415
+ }
416
+
375
417
  return null
376
418
  }
377
419
 
@@ -472,7 +514,7 @@ function getNearestParentFcTag(vNode: VNode) {
472
514
  function createVNode(
473
515
  parent: VNode,
474
516
  type: VNode["type"],
475
- props: VNode["props"],
517
+ props?: VNode["props"],
476
518
  key: VNode["key"] = null,
477
519
  index = 0
478
520
  ): VNode {
package/src/resource.ts CHANGED
@@ -1,23 +1,20 @@
1
1
  import { $HMR_ACCEPT, STREAMED_DATA_EVENT } from "./constants.js"
2
2
  import { hydrationMode, node, renderMode } from "./globals.js"
3
3
  import { Signal, signal } from "./signals/base.js"
4
+ import { executeWithTracking } from "./signals/tracking.js"
4
5
  import { createVNodeId, registerVNodeCleanup } from "./utils/vdom.js"
5
6
  import { generateRandomID } from "./utils/generateId.js"
6
7
  import { __DEV__, isBrowser } from "./env.js"
7
8
  import { GenericHMRAcceptor, performHmrAccept } from "./hmr.js"
8
9
 
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
- }
10
+ export type ResourceSource = Record<string, Signal<unknown>> | Signal<unknown>
19
11
 
20
- const resourceMeta = new WeakMap<Kiru.VNode, { id: string; index: number }>()
12
+ type InnerOf<T> = T extends Kiru.Signal<infer V> ? V : never
13
+
14
+ type UnwrapResourceSource<T extends ResourceSource> =
15
+ T extends Kiru.Signal<unknown>
16
+ ? InnerOf<T>
17
+ : { [K in keyof T]: InnerOf<T[K]> }
21
18
 
22
19
  interface ResourceState<T> {
23
20
  error: Signal<Error | null>
@@ -32,14 +29,31 @@ export interface ResourceLoaderContext {
32
29
  signal: AbortSignal
33
30
  }
34
31
 
35
- export function resource<T, Source>(
36
- source: Kiru.Signal<Source>,
37
- callback: (source: Source, ctx: ResourceLoaderContext) => Promise<T>
32
+ const resourceMeta = new WeakMap<Kiru.VNode, { id: string; index: number }>()
33
+
34
+ export function resource<T>(
35
+ callback: (ctx: ResourceLoaderContext) => Promise<T>
36
+ ): Resource<T>
37
+ export function resource<T, Source extends ResourceSource>(
38
+ source: Source,
39
+ callback: (
40
+ source: UnwrapResourceSource<Source>,
41
+ ctx: ResourceLoaderContext
42
+ ) => Promise<T>
43
+ ): Resource<T>
44
+ export function resource<T, Source extends ResourceSource>(
45
+ callbackOrSource: Source | ((ctx: ResourceLoaderContext) => Promise<T>),
46
+ callback?: (
47
+ source: UnwrapResourceSource<Source>,
48
+ ctx: ResourceLoaderContext
49
+ ) => Promise<T>
38
50
  ): Resource<T> {
39
51
  const data = signal(void 0 as T)
40
52
  const error = signal<Error | null>(null)
41
53
  const isPending = signal(true)
42
54
 
55
+ let controller = new AbortController()
56
+
43
57
  let promiseId = ""
44
58
  const vNode = node.current
45
59
  if (!vNode) {
@@ -62,17 +76,34 @@ export function resource<T, Source>(
62
76
  promiseId = generateRandomID()
63
77
  }
64
78
 
65
- const unsub = source.subscribe((src) => {
66
- resource.promise = createPromise(src)
79
+ const updateResource = () => {
80
+ resource.promise = createPromise()
67
81
  resource.notify()
68
- })
82
+ }
69
83
 
70
- let controller = new AbortController()
84
+ let unsubFromSource: (() => void) | undefined
85
+ if (typeof callbackOrSource === "object") {
86
+ if (Signal.isSignal(callbackOrSource)) {
87
+ unsubFromSource = callbackOrSource.subscribe(updateResource)
88
+ } else {
89
+ const unsubs: (() => void)[] = []
90
+ for (const key in callbackOrSource) {
91
+ if (!Signal.isSignal(callbackOrSource[key])) continue
92
+ unsubs.push(callbackOrSource[key].subscribe(updateResource))
93
+ }
94
+ unsubFromSource = () => {
95
+ unsubs.forEach((unsub) => unsub())
96
+ }
97
+ }
98
+ }
99
+
100
+ const observedSignalUnsubs = new Map<string, () => void>()
71
101
  const dispose = () => {
72
102
  if (!controller.signal.aborted) controller.abort()
73
103
  Signal.dispose(data)
74
104
  Signal.dispose(isPending)
75
- unsub()
105
+ observedSignalUnsubs.forEach((unsub) => unsub())
106
+ unsubFromSource?.()
76
107
  }
77
108
 
78
109
  if (vNode) {
@@ -85,7 +116,7 @@ export function resource<T, Source>(
85
116
  promise: undefined as unknown as Kiru.StatefulPromise<T>,
86
117
  refetch() {
87
118
  data.value = void 0 as T
88
- this.promise = createPromise(source.peek())
119
+ this.promise = createPromise()
89
120
  },
90
121
  dispose,
91
122
  })
@@ -111,33 +142,38 @@ export function resource<T, Source>(
111
142
  }
112
143
  }
113
144
 
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> {
145
+ function createPromise(): Kiru.StatefulPromise<T> {
123
146
  controller.abort()
124
147
  const ctrl = (controller = new AbortController())
125
148
  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
- }
149
+ const newPromise = executeWithTracking({
150
+ fn: () => {
151
+ let promise: Promise<T>
152
+ if (renderMode.current === "string") {
153
+ // if we're rendering to a string, there's no need to fire the callback
154
+ promise = Promise.resolve() as Promise<T>
155
+ } else if (
156
+ renderMode.current === "hydrate" &&
157
+ hydrationMode.current === "dynamic"
158
+ ) {
159
+ // if we're hydrating and the hydration mode is not static,
160
+ // we need to resolve the promise from cache/event
161
+ promise = resolveDeferredPromise<T>(promiseId, ctrl.signal)
162
+ } else {
163
+ // stream / dom / (hydrate + static)
164
+ if (typeof callbackOrSource === "function") {
165
+ promise = callbackOrSource({ signal: ctrl.signal })
166
+ } else {
167
+ const source = unwrapResourceSource(callbackOrSource)
168
+ promise = callback!(source, { signal: ctrl.signal })
169
+ }
170
+ }
171
+ return promise
172
+ },
173
+ id: Signal.id(resource),
174
+ onDepChanged: updateResource,
175
+ subs: observedSignalUnsubs,
176
+ })
141
177
 
142
178
  const statefulPromise: Kiru.StatefulPromise<T> = Object.assign(newPromise, {
143
179
  id: promiseId,
@@ -162,6 +198,12 @@ export function resource<T, Source>(
162
198
  return statefulPromise
163
199
  }
164
200
 
201
+ if (__DEV__ && isBrowser && window.__kiru.HMRContext?.isReplacement()) {
202
+ queueMicrotask(() => (resource.promise = createPromise()))
203
+ } else {
204
+ resource.promise = createPromise()
205
+ }
206
+
165
207
  return resource
166
208
  }
167
209
 
@@ -205,3 +247,29 @@ function resolveDeferredPromise<T>(
205
247
  })
206
248
  })
207
249
  }
250
+
251
+ /**
252
+ * Returns true if the value is a {@link Resource}
253
+ */
254
+ export function isResource(thing: unknown): thing is Resource<unknown> {
255
+ return (
256
+ Signal.isSignal(thing) &&
257
+ "promise" in thing &&
258
+ thing["promise"] instanceof Promise
259
+ )
260
+ }
261
+
262
+ function unwrapResourceSource<T extends ResourceSource>(
263
+ source: T
264
+ ): UnwrapResourceSource<T> {
265
+ if (Signal.isSignal(source)) {
266
+ return source.peek() as UnwrapResourceSource<T>
267
+ }
268
+ const out: Record<string, unknown> = {}
269
+ for (const key in source) {
270
+ if (Signal.isSignal(source[key])) {
271
+ out[key] = source[key].peek()
272
+ }
273
+ }
274
+ return out as UnwrapResourceSource<T>
275
+ }
@@ -9,8 +9,8 @@ import {
9
9
  match404Route,
10
10
  parseQuery,
11
11
  } from "../utils/index.js"
12
- import type { FormattedViteImportMap, PageModule } from "../types.internal"
13
- import type { FileRouterConfig, FileRouterPreloadConfig } from "../types"
12
+ import type { FormattedViteImportMap, PageModule } from "../types.internal.js"
13
+ import type { FileRouterConfig, FileRouterPreloadConfig } from "../types.js"
14
14
  import { fileRouterInstance, fileRouterRoute, routerCache } from "../globals.js"
15
15
  import { FileRouterController } from "../fileRouterController.js"
16
16
  import { FileRouterDataLoadError } from "../errors.js"
@@ -1,5 +1,5 @@
1
- import type { RouterCache } from "./cache"
2
- import type { FileRouterController } from "./fileRouterController"
1
+ import type { RouterCache } from "./cache.js"
2
+ import type { FileRouterController } from "./fileRouterController.js"
3
3
 
4
4
  export const fileRouterInstance = {
5
5
  current: null as FileRouterController | null,
@@ -1,4 +1,4 @@
1
- import type { ElementProps } from "../types"
1
+ import type { ElementProps } from "../types.js"
2
2
  import { createElement } from "../element.js"
3
3
  import { useFileRouter } from "./context.js"
4
4
 
@@ -1,6 +1,6 @@
1
1
  import { __DEV__, isBrowser } from "../env.js"
2
2
  import { fileRouterInstance } from "./globals.js"
3
- import type { PageConfig } from "./types"
3
+ import type { PageConfig } from "./types.js"
4
4
 
5
5
  export function definePageConfig<T>(config: PageConfig<T>): PageConfig<T> {
6
6
  if (__DEV__ && isBrowser) {
@@ -1,5 +1,5 @@
1
- import type { FileRouterContextType } from "./context"
2
- import type { PageConfig } from "./types"
1
+ import type { FileRouterContextType } from "./context.js"
2
+ import type { PageConfig } from "./types.js"
3
3
 
4
4
  export interface CurrentPage {
5
5
  component: Kiru.FC<any>
@@ -1,10 +1,10 @@
1
- import type { AsyncTaskState } from "../types.utils"
2
- import type { FileRouterDataLoadError } from "./errors"
1
+ import type { AsyncTaskState } from "../types.utils.js"
2
+ import type { FileRouterDataLoadError } from "./errors.js"
3
3
  import type {
4
4
  DefaultComponentModule,
5
5
  FormattedViteImportMap,
6
6
  PageModule,
7
- } from "./types.internal"
7
+ } from "./types.internal.js"
8
8
 
9
9
  export interface FileRouterPreloadConfig {
10
10
  pages: FormattedViteImportMap
@@ -4,7 +4,7 @@ import type {
4
4
  FormattedViteImportMap,
5
5
  RouteMatch,
6
6
  ViteImportMap,
7
- } from "../types.internal"
7
+ } from "../types.internal.js"
8
8
 
9
9
  export {
10
10
  formatViteImportMap,
package/src/scheduler.ts CHANGED
@@ -1,6 +1,12 @@
1
- import type { DomVNode, ErrorBoundaryNode, FunctionVNode } from "./types.utils"
1
+ import type {
2
+ DomVNode,
3
+ ErrorBoundaryNode,
4
+ FunctionVNode,
5
+ InlineFnNode,
6
+ } from "./types.utils.js"
2
7
  import {
3
8
  $ERROR_BOUNDARY,
9
+ $INLINE_FN,
4
10
  CONSECUTIVE_DIRTY_LIMIT,
5
11
  FLAG_DELETION,
6
12
  FLAG_DIRTY,
@@ -30,7 +36,7 @@ import { node, postEffectCleanups, renderMode, setups } from "./globals.js"
30
36
  import { hydrationStack } from "./hydration.js"
31
37
  import { reconcileChildren } from "./reconciler.js"
32
38
  import { isHmrUpdate } from "./hmr.js"
33
- import type { AppHandle } from "./appHandle"
39
+ import type { AppHandle } from "./appHandle.js"
34
40
 
35
41
  type VNode = Kiru.VNode
36
42
 
@@ -298,7 +304,18 @@ function updateExoticComponent(vNode: VNode): VNode | null {
298
304
  const { props, type } = vNode
299
305
  let children = props.children
300
306
 
301
- if (type === $ERROR_BOUNDARY) {
307
+ if (type === $INLINE_FN) {
308
+ node.current = vNode
309
+ let render = (props as InlineFnNode["props"]).expr
310
+ if (__DEV__) {
311
+ render = latest(render)
312
+ }
313
+ try {
314
+ children = render()
315
+ } finally {
316
+ node.current = null
317
+ }
318
+ } else if (type === $ERROR_BOUNDARY) {
302
319
  const n = vNode as ErrorBoundaryNode
303
320
  const { error } = n
304
321
  if (error) {
@@ -5,8 +5,14 @@ import {
5
5
  generateRandomID,
6
6
  registerVNodeCleanup,
7
7
  } from "../utils/index.js"
8
- import { $DEV_FILE_LINK, $HMR_ACCEPT, $SIGNAL } from "../constants.js"
8
+ import {
9
+ $DEV_FILE_LINK,
10
+ $HMR_ACCEPT,
11
+ $INLINE_FN,
12
+ $SIGNAL,
13
+ } from "../constants.js"
9
14
  import { __DEV__, isBrowser } from "../env.js"
15
+ import { KiruError } from "../error.js"
10
16
  import { node } from "../globals.js"
11
17
  import { requestUpdate } from "../scheduler.js"
12
18
  import { tracking } from "./tracking.js"
@@ -58,7 +64,15 @@ export class Signal<T> {
58
64
 
59
65
  const n = node.current
60
66
  if (n) {
61
- registerVNodeCleanup(n, this.$id, Signal.dispose.bind(null, this))
67
+ if (__DEV__ && n.type === $INLINE_FN) {
68
+ throw new KiruError({
69
+ message: "Signals cannot be created inside inline functions",
70
+ vNode: n,
71
+ })
72
+ }
73
+ if (sideEffectsEnabled()) {
74
+ registerVNodeCleanup(n, this.$id, Signal.dispose.bind(null, this))
75
+ }
62
76
  }
63
77
  }
64
78
 
@@ -151,6 +165,10 @@ export class Signal<T> {
151
165
  return typeof x === "object" && !!x && $SIGNAL in x
152
166
  }
153
167
 
168
+ static id(signal: Signal<any>) {
169
+ return signal.$id
170
+ }
171
+
154
172
  static subscribers(signal: Signal<any>) {
155
173
  return signal.$subs
156
174
  }
@@ -11,6 +11,8 @@ import {
11
11
  import type { Signal } from "./base.js"
12
12
  import type { SignalValues } from "./types.js"
13
13
  import { node } from "../globals.js"
14
+ import { $INLINE_FN } from "../constants.js"
15
+ import { KiruError } from "../error.js"
14
16
 
15
17
  type EffectCallbackReturn = (() => void) | void
16
18
 
@@ -37,6 +39,12 @@ export class Effect<const Deps extends readonly Signal<unknown>[] = []> {
37
39
  }
38
40
  const n = node.current
39
41
  if (n) {
42
+ if (__DEV__ && n.type === $INLINE_FN) {
43
+ throw new KiruError({
44
+ message: "Effects cannot be created inside inline functions",
45
+ vNode: n,
46
+ })
47
+ }
40
48
  if (!sideEffectsEnabled()) return // prevent side effects in non-browser environments
41
49
  registerVNodeCleanup(n, this.id, this.stop.bind(this))
42
50
  }
@@ -32,25 +32,19 @@ export function executeWithTracking<T, Deps extends readonly Signal<unknown>[]>(
32
32
  ctx: TrackedExecutionContext<T, Deps>
33
33
  ): T {
34
34
  const { id, subs, fn, deps = [], onDepChanged } = ctx
35
- let observations: Map<string, Signal<unknown>> | undefined
35
+ let observations: TrackingStackObservations | undefined
36
36
 
37
37
  effectQueue.delete(id)
38
- const isServer = !!node.current && !sideEffectsEnabled()
39
38
 
40
- if (!isServer) {
41
- observations = new Map<string, Signal<unknown>>()
39
+ // Prevent side effects in non-browser environments while rendering
40
+ if (!node.current || sideEffectsEnabled()) {
41
+ observations = new Map()
42
42
  tracking.stack.push(observations)
43
43
  }
44
44
 
45
45
  const result = fn(...(deps.map((s) => s.value) as SignalValues<Deps>))
46
46
 
47
- if (!isServer) {
48
- for (const [id, unsub] of subs) {
49
- if (observations!.has(id)) continue
50
- unsub()
51
- subs.delete(id)
52
- }
53
-
47
+ if (observations) {
54
48
  const effect = () => {
55
49
  if (!effectQueue.size) {
56
50
  queueMicrotask(tick)
@@ -58,11 +52,19 @@ export function executeWithTracking<T, Deps extends readonly Signal<unknown>[]>(
58
52
  effectQueue.set(id, onDepChanged)
59
53
  }
60
54
 
61
- for (const [id, sig] of observations!) {
62
- if (subs.has(id)) continue
63
- const unsub = sig.subscribe(effect)
64
- subs.set(id, unsub)
55
+ for (const [id, signal] of observations) {
56
+ if (!subs.has(id)) {
57
+ subs.set(id, signal.subscribe(effect))
58
+ }
65
59
  }
60
+
61
+ for (const [id, unsub] of subs) {
62
+ if (!observations.has(id)) {
63
+ unsub()
64
+ subs.delete(id)
65
+ }
66
+ }
67
+
66
68
  tracking.stack.pop()
67
69
  }
68
70
 
package/src/ssr/client.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AppHandle, AppHandleOptions } from "../appHandle"
1
+ import type { AppHandle, AppHandleOptions } from "../appHandle.js"
2
2
  import { hydrationStack } from "../hydration.js"
3
3
  import { hydrationMode, renderMode } from "../globals.js"
4
4
  import { mount } from "../index.js"