@solidrt/core 0.0.47 → 0.0.49

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/AGENTS.md CHANGED
@@ -94,7 +94,7 @@ tsconfig.json - the two load-bearing lines are jsx + jsxImportSource:
94
94
  ```
95
95
 
96
96
  Peer deps @solidjs/signals and @solidjs/universal must match (currently
97
- 2.0.0-beta.31); bun resolves them from peerDependencies.
97
+ 2.0.0-rc.0); bun resolves them from peerDependencies.
98
98
 
99
99
  ## Element model (the parts that are easy to get wrong)
100
100
 
@@ -200,8 +200,8 @@ Peer deps @solidjs/signals and @solidjs/universal must match (currently
200
200
  Also onResize, onLayout, onWindowFocus, onWindowBlur.
201
201
 
202
202
  - Device/GPU access via subpath imports: @solidrt/core/camera, /microphone,
203
- /speech, /gpu. Image flow: `decodeImage(bytes)` -> `createTexture(data,w,h)`
204
- -> `<texture src={id} />`.
203
+ /speech, /gpu. Image flow: `decodeImage(bytes)` ->
204
+ `createTexture(data,w,h)` -> `<texture src={id} />`.
205
205
 
206
206
  ## Minimal app, core primitives only (verified to render)
207
207
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidrt/core",
3
- "version": "0.0.47",
3
+ "version": "0.0.49",
4
4
  "license": "MIT",
5
5
  "author": "Antoine van Wel",
6
6
  "type": "module",
@@ -27,11 +27,11 @@
27
27
  "colord": "^2.9.3"
28
28
  },
29
29
  "devDependencies": {
30
- "@solidrt/flux-types": "0.0.47"
30
+ "@solidrt/flux-types": "0.0.49"
31
31
  },
32
32
  "peerDependencies": {
33
- "@solidjs/signals": "2.0.0-beta.31",
34
- "@solidjs/universal": "2.0.0-beta.31",
35
- "solid-js": "2.0.0-beta.31"
33
+ "@solidjs/signals": "2.0.0-rc.0",
34
+ "@solidjs/universal": "2.0.0-rc.0",
35
+ "solid-js": "2.0.0-rc.0"
36
36
  }
37
37
  }
package/src/index.ts CHANGED
@@ -84,6 +84,13 @@ export {
84
84
  } from "@solidjs/signals"
85
85
  export type { Accessor, Setter, Signal, Store, StoreSetter } from "@solidjs/signals"
86
86
 
87
+ // Owner and lifecycle (from @solidjs/signals). `createRoot` above is the fourth
88
+ // member of this group - it creates an owned scope, these reach the owner it
89
+ // established. Capture an owner to restore it across an async boundary
90
+ // (`runWithOwner(owner, ...)`), or pass null to detach.
91
+ export { getOwner, runWithOwner, createOwner, isDisposed } from "@solidjs/signals"
92
+ export type { Owner } from "@solidjs/signals"
93
+
87
94
  // Control flow, components, and context (from solid-js).
88
95
  export {
89
96
  For,
package/src/text-input.ts CHANGED
@@ -63,14 +63,19 @@ export type TextBuffer = {
63
63
  * source. Without one it holds the text itself. The selection is always
64
64
  * buffer-owned state and is clamped to the current text length on read, so an
65
65
  * external truncation of a controlled value cannot leave the caret dangling.
66
- * Every edit is clamped to `maxLength`.
66
+ * Every edit is clamped to `maxLength`. Mutations commit synchronously
67
+ * (flush), so consecutive edits in one task observe each other - required for
68
+ * burst input, where several handlers run with no microtask between them.
67
69
  */
68
70
  export function createTextBuffer(options: TextBufferOptions = {}): TextBuffer {
69
71
  let initial = options.defaultValue ?? ""
70
72
  let [internalValue, setInternalValue] = createSignal(initial)
73
+ // The caret starts at the end of the current text: for a controlled buffer
74
+ // that is the owner's value, which defaultValue does not reflect.
75
+ let initialCaret = (options.value?.() ?? initial).length
71
76
  let [selectionState, setSelectionState] = createSignal<Selection>({
72
- anchor: initial.length,
73
- focus: initial.length,
77
+ anchor: initialCaret,
78
+ focus: initialCaret,
74
79
  })
75
80
 
76
81
  let value = () => options.value?.() ?? internalValue()
@@ -89,7 +94,12 @@ export function createTextBuffer(options: TextBufferOptions = {}): TextBuffer {
89
94
 
90
95
  let setCaret = (offset: number) => setSelectionState({ anchor: offset, focus: offset })
91
96
 
92
- // Apply a text edit and place the caret, clamping to maxLength.
97
+ // Apply a text edit and place the caret, clamping to maxLength. The flush
98
+ // commits the writes (including a controlled owner's from onInput) before
99
+ // returning: edits must observe each other within one task, because event
100
+ // bursts can dispatch several handlers with no microtask between them
101
+ // (Android IME input arrives as backspace+commit bursts; see
102
+ // okf/backlog/event-burst-stale-signal-reads.md).
93
103
  let apply = (next: string, caret: number) => {
94
104
  let max = options.maxLength?.()
95
105
  if (max != null && next.length > max) next = next.slice(0, max)
@@ -97,6 +107,7 @@ export function createTextBuffer(options: TextBufferOptions = {}): TextBuffer {
97
107
  if (options.value?.() == null) setInternalValue(next)
98
108
  setCaret(caret)
99
109
  options.onInput?.(next)
110
+ flush()
100
111
  }
101
112
 
102
113
  return {
@@ -131,6 +142,7 @@ export function createTextBuffer(options: TextBufferOptions = {}): TextBuffer {
131
142
  // A non-extending left/right on a range collapses to the near edge.
132
143
  if (!extend && anchor !== focus && (direction === "left" || direction === "right")) {
133
144
  setCaret(direction === "left" ? Math.min(anchor, focus) : Math.max(anchor, focus))
145
+ flush()
134
146
  return
135
147
  }
136
148
  let next = focus
@@ -139,11 +151,13 @@ export function createTextBuffer(options: TextBufferOptions = {}): TextBuffer {
139
151
  else if (direction === "start") next = 0
140
152
  else if (direction === "end") next = len
141
153
  setSelectionState({ anchor: extend ? anchor : next, focus: next })
154
+ flush()
142
155
  },
143
156
 
144
157
  setSelection: (anchor, focus) => {
145
158
  let len = value().length
146
159
  setSelectionState({ anchor: Math.min(anchor, len), focus: Math.min(focus, len) })
160
+ flush()
147
161
  },
148
162
 
149
163
  setValue: (next) => apply(next, next.length),
package/src/video.ts ADDED
@@ -0,0 +1,100 @@
1
+ // Video playback, reactive (SolidJS) layer. There is no <video> element by
2
+ // design: a player streams into a texture id, so displaying it is just
3
+ // <texture src={video.texture()} /> (or <d-texture> for detached-layout
4
+ // placement), and every texture capability - shaders, offscreen targets,
5
+ // subtree effects - applies to video for free. A richer Video component
6
+ // composes on top of this in a higher layer, not in core.
7
+ //
8
+ // The imperative primitive lives in the `flux:video` module; import { open }
9
+ // from "flux:video" for non-reactive use.
10
+
11
+ import { createSignal, onCleanup } from "@solidjs/signals"
12
+ import type { TextureId } from "flux:gpu"
13
+ import { open } from "flux:video"
14
+
15
+ export type VideoOptions = {
16
+ /** Start playback as soon as the file is open. */
17
+ autoplay?: boolean
18
+ }
19
+
20
+ /** An opened video as reactive accessors plus playback controls. */
21
+ export type VideoStream = {
22
+ /** Texture id once the file is open, undefined while opening; render with <texture src={...}>. */
23
+ texture(): TextureId | undefined
24
+ /** Frame size, undefined while opening. */
25
+ width(): number | undefined
26
+ height(): number | undefined
27
+ /** Duration in seconds, undefined while opening. */
28
+ duration(): number | undefined
29
+ /** Set if opening failed (unreadable file, unsupported codec). */
30
+ error(): Error | undefined
31
+ /** Start or resume playback (before open resolves, playback starts on resolve). */
32
+ play(): void
33
+ /** Pause playback; the current frame stays displayed. */
34
+ pause(): void
35
+ /** Whether playback is running (plain read, not a signal). */
36
+ playing(): boolean
37
+ /** Presentation time of the displayed frame in seconds (plain read, not a signal). */
38
+ currentTime(): number
39
+ /** Whether the last frame has been displayed (plain read, not a signal). */
40
+ finished(): boolean
41
+ }
42
+
43
+ /**
44
+ * Opens a video file (MP4, H.264 + AAC) and exposes it as reactive signals:
45
+ * read texture() in JSX and the frames appear once playback starts. Closes
46
+ * automatically when the reactive owner is disposed (e.g. the component
47
+ * unmounts). For imperative use, call open() from "flux:video" directly.
48
+ */
49
+ export function createVideo(path: string, options: VideoOptions = {}): VideoStream {
50
+ let [texture, setTexture] = createSignal<TextureId | undefined>(undefined)
51
+ let [width, setWidth] = createSignal<number | undefined>(undefined)
52
+ let [height, setHeight] = createSignal<number | undefined>(undefined)
53
+ let [duration, setDuration] = createSignal<number | undefined>(undefined)
54
+ let [error, setError] = createSignal<Error | undefined>(undefined)
55
+ let player: Awaited<ReturnType<typeof open>> | undefined
56
+ let disposed = false
57
+ let wantPlay = options.autoplay ?? false
58
+
59
+ open(path)
60
+ .then((video) => {
61
+ if (disposed) {
62
+ video.close()
63
+ return
64
+ }
65
+ player = video
66
+ setTexture(video.texture)
67
+ setWidth(video.width)
68
+ setHeight(video.height)
69
+ setDuration(video.duration)
70
+ if (wantPlay) video.play()
71
+ })
72
+ .catch((e) => setError(e instanceof Error ? e : new Error(String(e))))
73
+
74
+ onCleanup(() => {
75
+ disposed = true
76
+ if (player) {
77
+ player.close()
78
+ player = undefined
79
+ }
80
+ })
81
+
82
+ return {
83
+ texture,
84
+ width,
85
+ height,
86
+ duration,
87
+ error,
88
+ play() {
89
+ wantPlay = true
90
+ player?.play()
91
+ },
92
+ pause() {
93
+ wantPlay = false
94
+ player?.pause()
95
+ },
96
+ playing: () => player?.playing() ?? false,
97
+ currentTime: () => player?.currentTime() ?? 0,
98
+ finished: () => player?.finished() ?? false,
99
+ }
100
+ }