@solidrt/core 0.0.54 → 0.0.56

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/src/window.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { createSignal, onCleanup, onSettled, flush } from "@solidjs/signals"
1
+ import { createSignal, getOwner, onCleanup, onSettled, runWithOwner, flush } from "@solidjs/signals"
2
2
  import { requestFrame, setPointerLock } from "flux:rendertree"
3
3
  import { renderFrame } from "srt:render"
4
4
  import { on, once } from "srt:events"
@@ -7,8 +7,8 @@ import { getEventHandler, focusedNode, setFocus, activateTextInput, setInterestR
7
7
  import { scanForOrphans, getNodePath } from "./renderer"
8
8
 
9
9
  /**
10
- * Leaves the current app, unconditionally: back to the launcher in a dev
11
- * client, quitting when standalone or at the launcher itself (on Android the
10
+ * Leaves the current app, unconditionally: back to the player in a dev
11
+ * client, quitting when standalone or at the player itself (on Android the
12
12
  * client backgrounds instead of dying). The default action of an unprevented
13
13
  * `back` event; call it directly to exit programmatically, e.g. after
14
14
  * intercepting back for an unsaved-changes dialog.
@@ -44,7 +44,10 @@ let refreshRate = 60
44
44
  * control, but measure their delays against the wall clock, not this paced
45
45
  * timeline. performance.now() is not on it either: it is real elapsed time
46
46
  * for measuring work; Date.now() is calendar time.
47
- * Returns a cleanup function; also auto-cleans within a reactive scope.
47
+ * Returns a cleanup function; also auto-cleans within an owned scope. Outside
48
+ * one - an effect's apply phase, an event handler - the returned cleanup is
49
+ * the only handle, so return it from the apply (`return onFrame(...)`) to run
50
+ * a loop only while a condition holds.
48
51
  */
49
52
  export function onFrame(fn: (tick: number, frame: number, rate: number) => void) {
50
53
  let frameId: number = null!
@@ -77,7 +80,7 @@ export function onFrame(fn: (tick: number, frame: number, rate: number) => void)
77
80
  cancelled = true
78
81
  animationFrames.delete(frameId)
79
82
  }
80
- onCleanup(cleanup)
83
+ if (getOwner()) onCleanup(cleanup)
81
84
  return cleanup
82
85
  }
83
86
 
@@ -101,7 +104,7 @@ interface ResizeEvent {
101
104
 
102
105
  export function onResize(fn: (data: ResizeEvent) => void) {
103
106
  let unsubscribe = on("resize", fn)
104
- onCleanup(unsubscribe)
107
+ if (getOwner()) onCleanup(unsubscribe)
105
108
  return unsubscribe
106
109
  }
107
110
 
@@ -110,9 +113,13 @@ export function onResize(fn: (data: ResizeEvent) => void) {
110
113
  // Singleton accessors over the same events as onResize / onWindowFocus. There
111
114
  // is one window, so these are bare accessors rather than a createX instance.
112
115
  // Lazily subscribed on first read (resize is sticky, so the first read sees the
113
- // current value); app-lifetime, so no onCleanup. `ownedWrite: true` on the
114
- // signals below is the sticky-replay-into-a-tracked-scope case explained on
115
- // the ensure* functions in environment.ts.
116
+ // current value); app-lifetime, so no onCleanup. Each is created under no
117
+ // owner (runWithOwner(null)): the first read is usually a component or a
118
+ // computation, and a sticky event replays its cached value synchronously
119
+ // inside on(), so creating the signal in the reader's scope would write it
120
+ // there too - a render-time write, which the guard rejects. Detached, the
121
+ // fact belongs to the app, not to whoever read it first (the reasoning
122
+ // in full is on the ensure* functions in environment.ts).
116
123
 
117
124
  let sizeAccessor: (() => { width: number; height: number }) | undefined
118
125
  let safeAreaAccessor: (() => SafeArea) | undefined
@@ -120,17 +127,19 @@ let displayScaleAccessor: (() => number) | undefined
120
127
 
121
128
  function ensureResizeState() {
122
129
  if (sizeAccessor) return
123
- let [size, setSize] = createSignal({ width: 0, height: 0 }, { ownedWrite: true })
124
- let [safe, setSafe] = createSignal<SafeArea>({ top: 0, left: 0, right: 0, bottom: 0 }, { ownedWrite: true })
125
- let [scale, setScale] = createSignal(1, { ownedWrite: true })
126
- on("resize", (e: ResizeEvent) => {
127
- setSize({ width: e.width, height: e.height })
128
- setSafe(e.safeArea)
129
- setScale(e.displayScale)
130
+ runWithOwner(null, () => {
131
+ let [size, setSize] = createSignal({ width: 0, height: 0 })
132
+ let [safe, setSafe] = createSignal<SafeArea>({ top: 0, left: 0, right: 0, bottom: 0 })
133
+ let [scale, setScale] = createSignal(1)
134
+ on("resize", (e: ResizeEvent) => {
135
+ setSize({ width: e.width, height: e.height })
136
+ setSafe(e.safeArea)
137
+ setScale(e.displayScale)
138
+ })
139
+ sizeAccessor = size
140
+ safeAreaAccessor = safe
141
+ displayScaleAccessor = scale
130
142
  })
131
- sizeAccessor = size
132
- safeAreaAccessor = safe
133
- displayScaleAccessor = scale
134
143
  }
135
144
 
136
145
  /** Current window size, as a reactive accessor. */
@@ -156,12 +165,14 @@ let focusedAccessor: (() => boolean) | undefined
156
165
  /** Whether the window currently has focus, as a reactive accessor. */
157
166
  export function windowFocused(): boolean {
158
167
  if (!focusedAccessor) {
159
- let [focused, setFocused] = createSignal(true)
160
- on("windowFocus", () => setFocused(true))
161
- on("windowBlur", () => setFocused(false))
162
- focusedAccessor = focused
168
+ runWithOwner(null, () => {
169
+ let [focused, setFocused] = createSignal(true)
170
+ on("windowFocus", () => setFocused(true))
171
+ on("windowBlur", () => setFocused(false))
172
+ focusedAccessor = focused
173
+ })
163
174
  }
164
- return focusedAccessor()
175
+ return focusedAccessor!()
165
176
  }
166
177
 
167
178
  /**
@@ -183,12 +194,14 @@ let pointerLockedAccessor: (() => boolean) | undefined
183
194
  /** Whether the pointer is currently locked (relative mouse mode), as a reactive accessor. */
184
195
  export function pointerLocked(): boolean {
185
196
  if (!pointerLockedAccessor) {
186
- let [locked, setLocked] = createSignal(false)
187
- // Sticky event: a subscriber after the lock still observes the state.
188
- on("pointerLock", ({ locked }: { locked: boolean }) => setLocked(locked))
189
- pointerLockedAccessor = locked
197
+ runWithOwner(null, () => {
198
+ let [locked, setLocked] = createSignal(false)
199
+ // Sticky event: a subscriber after the lock still observes the state.
200
+ on("pointerLock", ({ locked }: { locked: boolean }) => setLocked(locked))
201
+ pointerLockedAccessor = locked
202
+ })
190
203
  }
191
- return pointerLockedAccessor()
204
+ return pointerLockedAccessor!()
192
205
  }
193
206
 
194
207
  let keyboardHeightAccessor: (() => number) | undefined
@@ -201,11 +214,13 @@ let keyboardHeightAccessor: (() => number) | undefined
201
214
  */
202
215
  export function keyboardHeight(): number {
203
216
  if (!keyboardHeightAccessor) {
204
- let [height, setHeight] = createSignal(0)
205
- on("keyboardVisibility", ({ height: h }: { height: number }) => setHeight(h ?? 0))
206
- keyboardHeightAccessor = height
217
+ runWithOwner(null, () => {
218
+ let [height, setHeight] = createSignal(0)
219
+ on("keyboardVisibility", ({ height: h }: { height: number }) => setHeight(h ?? 0))
220
+ keyboardHeightAccessor = height
221
+ })
207
222
  }
208
- return keyboardHeightAccessor()
223
+ return keyboardHeightAccessor!()
209
224
  }
210
225
 
211
226
  // Post-layout handlers run in registration order from one bus subscription,
@@ -251,19 +266,19 @@ export function onLayout(fn: () => void) {
251
266
  let i = layoutHandlers.indexOf(fn)
252
267
  if (i >= 0) layoutHandlers.splice(i, 1)
253
268
  }
254
- onCleanup(unsubscribe)
269
+ if (getOwner()) onCleanup(unsubscribe)
255
270
  return unsubscribe
256
271
  }
257
272
 
258
273
  export function onWindowFocus(fn: () => void) {
259
274
  let unsubscribe = on("windowFocus", fn)
260
- onCleanup(unsubscribe)
275
+ if (getOwner()) onCleanup(unsubscribe)
261
276
  return unsubscribe
262
277
  }
263
278
 
264
279
  export function onWindowBlur(fn: () => void) {
265
280
  let unsubscribe = on("windowBlur", fn)
266
- onCleanup(unsubscribe)
281
+ if (getOwner()) onCleanup(unsubscribe)
267
282
  return unsubscribe
268
283
  }
269
284
 
@@ -295,7 +310,8 @@ let backHandlers: ((e: BackEvent) => void)[] = []
295
310
  * handler that does not prevent must not act either - the event is still on its
296
311
  * way to whoever will handle it.
297
312
  *
298
- * Returns a cleanup function; also auto-cleans within a reactive scope.
313
+ * Returns a cleanup function; also auto-cleans within an owned scope (outside
314
+ * one the returned cleanup is the only handle).
299
315
  */
300
316
  export function onBack(fn: (e: BackEvent) => void) {
301
317
  backHandlers.push(fn)
@@ -303,7 +319,7 @@ export function onBack(fn: (e: BackEvent) => void) {
303
319
  let i = backHandlers.lastIndexOf(fn)
304
320
  if (i >= 0) backHandlers.splice(i, 1)
305
321
  }
306
- onCleanup(cleanup)
322
+ if (getOwner()) onCleanup(cleanup)
307
323
  return cleanup
308
324
  }
309
325