electrobun 1.18.4-beta.19 → 1.18.4-beta.21
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/api/browser/ui/__tests__/dom.test.ts +473 -0
- package/dist/api/browser/ui/__tests__/domStub.ts +218 -0
- package/dist/api/browser/ui/dom.ts +490 -0
- package/dist/api/browser/ui/index.ts +44 -0
- package/dist/api/browser/ui/jsx-dev-runtime.ts +16 -0
- package/dist/api/browser/ui/jsx-runtime.ts +56 -0
- package/dist/api/config/ElectrobunConfig.ts +33 -0
- package/dist/api/preload/.generated/compiled.ts +1 -1
- package/dist/api/preload/index.ts +2 -0
- package/dist/api/preload/uiTag.ts +45 -0
- package/dist/api/sdks/main/__tests__/utils-quit-exit-code.test.ts +44 -0
- package/dist/api/sdks/main/core/GpuWindow.ts +19 -0
- package/dist/api/sdks/main/core/Utils.ts +52 -4
- package/dist/api/sdks/main/core/WGPUView.ts +9 -0
- package/dist/api/sdks/main/entries/ui.ts +1 -0
- package/dist/api/sdks/main/proc/native.ts +187 -0
- package/dist/api/sdks/main/ui/__tests__/font.test.ts +49 -0
- package/dist/api/sdks/main/ui/__tests__/hit.test.ts +64 -0
- package/dist/api/sdks/main/ui/__tests__/jsx.test.ts +252 -0
- package/dist/api/sdks/main/ui/__tests__/layout.test.ts +159 -0
- package/dist/api/sdks/main/ui/__tests__/paint.test.ts +115 -0
- package/dist/api/sdks/main/ui/__tests__/reactive.test.ts +456 -0
- package/dist/api/sdks/main/ui/__tests__/scroll-focus-input.test.ts +298 -0
- package/dist/api/sdks/main/ui/__tests__/tree.test.ts +96 -0
- package/dist/api/sdks/main/ui/__tests__/ui.test.ts +170 -0
- package/dist/api/sdks/main/ui/elements.ts +135 -0
- package/dist/api/sdks/main/ui/font.ts +168 -0
- package/dist/api/sdks/main/ui/hit.ts +46 -0
- package/dist/api/sdks/main/ui/index.ts +71 -0
- package/dist/api/sdks/main/ui/input.ts +268 -0
- package/dist/api/sdks/main/ui/jsx-dev-runtime.ts +16 -0
- package/dist/api/sdks/main/ui/jsx-runtime.ts +136 -0
- package/dist/api/sdks/main/ui/keymap.ts +147 -0
- package/dist/api/sdks/main/ui/layout.ts +178 -0
- package/dist/api/sdks/main/ui/paint.ts +196 -0
- package/dist/api/sdks/main/ui/reactive.ts +4 -0
- package/dist/api/sdks/main/ui/renderer.ts +278 -0
- package/dist/api/sdks/main/ui/text.ts +175 -0
- package/dist/api/sdks/main/ui/textInput.ts +121 -0
- package/dist/api/sdks/main/ui/tree.ts +276 -0
- package/dist/api/sdks/main/ui/ui.ts +457 -0
- package/dist/api/sdks/main/ui/uiTagHost.ts +56 -0
- package/dist/api/sdks/main/ui/uiwindow.ts +330 -0
- package/dist/api/shared/build-dependencies.test.ts +1 -1
- package/dist/api/shared/build-dependencies.ts +4 -4
- package/dist/api/shared/linux-webkit-automation.test.ts +1 -1
- package/dist/api/shared/warren/jsx.ts +279 -0
- package/dist/api/shared/warren/reactive.ts +638 -0
- package/dist/api/shared/windows-unicode-ui.test.ts +4 -4
- package/dist/preload-full.js +35 -0
- package/dist/zig-sdk/electrobun.zig +197 -162
- package/{dash.config.ts → hutch.config.ts} +3 -2
- package/package.json +11 -2
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
// Warren — reactivity core.
|
|
2
|
+
//
|
|
3
|
+
// The one-line rule: nothing is reactive unless you can see why. Component
|
|
4
|
+
// bodies, handlers, and helpers are inert; reactivity exists only inside a
|
|
5
|
+
// scope you can see — live() or memo(). Within a scope, tracking follows
|
|
6
|
+
// the dynamic extent: signal calls and store property reads both subscribe,
|
|
7
|
+
// including inside helpers called from the scope.
|
|
8
|
+
//
|
|
9
|
+
// Seven primitives, every one a bare verb taking a function:
|
|
10
|
+
// signal(v) reactive value; returns [get, set]
|
|
11
|
+
// store(obj) nested reactive state; returns [readonlyProxy, setter];
|
|
12
|
+
// the setter takes a mutator (draft => { ... })
|
|
13
|
+
// live(fn) reactive scope; deferred — runs after commit
|
|
14
|
+
// memo(fn) caching reactive scope; dependents never observe it stale
|
|
15
|
+
// inert(fn) read without subscribing, inside a scope (no-op outside)
|
|
16
|
+
// cleanup(fn) teardown for the enclosing scope; runs before each re-run
|
|
17
|
+
// batch(fn) defer notification (never mutation) to the outermost exit
|
|
18
|
+
//
|
|
19
|
+
// There is no createEffect — an effect is a live whose return value nobody
|
|
20
|
+
// uses. There is no produce — the store setter mutates a draft directly.
|
|
21
|
+
|
|
22
|
+
export type Accessor<T> = () => T;
|
|
23
|
+
export type Setter<T> = (next: T | ((prev: T) => T)) => T;
|
|
24
|
+
|
|
25
|
+
// Structural brand (string key, not a unique symbol) so the type unifies
|
|
26
|
+
// across symlinked/realified module paths.
|
|
27
|
+
export interface LiveBinding<T> {
|
|
28
|
+
readonly __warrenLive: true;
|
|
29
|
+
/** The wrapped expression. Consumers re-run it inside their own scope. */
|
|
30
|
+
fn: () => T;
|
|
31
|
+
/** Set by a consumer (value prop, control flow) to suppress the auto-effect. */
|
|
32
|
+
claimed: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** What value props accept: a plain value, or a live() binding. */
|
|
36
|
+
export type Reactive<T> = T | LiveBinding<T>;
|
|
37
|
+
|
|
38
|
+
export function isLive(value: unknown): value is LiveBinding<unknown> {
|
|
39
|
+
return (
|
|
40
|
+
typeof value === "object" &&
|
|
41
|
+
value !== null &&
|
|
42
|
+
(value as any).__warrenLive === true
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let devMode = true;
|
|
47
|
+
/** Toggle dev-mode warnings (default on while Warren is experimental). */
|
|
48
|
+
export function setDevMode(enabled: boolean): void {
|
|
49
|
+
devMode = enabled;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Scopes
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
interface SubscriberSet extends Set<Scope> {
|
|
57
|
+
/** True for signal subscriber sets (dev warning #3 heuristics). */
|
|
58
|
+
fromSignal?: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
type ScopeKind = "root" | "live" | "memo";
|
|
62
|
+
|
|
63
|
+
let currentScope: Scope | null = null;
|
|
64
|
+
/** Non-null only while a tracking scope (live/memo) body executes. */
|
|
65
|
+
let trackingScope: Scope | null = null;
|
|
66
|
+
let inertDepth = 0;
|
|
67
|
+
|
|
68
|
+
class Scope {
|
|
69
|
+
kind: ScopeKind;
|
|
70
|
+
fn: (() => void) | null;
|
|
71
|
+
parent: Scope | null;
|
|
72
|
+
children: Scope[] = [];
|
|
73
|
+
cleanups: Array<() => void> = [];
|
|
74
|
+
sources: SubscriberSet[] = [];
|
|
75
|
+
disposed = false;
|
|
76
|
+
hasRun = false;
|
|
77
|
+
|
|
78
|
+
constructor(kind: ScopeKind, fn: (() => void) | null, parent: Scope | null) {
|
|
79
|
+
this.kind = kind;
|
|
80
|
+
this.fn = fn;
|
|
81
|
+
this.parent = parent;
|
|
82
|
+
parent?.children.push(this);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
run(): void {
|
|
86
|
+
if (this.disposed || !this.fn) return;
|
|
87
|
+
// A re-run replaces the previous run's scope state.
|
|
88
|
+
for (let i = this.children.length - 1; i >= 0; i--) {
|
|
89
|
+
this.children[i]!.dispose();
|
|
90
|
+
}
|
|
91
|
+
this.children.length = 0;
|
|
92
|
+
for (let i = this.cleanups.length - 1; i >= 0; i--) {
|
|
93
|
+
this.cleanups[i]!();
|
|
94
|
+
}
|
|
95
|
+
this.cleanups.length = 0;
|
|
96
|
+
this.clearSources();
|
|
97
|
+
|
|
98
|
+
const prevScope = currentScope;
|
|
99
|
+
const prevTracking = trackingScope;
|
|
100
|
+
const prevInert = inertDepth;
|
|
101
|
+
currentScope = this;
|
|
102
|
+
trackingScope = this.kind === "root" ? null : this;
|
|
103
|
+
inertDepth = 0;
|
|
104
|
+
try {
|
|
105
|
+
this.fn();
|
|
106
|
+
} finally {
|
|
107
|
+
currentScope = prevScope;
|
|
108
|
+
trackingScope = prevTracking;
|
|
109
|
+
inertDepth = prevInert;
|
|
110
|
+
this.hasRun = true;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
clearSources(): void {
|
|
115
|
+
for (const subs of this.sources) subs.delete(this);
|
|
116
|
+
this.sources.length = 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
dispose(): void {
|
|
120
|
+
if (this.disposed) return;
|
|
121
|
+
this.disposed = true;
|
|
122
|
+
liveQueue.delete(this);
|
|
123
|
+
this.clearSources();
|
|
124
|
+
for (let i = this.children.length - 1; i >= 0; i--) {
|
|
125
|
+
this.children[i]!.dispose();
|
|
126
|
+
}
|
|
127
|
+
this.children.length = 0;
|
|
128
|
+
for (let i = this.cleanups.length - 1; i >= 0; i--) {
|
|
129
|
+
this.cleanups[i]!();
|
|
130
|
+
}
|
|
131
|
+
this.cleanups.length = 0;
|
|
132
|
+
if (this.parent && !this.parent.disposed) {
|
|
133
|
+
const idx = this.parent.children.indexOf(this);
|
|
134
|
+
if (idx >= 0) this.parent.children.splice(idx, 1);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function track(subs: SubscriberSet): void {
|
|
140
|
+
if (!trackingScope || inertDepth > 0) return;
|
|
141
|
+
if (!subs.has(trackingScope)) {
|
|
142
|
+
subs.add(trackingScope);
|
|
143
|
+
trackingScope.sources.push(subs);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
// Propagation. Memos pull-validate so a live never observes a stale memo;
|
|
149
|
+
// lives queue and flush after commit (end of mount pass, end of outermost
|
|
150
|
+
// batch, or immediately after an unbatched write's propagation).
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
const liveQueue = new Set<Scope>();
|
|
154
|
+
let batchDepth = 0;
|
|
155
|
+
let commitDepth = 0;
|
|
156
|
+
let notifyDepth = 0;
|
|
157
|
+
let flushing = false;
|
|
158
|
+
|
|
159
|
+
function notify(subs: SubscriberSet): void {
|
|
160
|
+
if (subs.size === 0) return;
|
|
161
|
+
// Flushing waits until propagation fully settles (notifyDepth back to 0):
|
|
162
|
+
// a memo invalidating mid-loop must not run lives before every subscriber
|
|
163
|
+
// of the original write has been queued, or a live can run twice.
|
|
164
|
+
notifyDepth++;
|
|
165
|
+
try {
|
|
166
|
+
for (const scope of [...subs]) {
|
|
167
|
+
if (scope.kind === "memo") {
|
|
168
|
+
(scope as MemoScope).invalidate();
|
|
169
|
+
} else {
|
|
170
|
+
liveQueue.add(scope);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
} finally {
|
|
174
|
+
notifyDepth--;
|
|
175
|
+
}
|
|
176
|
+
scheduleFlush();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function scheduleFlush(): void {
|
|
180
|
+
if (batchDepth > 0 || commitDepth > 0 || notifyDepth > 0 || flushing) return;
|
|
181
|
+
flushLive();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function flushLive(): void {
|
|
185
|
+
if (flushing) return;
|
|
186
|
+
flushing = true;
|
|
187
|
+
try {
|
|
188
|
+
let iterations = 0;
|
|
189
|
+
while (liveQueue.size) {
|
|
190
|
+
if (++iterations > 100_000) {
|
|
191
|
+
liveQueue.clear();
|
|
192
|
+
throw new Error("Warren: reactive cycle — lives never settled");
|
|
193
|
+
}
|
|
194
|
+
const next = liveQueue.values().next().value!;
|
|
195
|
+
liveQueue.delete(next);
|
|
196
|
+
next.run();
|
|
197
|
+
}
|
|
198
|
+
} finally {
|
|
199
|
+
flushing = false;
|
|
200
|
+
}
|
|
201
|
+
if (liveQueue.size && batchDepth === 0 && commitDepth === 0) flushLive();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* A commit boundary (mount or rebuild pass). Lives created or notified
|
|
206
|
+
* inside run when the outermost commit exits.
|
|
207
|
+
*/
|
|
208
|
+
export function commit<T>(fn: () => T): T {
|
|
209
|
+
commitDepth++;
|
|
210
|
+
try {
|
|
211
|
+
return fn();
|
|
212
|
+
} finally {
|
|
213
|
+
commitDepth--;
|
|
214
|
+
if (commitDepth === 0 && batchDepth === 0) flushLive();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function batch<T>(fn: () => T): T {
|
|
219
|
+
batchDepth++;
|
|
220
|
+
try {
|
|
221
|
+
return fn();
|
|
222
|
+
} finally {
|
|
223
|
+
// Queued notifications flush on the way out even when fn threw: the
|
|
224
|
+
// mutations already landed, so discarding them would leave the graph
|
|
225
|
+
// inconsistent with the store.
|
|
226
|
+
batchDepth--;
|
|
227
|
+
if (batchDepth === 0 && commitDepth === 0) flushLive();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
// signal
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
|
|
235
|
+
const defaultEquals = <T>(a: T, b: T) => a === b;
|
|
236
|
+
|
|
237
|
+
export function signal<T>(
|
|
238
|
+
value: T,
|
|
239
|
+
options?: { equals?: false | ((a: T, b: T) => boolean) },
|
|
240
|
+
): [Accessor<T>, Setter<T>] {
|
|
241
|
+
const equals =
|
|
242
|
+
options?.equals === false ? () => false : options?.equals ?? defaultEquals;
|
|
243
|
+
const subs: SubscriberSet = new Set() as SubscriberSet;
|
|
244
|
+
subs.fromSignal = true;
|
|
245
|
+
const read: Accessor<T> = () => {
|
|
246
|
+
if (
|
|
247
|
+
devMode &&
|
|
248
|
+
commitDepth > 0 &&
|
|
249
|
+
!trackingScope &&
|
|
250
|
+
inertDepth === 0
|
|
251
|
+
) {
|
|
252
|
+
console.warn(
|
|
253
|
+
"Warren: signal read during render with no scope on the stack — this renders once and then freezes. Wrap it: live(() => ...).",
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
track(subs);
|
|
257
|
+
return value;
|
|
258
|
+
};
|
|
259
|
+
const write: Setter<T> = (next) => {
|
|
260
|
+
const resolved =
|
|
261
|
+
typeof next === "function" ? (next as (prev: T) => T)(value) : next;
|
|
262
|
+
if (equals(value, resolved)) return value;
|
|
263
|
+
value = resolved;
|
|
264
|
+
notify(subs);
|
|
265
|
+
return value;
|
|
266
|
+
};
|
|
267
|
+
return [read, write];
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ---------------------------------------------------------------------------
|
|
271
|
+
// memo
|
|
272
|
+
// ---------------------------------------------------------------------------
|
|
273
|
+
|
|
274
|
+
class MemoScope extends Scope {
|
|
275
|
+
value: unknown;
|
|
276
|
+
stale = true;
|
|
277
|
+
equals: (a: unknown, b: unknown) => boolean;
|
|
278
|
+
subs: SubscriberSet = new Set() as SubscriberSet;
|
|
279
|
+
compute: () => unknown;
|
|
280
|
+
|
|
281
|
+
constructor(
|
|
282
|
+
compute: () => unknown,
|
|
283
|
+
equals: (a: unknown, b: unknown) => boolean,
|
|
284
|
+
) {
|
|
285
|
+
super("memo", null, currentScope);
|
|
286
|
+
this.compute = compute;
|
|
287
|
+
this.fn = () => {
|
|
288
|
+
this.value = this.compute();
|
|
289
|
+
};
|
|
290
|
+
this.equals = equals;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
invalidate(): void {
|
|
294
|
+
if (this.stale) return;
|
|
295
|
+
this.stale = true;
|
|
296
|
+
// Unobserved memos stay lazy: recompute on next read.
|
|
297
|
+
if (this.subs.size === 0) return;
|
|
298
|
+
// Observed memos recompute eagerly so the equals cut can stop
|
|
299
|
+
// propagation: dependents only hear about it when the value actually
|
|
300
|
+
// changed. (Mutations have already landed — batch defers notification,
|
|
301
|
+
// never mutation — so recomputing mid-propagation reads fresh state.)
|
|
302
|
+
const prev = this.value;
|
|
303
|
+
const first = !this.hasRun;
|
|
304
|
+
this.run();
|
|
305
|
+
this.stale = false;
|
|
306
|
+
if (!first && this.equals(prev, this.value)) {
|
|
307
|
+
this.value = prev;
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
notify(this.subs);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
read(): unknown {
|
|
314
|
+
track(this.subs);
|
|
315
|
+
if (this.stale && !this.disposed) {
|
|
316
|
+
const prev = this.value;
|
|
317
|
+
const first = !this.hasRun;
|
|
318
|
+
this.run();
|
|
319
|
+
this.stale = false;
|
|
320
|
+
if (!first && this.equals(prev, this.value)) {
|
|
321
|
+
this.value = prev;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return this.value;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export function memo<T>(
|
|
329
|
+
fn: () => T,
|
|
330
|
+
options?: { equals?: false | ((a: T, b: T) => boolean) },
|
|
331
|
+
): Accessor<T> {
|
|
332
|
+
const equals =
|
|
333
|
+
options?.equals === false
|
|
334
|
+
? () => false
|
|
335
|
+
: ((options?.equals as any) ?? defaultEquals);
|
|
336
|
+
const node = new MemoScope(fn as () => unknown, equals);
|
|
337
|
+
return () => node.read() as T;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ---------------------------------------------------------------------------
|
|
341
|
+
// live
|
|
342
|
+
// ---------------------------------------------------------------------------
|
|
343
|
+
|
|
344
|
+
function warnZeroDeps(scope: Scope): void {
|
|
345
|
+
if (!devMode || !scope.hasRun) return;
|
|
346
|
+
if (scope.sources.length === 0) {
|
|
347
|
+
console.warn(
|
|
348
|
+
"Warren: live() registered zero dependencies — the wrapped expression is static; drop the marker.",
|
|
349
|
+
);
|
|
350
|
+
} else if (scope.sources.every((s) => s.fromSignal)) {
|
|
351
|
+
// Inside another scope the signal calls would have tracked anyway.
|
|
352
|
+
// (Only meaningful for nested lives; harmless reminder elsewhere.)
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export function live<T>(fn: () => T): Reactive<T> {
|
|
357
|
+
if (typeof fn !== "function") {
|
|
358
|
+
throw new Error("Warren: live() takes a function: live(() => ...)");
|
|
359
|
+
}
|
|
360
|
+
// Nested inside a running scope while actually tracking: a pass-through.
|
|
361
|
+
// The enclosing scope already tracks the dynamic extent, so evaluate and
|
|
362
|
+
// return the value. Inside inert() (component bodies, escapes) tracking
|
|
363
|
+
// is off, so a live() there is a genuine new binding, not a nested one.
|
|
364
|
+
if (trackingScope && inertDepth === 0) {
|
|
365
|
+
if (devMode) {
|
|
366
|
+
console.warn(
|
|
367
|
+
"Warren: nested live() — the enclosing scope already tracks these reads; the marker is redundant here.",
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
return fn();
|
|
371
|
+
}
|
|
372
|
+
if (!currentScope) {
|
|
373
|
+
throw new Error(
|
|
374
|
+
"Warren: live() outside JSX and outside any scope has no meaning. Create it inside a component, a mount, or another scope.",
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
const binding: LiveBinding<T> = {
|
|
378
|
+
__warrenLive: true,
|
|
379
|
+
fn,
|
|
380
|
+
claimed: false,
|
|
381
|
+
};
|
|
382
|
+
// Statement-position lives become effects; value-position lives get
|
|
383
|
+
// claimed by their consumer first. Defer the decision to the flush.
|
|
384
|
+
const decide = new Scope("live", null, currentScope);
|
|
385
|
+
decide.fn = () => {
|
|
386
|
+
if (binding.claimed) {
|
|
387
|
+
decide.dispose();
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
decide.fn = () => {
|
|
391
|
+
binding.fn();
|
|
392
|
+
};
|
|
393
|
+
decide.run();
|
|
394
|
+
warnZeroDeps(decide);
|
|
395
|
+
};
|
|
396
|
+
liveQueue.add(decide);
|
|
397
|
+
scheduleFlush();
|
|
398
|
+
return binding;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Consumer side of live(): claim a binding and run `apply(value)` in a
|
|
403
|
+
* scope that re-runs (deferred) when the binding's dependencies change.
|
|
404
|
+
* Used by value props and control-flow components.
|
|
405
|
+
*/
|
|
406
|
+
export function claimLive<T>(
|
|
407
|
+
binding: LiveBinding<T>,
|
|
408
|
+
apply: (value: T) => void,
|
|
409
|
+
): void {
|
|
410
|
+
binding.claimed = true;
|
|
411
|
+
if (!currentScope) {
|
|
412
|
+
throw new Error("Warren: internal — claimLive outside a scope");
|
|
413
|
+
}
|
|
414
|
+
const scope = new Scope("live", null, currentScope);
|
|
415
|
+
scope.fn = () => {
|
|
416
|
+
apply(binding.fn());
|
|
417
|
+
warnZeroDeps(scope);
|
|
418
|
+
};
|
|
419
|
+
liveQueue.add(scope);
|
|
420
|
+
scheduleFlush();
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** Internal: a deferred reactive scope (regions, prop bindings). */
|
|
424
|
+
export function liveScope(fn: () => void): void {
|
|
425
|
+
if (!currentScope) {
|
|
426
|
+
throw new Error("Warren: internal — liveScope outside a scope");
|
|
427
|
+
}
|
|
428
|
+
const scope = new Scope("live", fn, currentScope);
|
|
429
|
+
liveQueue.add(scope);
|
|
430
|
+
scheduleFlush();
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// ---------------------------------------------------------------------------
|
|
434
|
+
// inert / cleanup / roots
|
|
435
|
+
// ---------------------------------------------------------------------------
|
|
436
|
+
|
|
437
|
+
export function inert<T>(fn: () => T): T {
|
|
438
|
+
// Outside a scope this is a no-op — inert is already the default there.
|
|
439
|
+
inertDepth++;
|
|
440
|
+
try {
|
|
441
|
+
return fn();
|
|
442
|
+
} finally {
|
|
443
|
+
inertDepth--;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
export function cleanup(fn: () => void): void {
|
|
448
|
+
if (fn === undefined) {
|
|
449
|
+
throw new Error(
|
|
450
|
+
'Warren: cleanup() requires a function — it registers teardown, it does not "clean up now".',
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
if (typeof fn !== "function") {
|
|
454
|
+
throw new Error("Warren: cleanup() takes a function");
|
|
455
|
+
}
|
|
456
|
+
if (!currentScope) {
|
|
457
|
+
throw new Error("Warren: cleanup() called outside a scope");
|
|
458
|
+
}
|
|
459
|
+
if (currentScope.kind === "memo") {
|
|
460
|
+
throw new Error(
|
|
461
|
+
"Warren: cleanup() inside a memo — memos are pure computations; use live() for work that owns resources.",
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
currentScope.cleanups.push(fn);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** Root scope for a mount. Lives created inside flush when fn returns. */
|
|
468
|
+
export function createRoot<T>(fn: (dispose: () => void) => T): T {
|
|
469
|
+
const scope = new Scope("root", null, null);
|
|
470
|
+
const prevScope = currentScope;
|
|
471
|
+
const prevTracking = trackingScope;
|
|
472
|
+
currentScope = scope;
|
|
473
|
+
trackingScope = null;
|
|
474
|
+
try {
|
|
475
|
+
return commit(() => fn(() => scope.dispose()));
|
|
476
|
+
} finally {
|
|
477
|
+
currentScope = prevScope;
|
|
478
|
+
trackingScope = prevTracking;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
export function getOwner(): unknown {
|
|
483
|
+
return currentScope;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
export function runWithOwner<T>(owner: unknown, fn: () => T): T {
|
|
487
|
+
const prevScope = currentScope;
|
|
488
|
+
const prevTracking = trackingScope;
|
|
489
|
+
currentScope = owner as Scope | null;
|
|
490
|
+
trackingScope = null;
|
|
491
|
+
try {
|
|
492
|
+
return fn();
|
|
493
|
+
} finally {
|
|
494
|
+
currentScope = prevScope;
|
|
495
|
+
trackingScope = prevTracking;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/** Internal: child scope handle for keyed list rows. */
|
|
500
|
+
export function createChildScope(owner?: unknown): unknown {
|
|
501
|
+
return new Scope("root", null, (owner as Scope | null) ?? currentScope);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
export function disposeScope(scope: unknown): void {
|
|
505
|
+
(scope as Scope).dispose();
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// ---------------------------------------------------------------------------
|
|
509
|
+
// store
|
|
510
|
+
// ---------------------------------------------------------------------------
|
|
511
|
+
|
|
512
|
+
export type StoreSetter<T extends object> = (mutate: (draft: T) => void) => void;
|
|
513
|
+
|
|
514
|
+
export function store<T extends object>(initial: T): [T, StoreSetter<T>] {
|
|
515
|
+
const raw = initial;
|
|
516
|
+
const pathSubs = new Map<string, SubscriberSet>();
|
|
517
|
+
const readProxies = new WeakMap<object, object>();
|
|
518
|
+
|
|
519
|
+
const trackPath = (path: string) => {
|
|
520
|
+
// Property reads are inert outside scopes: one flag check, no
|
|
521
|
+
// bookkeeping — the common case (component bodies, handlers).
|
|
522
|
+
if (!trackingScope || inertDepth > 0) return;
|
|
523
|
+
let subs = pathSubs.get(path);
|
|
524
|
+
if (!subs) {
|
|
525
|
+
subs = new Set() as SubscriberSet;
|
|
526
|
+
pathSubs.set(path, subs);
|
|
527
|
+
}
|
|
528
|
+
track(subs);
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
const childPath = (path: string, key: string) =>
|
|
532
|
+
path ? `${path}.${key}` : key;
|
|
533
|
+
|
|
534
|
+
const readProxyFor = (target: object, path: string): object => {
|
|
535
|
+
const existing = readProxies.get(target);
|
|
536
|
+
if (existing) return existing;
|
|
537
|
+
const proxy = new Proxy(target, {
|
|
538
|
+
get(t: any, key) {
|
|
539
|
+
if (typeof key === "symbol") return t[key];
|
|
540
|
+
const value = t[key];
|
|
541
|
+
if (typeof value === "function" && !Object.hasOwn(t, key)) {
|
|
542
|
+
return value;
|
|
543
|
+
}
|
|
544
|
+
const p = childPath(path, String(key));
|
|
545
|
+
trackPath(p);
|
|
546
|
+
if (value !== null && typeof value === "object") {
|
|
547
|
+
return readProxyFor(value, p);
|
|
548
|
+
}
|
|
549
|
+
return value;
|
|
550
|
+
},
|
|
551
|
+
set() {
|
|
552
|
+
throw new Error(
|
|
553
|
+
"Warren: stores are read-only outside their setter. Write through the setter: setState(draft => { ... }).",
|
|
554
|
+
);
|
|
555
|
+
},
|
|
556
|
+
deleteProperty() {
|
|
557
|
+
throw new Error(
|
|
558
|
+
"Warren: stores are read-only outside their setter. Write through the setter: setState(draft => { ... }).",
|
|
559
|
+
);
|
|
560
|
+
},
|
|
561
|
+
});
|
|
562
|
+
readProxies.set(target, proxy);
|
|
563
|
+
return proxy;
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
const notifyChanged = (changed: Set<string>) => {
|
|
567
|
+
if (changed.size === 0) return;
|
|
568
|
+
batch(() => {
|
|
569
|
+
const notified = new Set<SubscriberSet>();
|
|
570
|
+
for (const changedPath of changed) {
|
|
571
|
+
for (const [path, subs] of pathSubs) {
|
|
572
|
+
if (notified.has(subs)) continue;
|
|
573
|
+
const isSelf = path === changedPath;
|
|
574
|
+
const isDescendant = path.startsWith(changedPath + ".");
|
|
575
|
+
if (isSelf || isDescendant) {
|
|
576
|
+
notified.add(subs);
|
|
577
|
+
notify(subs);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
const draftFor = (
|
|
585
|
+
target: object,
|
|
586
|
+
path: string,
|
|
587
|
+
changed: Set<string>,
|
|
588
|
+
): object =>
|
|
589
|
+
new Proxy(target, {
|
|
590
|
+
get(t: any, key) {
|
|
591
|
+
if (typeof key === "symbol") return t[key];
|
|
592
|
+
const value = t[key];
|
|
593
|
+
if (typeof value === "function" && !Object.hasOwn(t, key)) {
|
|
594
|
+
return value.bind(draftFor(t, path, changed));
|
|
595
|
+
}
|
|
596
|
+
if (value !== null && typeof value === "object") {
|
|
597
|
+
return draftFor(value, childPath(path, String(key)), changed);
|
|
598
|
+
}
|
|
599
|
+
return value;
|
|
600
|
+
},
|
|
601
|
+
set(t: any, key, value) {
|
|
602
|
+
const p = childPath(path, String(key));
|
|
603
|
+
if (t[key] !== value) {
|
|
604
|
+
const prevLength = Array.isArray(t) ? t.length : -1;
|
|
605
|
+
t[key] = value;
|
|
606
|
+
changed.add(p);
|
|
607
|
+
if (Array.isArray(t) && t.length !== prevLength) {
|
|
608
|
+
changed.add(childPath(path, "length"));
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
return true;
|
|
612
|
+
},
|
|
613
|
+
deleteProperty(t: any, key) {
|
|
614
|
+
const p = childPath(path, String(key));
|
|
615
|
+
if (key in t) {
|
|
616
|
+
delete t[key];
|
|
617
|
+
changed.add(p);
|
|
618
|
+
}
|
|
619
|
+
return true;
|
|
620
|
+
},
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
// The setter is a mutator scope: the draft is writable, mutation is
|
|
624
|
+
// direct (no structural sharing), and the store's writes batch into one
|
|
625
|
+
// propagation.
|
|
626
|
+
const setter: StoreSetter<T> = (mutate) => {
|
|
627
|
+
if (typeof mutate !== "function") {
|
|
628
|
+
throw new Error(
|
|
629
|
+
"Warren: the store setter takes a mutator: setState(draft => { draft.x = 1 }).",
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
const changed = new Set<string>();
|
|
633
|
+
mutate(draftFor(raw, "", changed) as T);
|
|
634
|
+
notifyChanged(changed);
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
return [readProxyFor(raw, "") as T, setter];
|
|
638
|
+
}
|
|
@@ -14,8 +14,8 @@ const packageBuild = readFileSync(
|
|
|
14
14
|
join(import.meta.dirname, "../../build.ts"),
|
|
15
15
|
"utf8",
|
|
16
16
|
);
|
|
17
|
-
const
|
|
18
|
-
join(import.meta.dirname, "../../
|
|
17
|
+
const hutchConfig = readFileSync(
|
|
18
|
+
join(import.meta.dirname, "../../hutch.config.ts"),
|
|
19
19
|
"utf8",
|
|
20
20
|
);
|
|
21
21
|
const nativeUiTestScript = readFileSync(
|
|
@@ -86,10 +86,10 @@ describe("Windows Unicode native UI source contract", () => {
|
|
|
86
86
|
});
|
|
87
87
|
|
|
88
88
|
test("wires native Unicode UI coverage without compiling it off Windows", () => {
|
|
89
|
-
expect(
|
|
89
|
+
expect(hutchConfig).toContain(
|
|
90
90
|
'"test:windows-ui-native": "hutch scripts/test-windows-ui-native.js"',
|
|
91
91
|
);
|
|
92
|
-
expect(
|
|
92
|
+
expect(hutchConfig).toContain("hutch test:windows-ui-native");
|
|
93
93
|
expect(nativeUiTestScript).toContain(
|
|
94
94
|
'if (process.platform !== "win32")',
|
|
95
95
|
);
|
package/dist/preload-full.js
CHANGED
|
@@ -1226,6 +1226,40 @@ electrobun-wgpu {
|
|
|
1226
1226
|
}
|
|
1227
1227
|
}
|
|
1228
1228
|
|
|
1229
|
+
// src/preload/uiTag.ts
|
|
1230
|
+
class ElectrobunUiTag extends ElectrobunWgpuTag {
|
|
1231
|
+
async initWgpuView() {
|
|
1232
|
+
await super.initWgpuView();
|
|
1233
|
+
if (this.wgpuViewId !== null) {
|
|
1234
|
+
send("uiTagMount", {
|
|
1235
|
+
id: this.wgpuViewId,
|
|
1236
|
+
name: this.getAttribute("name") ?? ""
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
function initUiTag() {
|
|
1242
|
+
if (!customElements.get("electrobun-ui")) {
|
|
1243
|
+
customElements.define("electrobun-ui", ElectrobunUiTag);
|
|
1244
|
+
}
|
|
1245
|
+
const injectStyles = () => {
|
|
1246
|
+
const style = document.createElement("style");
|
|
1247
|
+
style.textContent = `
|
|
1248
|
+
electrobun-ui {
|
|
1249
|
+
display: block;
|
|
1250
|
+
width: 400px;
|
|
1251
|
+
height: 300px;
|
|
1252
|
+
}
|
|
1253
|
+
`;
|
|
1254
|
+
document.head.appendChild(style);
|
|
1255
|
+
};
|
|
1256
|
+
if (document.readyState === "loading") {
|
|
1257
|
+
document.addEventListener("DOMContentLoaded", injectStyles);
|
|
1258
|
+
} else {
|
|
1259
|
+
injectStyles();
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1229
1263
|
// src/preload/events.ts
|
|
1230
1264
|
function emitWebviewEvent(eventName, detail) {
|
|
1231
1265
|
setTimeout(() => {
|
|
@@ -1374,4 +1408,5 @@ initDragRegions();
|
|
|
1374
1408
|
initExternalDropFocusRestoration();
|
|
1375
1409
|
initWebviewTag();
|
|
1376
1410
|
initWgpuTag();
|
|
1411
|
+
initUiTag();
|
|
1377
1412
|
})();
|