@blender-ui/native 0.0.0-stage → 0.2.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.
- package/LICENSE +674 -0
- package/README.md +125 -2
- package/blenderui.win32-x64.node +0 -0
- package/bui-runtime.win32-x64.exe +0 -0
- package/cli.js +138 -0
- package/index.d.ts +187 -0
- package/index.js +553 -0
- package/package.json +65 -4
- package/signals.js +628 -0
package/signals.js
ADDED
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
// Signals: BUI's hooks for JavaScript (the Rust crate's Signal / Memo /
|
|
2
|
+
// Effect, bindings/rust/blenderui/src/hooks.rs).
|
|
3
|
+
//
|
|
4
|
+
// const count = signal(0)
|
|
5
|
+
// const doubled = memo(() => count.value * 2) // recomputed when count changes
|
|
6
|
+
// effect(() => console.log(doubled.value)) // re-runs when doubled changes
|
|
7
|
+
// label.textContent = doubled // bound: only this text node updates
|
|
8
|
+
// count.value++
|
|
9
|
+
//
|
|
10
|
+
// Bound to an element (`el.setAttribute('class', sig)`, `el.style = sig`,
|
|
11
|
+
// `text.data = sig`, or a signal as a React prop or child), a write updates
|
|
12
|
+
// that one node: no component re-renders, no tree rebuilt, almost no garbage.
|
|
13
|
+
//
|
|
14
|
+
// Push-pull, glitch-free: a write marks direct readers dirty and indirect
|
|
15
|
+
// ones "check"; memos recompute lazily and stop propagation when their value
|
|
16
|
+
// is unchanged; effects run once per batch, after their sources settle. A
|
|
17
|
+
// memo nobody observes holds no subscriptions (it is garbage once dropped)
|
|
18
|
+
// and revalidates on read.
|
|
19
|
+
//
|
|
20
|
+
// Transitions (`startTransition`) mark the effects their writes trigger as
|
|
21
|
+
// low priority: they run after urgent work, in 5 ms slices.
|
|
22
|
+
//
|
|
23
|
+
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
24
|
+
'use strict'
|
|
25
|
+
|
|
26
|
+
const CLEAN = 0, CHECK = 1, DIRTY = 2
|
|
27
|
+
const NONE = 0, URGENT = 1, DEFERRED = 2
|
|
28
|
+
|
|
29
|
+
let observer = null // the memo / effect running now (collects what it reads)
|
|
30
|
+
let owner = null // the effect running now (owns effects created inside it)
|
|
31
|
+
let batchDepth = 0
|
|
32
|
+
let flushing = false
|
|
33
|
+
let writes = 0 // bumped by every signal write: unobserved memos revalidate against it
|
|
34
|
+
const pending = []
|
|
35
|
+
|
|
36
|
+
let transition = null // the transition whose writes are being made now
|
|
37
|
+
const deferred = [] // effects and tasks of transitions, run in slices
|
|
38
|
+
let deferredAt = 0
|
|
39
|
+
let deferredScheduled = false
|
|
40
|
+
const waiting = new Set() // transitions waiting for the deferred queue
|
|
41
|
+
|
|
42
|
+
function track (source) {
|
|
43
|
+
if (observer !== null && !observer._deps.has(source)) {
|
|
44
|
+
observer._deps.add(source)
|
|
45
|
+
if (observer._attached) source._addSub(observer)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** A value that re-runs what reads it when it changes (Rust's `Signal`). */
|
|
50
|
+
class Signal {
|
|
51
|
+
constructor (value, options) {
|
|
52
|
+
this._value = value
|
|
53
|
+
this._subs = new Set()
|
|
54
|
+
this._equals = options?.equals ?? Object.is
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Read (and subscribe the running memo / effect). */
|
|
58
|
+
get value () { track(this); return this._value }
|
|
59
|
+
set value (v) { this.set(v) }
|
|
60
|
+
get () { return this.value }
|
|
61
|
+
/** Read without subscribing. */
|
|
62
|
+
peek () { return this._value }
|
|
63
|
+
|
|
64
|
+
set (v) {
|
|
65
|
+
if (this._equals(this._value, v)) return
|
|
66
|
+
this._value = v
|
|
67
|
+
writes++
|
|
68
|
+
for (const s of this._subs) s._mark(DIRTY)
|
|
69
|
+
flush()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** `count.update(n => n + 1)` */
|
|
73
|
+
update (fn) { this.set(fn(this._value)) }
|
|
74
|
+
/** Rust's `with`: `fn(value)`, subscribing. */
|
|
75
|
+
with (fn) { return fn(this.value) }
|
|
76
|
+
/** Call `fn(value)` now and on every change; returns the unsubscribe function. */
|
|
77
|
+
subscribe (fn) { return effect(() => { fn(this.value) }) }
|
|
78
|
+
toString () { return String(this.value) }
|
|
79
|
+
/** `count + 1`, `count > 5` read the value (`===` and truthiness don't: use `.value`). */
|
|
80
|
+
valueOf () { return this.value }
|
|
81
|
+
toJSON () { return this.value }
|
|
82
|
+
|
|
83
|
+
_addSub (s) { this._subs.add(s) }
|
|
84
|
+
_removeSub (s) { this._subs.delete(s) }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
class Computation {
|
|
88
|
+
constructor (fn) {
|
|
89
|
+
this._fn = fn
|
|
90
|
+
this._deps = new Set()
|
|
91
|
+
this._state = DIRTY
|
|
92
|
+
this._attached = false
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
_unsubscribe () {
|
|
96
|
+
if (this._attached) for (const d of this._deps) d._removeSub(this)
|
|
97
|
+
this._deps.clear()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
_track () {
|
|
101
|
+
this._unsubscribe()
|
|
102
|
+
const prev = observer
|
|
103
|
+
observer = this
|
|
104
|
+
try {
|
|
105
|
+
return this._fn()
|
|
106
|
+
} finally {
|
|
107
|
+
observer = prev
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Bring up to date: re-run only if a source really changed. */
|
|
112
|
+
_refresh () {
|
|
113
|
+
if (this._state === CHECK) {
|
|
114
|
+
for (const d of this._deps) {
|
|
115
|
+
if (d instanceof Memo) {
|
|
116
|
+
d._refresh()
|
|
117
|
+
if (this._state === DIRTY) break
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (this._state === DIRTY) {
|
|
122
|
+
this._state = CLEAN // a mark made while running (a write to a source) re-queues it
|
|
123
|
+
this._run()
|
|
124
|
+
} else {
|
|
125
|
+
this._state = CLEAN
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** A derived value, recomputed only when what it read changed (Rust's `Memo`). */
|
|
131
|
+
class Memo extends Computation {
|
|
132
|
+
constructor (fn, options) {
|
|
133
|
+
super(fn)
|
|
134
|
+
this._subs = new Set()
|
|
135
|
+
this._value = undefined
|
|
136
|
+
this._init = false
|
|
137
|
+
this._seen = -1 // `writes` when last known up to date while detached
|
|
138
|
+
this._error = null // { error } when the computation threw: rethrown on read
|
|
139
|
+
this._lowPriority = false // marked by a transition only
|
|
140
|
+
this._equals = options?.equals ?? Object.is
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
get value () {
|
|
144
|
+
track(this)
|
|
145
|
+
this._refresh()
|
|
146
|
+
if (this._error !== null) throw this._error.error
|
|
147
|
+
return this._value
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
get () { return this.value }
|
|
151
|
+
peek () {
|
|
152
|
+
this._refresh()
|
|
153
|
+
if (this._error !== null) throw this._error.error
|
|
154
|
+
return this._value
|
|
155
|
+
}
|
|
156
|
+
with (fn) { return fn(this.value) }
|
|
157
|
+
subscribe (fn) { return effect(() => { fn(this.value) }) }
|
|
158
|
+
toString () { return String(this.value) }
|
|
159
|
+
/** `count + 1`, `count > 5` read the value (`===` and truthiness don't: use `.value`). */
|
|
160
|
+
valueOf () { return this.value }
|
|
161
|
+
toJSON () { return this.value }
|
|
162
|
+
|
|
163
|
+
_refresh () {
|
|
164
|
+
if (this._attached) return super._refresh()
|
|
165
|
+
// nobody observes this memo: revalidate against the global write count
|
|
166
|
+
const at = writes
|
|
167
|
+
this._state = CLEAN
|
|
168
|
+
if (this._seen !== at || !this._init) this._run()
|
|
169
|
+
this._seen = at
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
_run () {
|
|
173
|
+
let v
|
|
174
|
+
try {
|
|
175
|
+
v = this._track()
|
|
176
|
+
} catch (error) {
|
|
177
|
+
this._init = true
|
|
178
|
+
this._error = { error }
|
|
179
|
+
for (const s of this._subs) if (s._state === CHECK) s._state = DIRTY // readers waiting on this check
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
if (!this._init || this._error !== null || !this._equals(this._value, v)) {
|
|
183
|
+
this._init = true
|
|
184
|
+
this._error = null
|
|
185
|
+
this._value = v
|
|
186
|
+
for (const s of this._subs) if (s._state === CHECK) s._state = DIRTY // readers waiting on this check
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
_mark (state) {
|
|
191
|
+
const urgent = transition === null
|
|
192
|
+
const was = this._state
|
|
193
|
+
// already marked: pass it on only to make a transition's mark urgent
|
|
194
|
+
if (was >= state && !(urgent && this._lowPriority)) return
|
|
195
|
+
if (state > was) this._state = state
|
|
196
|
+
const propagate = was === CLEAN || (urgent && this._lowPriority)
|
|
197
|
+
this._lowPriority = !urgent && (was === CLEAN || this._lowPriority)
|
|
198
|
+
if (propagate) for (const s of this._subs) s._mark(CHECK)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
_addSub (s) {
|
|
202
|
+
this._subs.add(s)
|
|
203
|
+
if (!this._attached) {
|
|
204
|
+
// first observer: subscribe to the sources (stale if anything was written since)
|
|
205
|
+
this._attached = true
|
|
206
|
+
if (this._seen !== writes) this._state = DIRTY
|
|
207
|
+
for (const d of this._deps) d._addSub(this)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
_removeSub (s) {
|
|
212
|
+
this._subs.delete(s)
|
|
213
|
+
if (this._subs.size === 0 && this._attached) {
|
|
214
|
+
// last observer gone: let go of the sources, so this memo can be collected
|
|
215
|
+
for (const d of this._deps) d._removeSub(this)
|
|
216
|
+
this._attached = false
|
|
217
|
+
this._seen = this._state === CLEAN ? writes : -1
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Runs `fn` now and again when what it read changes (Rust's `Effect`). */
|
|
223
|
+
class Effect extends Computation {
|
|
224
|
+
constructor (fn) {
|
|
225
|
+
super(fn)
|
|
226
|
+
this._attached = true
|
|
227
|
+
this._cleanup = undefined
|
|
228
|
+
this._children = null
|
|
229
|
+
this._disposed = false
|
|
230
|
+
this._queued = NONE
|
|
231
|
+
if (owner !== null) (owner._children ??= []).push(this)
|
|
232
|
+
this._state = CLEAN
|
|
233
|
+
batchDepth++ // writes made by this run re-run it after, not inside, the run
|
|
234
|
+
try {
|
|
235
|
+
this._run()
|
|
236
|
+
} catch (error) {
|
|
237
|
+
this.dispose() // no half-subscribed effect left behind
|
|
238
|
+
throw error
|
|
239
|
+
} finally {
|
|
240
|
+
batchDepth--
|
|
241
|
+
flush()
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
_run () {
|
|
246
|
+
this._clean()
|
|
247
|
+
const prevOwner = owner
|
|
248
|
+
owner = this
|
|
249
|
+
try {
|
|
250
|
+
const r = this._track()
|
|
251
|
+
if (typeof r === 'function') this._cleanup = r
|
|
252
|
+
} finally {
|
|
253
|
+
owner = prevOwner
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
_clean () {
|
|
258
|
+
if (this._children !== null) {
|
|
259
|
+
for (const c of this._children) c.dispose()
|
|
260
|
+
this._children = null
|
|
261
|
+
}
|
|
262
|
+
if (this._cleanup !== undefined) {
|
|
263
|
+
const c = this._cleanup
|
|
264
|
+
this._cleanup = undefined
|
|
265
|
+
untrack(c)
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
_mark (state) {
|
|
270
|
+
if (state > this._state) this._state = state
|
|
271
|
+
if (transition === null) {
|
|
272
|
+
if (this._queued !== URGENT) {
|
|
273
|
+
this._queued = URGENT
|
|
274
|
+
pending.push(this)
|
|
275
|
+
}
|
|
276
|
+
} else if (this._queued === NONE) {
|
|
277
|
+
this._queued = DEFERRED
|
|
278
|
+
defer(this)
|
|
279
|
+
} else if (this._queued === DEFERRED) {
|
|
280
|
+
holdTransition() // this transition also waits for the deferred run
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
dispose () {
|
|
285
|
+
if (this._disposed) return
|
|
286
|
+
this._disposed = true
|
|
287
|
+
this._unsubscribe()
|
|
288
|
+
this._attached = false // reads after dispose (mid-run) don't subscribe
|
|
289
|
+
this._clean()
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function runEffect (e) {
|
|
294
|
+
e._queued = NONE
|
|
295
|
+
if (e._disposed) return
|
|
296
|
+
try {
|
|
297
|
+
e._refresh()
|
|
298
|
+
} catch (err) {
|
|
299
|
+
e._state = CLEAN
|
|
300
|
+
reportError(err)
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function flush () {
|
|
305
|
+
if (batchDepth > 0 || flushing) return
|
|
306
|
+
flushing = true
|
|
307
|
+
let i = 0
|
|
308
|
+
try {
|
|
309
|
+
for (; i < pending.length; i++) {
|
|
310
|
+
if (i > 100000) throw new Error('blenderui: effects keep re-triggering each other')
|
|
311
|
+
if (pending[i]._queued === URGENT) runEffect(pending[i])
|
|
312
|
+
}
|
|
313
|
+
} finally {
|
|
314
|
+
for (; i < pending.length; i++) if (pending[i]._queued === URGENT) pending[i]._queued = NONE
|
|
315
|
+
pending.length = 0
|
|
316
|
+
flushing = false
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function reportError (err) {
|
|
321
|
+
// like a browser: one effect's error doesn't stop the others
|
|
322
|
+
queueMicrotask(() => { throw err })
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ---- transitions -----------------------------------------------------------
|
|
326
|
+
|
|
327
|
+
class Transition {
|
|
328
|
+
constructor () {
|
|
329
|
+
this._open = 1 // the synchronous part
|
|
330
|
+
this._done = []
|
|
331
|
+
this._queued = false
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
_hold () { this._open++ }
|
|
335
|
+
_release () {
|
|
336
|
+
if (--this._open === 0) for (const f of this._done.splice(0)) f()
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const now = typeof performance !== 'undefined' ? () => performance.now() : () => Date.now()
|
|
341
|
+
const macrotask = typeof setImmediate === 'function' ? setImmediate : (f) => setTimeout(f, 0)
|
|
342
|
+
|
|
343
|
+
/** The current transition waits for the deferred queue to drain. */
|
|
344
|
+
function holdTransition () {
|
|
345
|
+
if (transition !== null && !transition._queued) {
|
|
346
|
+
transition._queued = true
|
|
347
|
+
transition._hold()
|
|
348
|
+
waiting.add(transition)
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** Queue an effect or task of the current transition. */
|
|
353
|
+
function defer (item) {
|
|
354
|
+
deferred.push(item)
|
|
355
|
+
holdTransition()
|
|
356
|
+
if (!deferredScheduled) {
|
|
357
|
+
deferredScheduled = true
|
|
358
|
+
macrotask(runDeferred)
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function runDeferred () {
|
|
363
|
+
deferredScheduled = false
|
|
364
|
+
const end = now() + 5
|
|
365
|
+
while (deferredAt < deferred.length) {
|
|
366
|
+
const item = deferred[deferredAt++]
|
|
367
|
+
const t = item._transition
|
|
368
|
+
const prev = transition
|
|
369
|
+
transition = t ?? IDLE
|
|
370
|
+
batchDepth++
|
|
371
|
+
try {
|
|
372
|
+
if (typeof item === 'function') {
|
|
373
|
+
try { item() } catch (err) { reportError(err) }
|
|
374
|
+
} else if (item._queued === DEFERRED) {
|
|
375
|
+
runEffect(item)
|
|
376
|
+
}
|
|
377
|
+
} finally {
|
|
378
|
+
batchDepth--
|
|
379
|
+
transition = prev
|
|
380
|
+
}
|
|
381
|
+
flush()
|
|
382
|
+
if (now() >= end && deferredAt < deferred.length) {
|
|
383
|
+
// yield to urgent work (input, frames), then carry on
|
|
384
|
+
deferredScheduled = true
|
|
385
|
+
macrotask(runDeferred)
|
|
386
|
+
return
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
deferred.length = 0
|
|
390
|
+
deferredAt = 0
|
|
391
|
+
const done = [...waiting]
|
|
392
|
+
waiting.clear()
|
|
393
|
+
for (const t of done) {
|
|
394
|
+
t._queued = false
|
|
395
|
+
t._release()
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const IDLE = new Transition()
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Run `fn` as a transition: what its signal writes trigger (bindings,
|
|
403
|
+
* effects) runs after urgent work, in slices. Like React, only writes made
|
|
404
|
+
* before `fn`'s first `await` belong to it. Returns a promise that settles
|
|
405
|
+
* when `fn` (and its promise, if any) and the deferred work are done; it
|
|
406
|
+
* rejects with the error of an async `fn` (a synchronous throw propagates).
|
|
407
|
+
*/
|
|
408
|
+
function startTransition (fn) {
|
|
409
|
+
const t = new Transition()
|
|
410
|
+
let failure = null
|
|
411
|
+
const done = new Promise((resolve, reject) => {
|
|
412
|
+
t._done.push(() => (failure ? reject(failure.error) : resolve()))
|
|
413
|
+
})
|
|
414
|
+
const prev = transition
|
|
415
|
+
transition = t
|
|
416
|
+
batchDepth++
|
|
417
|
+
let result
|
|
418
|
+
try {
|
|
419
|
+
result = fn()
|
|
420
|
+
} catch (error) {
|
|
421
|
+
t._release()
|
|
422
|
+
throw error
|
|
423
|
+
} finally {
|
|
424
|
+
batchDepth--
|
|
425
|
+
transition = prev
|
|
426
|
+
flush()
|
|
427
|
+
}
|
|
428
|
+
if (result && typeof result.then === 'function') {
|
|
429
|
+
t._hold()
|
|
430
|
+
result.then(() => t._release(), (error) => { failure = { error }; t._release() })
|
|
431
|
+
}
|
|
432
|
+
t._release()
|
|
433
|
+
return done
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** Inside a transition: call `cb` when it completes. False outside one. */
|
|
437
|
+
function onTransitionDone (cb) {
|
|
438
|
+
if (transition === null || transition === IDLE) return false
|
|
439
|
+
transition._done.push(cb) // the transition is open while its body runs
|
|
440
|
+
return true
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** Run `fn` at transition priority, later (deferred values). */
|
|
444
|
+
function scheduleDeferred (fn) {
|
|
445
|
+
fn._transition = IDLE
|
|
446
|
+
const prev = transition
|
|
447
|
+
transition = IDLE
|
|
448
|
+
try {
|
|
449
|
+
defer(fn)
|
|
450
|
+
} finally {
|
|
451
|
+
transition = prev
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/** Run `fn` with urgent priority, even inside a transition. */
|
|
456
|
+
function urgent (fn) {
|
|
457
|
+
const prev = transition
|
|
458
|
+
transition = null
|
|
459
|
+
try {
|
|
460
|
+
return fn()
|
|
461
|
+
} finally {
|
|
462
|
+
transition = prev
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const inTransition = () => transition !== null
|
|
467
|
+
|
|
468
|
+
// ---- render tracking (components) ------------------------------------------
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* What a component's render read, and a callback when any of it changes.
|
|
472
|
+
* `run(fn)` collects reads without subscribing (a render may be thrown
|
|
473
|
+
* away); `commit(reads)` subscribes once the render is committed; `pause`
|
|
474
|
+
* / `resume` follow mount and unmount. Built for React components.
|
|
475
|
+
*/
|
|
476
|
+
class Tracker extends Computation {
|
|
477
|
+
constructor (onChange) {
|
|
478
|
+
super(null)
|
|
479
|
+
this._attached = true
|
|
480
|
+
this._state = CLEAN
|
|
481
|
+
this._queued = NONE
|
|
482
|
+
this._disposed = false
|
|
483
|
+
this._onChange = onChange
|
|
484
|
+
this._pausedAt = 0
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/** Run `fn`, collecting the signals it reads. Returns `[result, reads]`. */
|
|
488
|
+
run (fn) {
|
|
489
|
+
const reads = { _deps: new Set(), _attached: false, _render: true, _at: 0 }
|
|
490
|
+
const prev = observer
|
|
491
|
+
observer = reads
|
|
492
|
+
try {
|
|
493
|
+
const result = fn()
|
|
494
|
+
reads._at = writes // writes made by the render itself don't count
|
|
495
|
+
return [result, reads]
|
|
496
|
+
} finally {
|
|
497
|
+
observer = prev
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/** The render that read `reads` was committed: follow those signals now. */
|
|
502
|
+
commit (reads) {
|
|
503
|
+
if (reads === null) return
|
|
504
|
+
const next = reads._deps
|
|
505
|
+
if (this._attached) {
|
|
506
|
+
for (const d of this._deps) if (!next.has(d)) d._removeSub(this)
|
|
507
|
+
for (const d of next) if (!this._deps.has(d)) d._addSub(this)
|
|
508
|
+
}
|
|
509
|
+
this._deps = next
|
|
510
|
+
this._state = CLEAN
|
|
511
|
+
if (writes !== reads._at && next.size > 0) this._onChange() // changed in between
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
pause () {
|
|
515
|
+
if (!this._attached) return
|
|
516
|
+
for (const d of this._deps) d._removeSub(this)
|
|
517
|
+
this._attached = false
|
|
518
|
+
this._pausedAt = writes
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
resume () {
|
|
522
|
+
if (this._attached) return
|
|
523
|
+
this._attached = true
|
|
524
|
+
for (const d of this._deps) d._addSub(this)
|
|
525
|
+
if (writes !== this._pausedAt && this._deps.size > 0) this._onChange()
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
_run () { this._onChange() }
|
|
529
|
+
dispose () { this.pause(); this._disposed = true }
|
|
530
|
+
}
|
|
531
|
+
Tracker.prototype._mark = Effect.prototype._mark
|
|
532
|
+
|
|
533
|
+
const inRender = () => observer !== null && observer._render === true
|
|
534
|
+
|
|
535
|
+
/** A ref whose `.current` is a signal for effects and memos, but not for
|
|
536
|
+
* renders (reading a ref while rendering doesn't subscribe, as in React). */
|
|
537
|
+
class Ref extends Signal {
|
|
538
|
+
constructor (value) {
|
|
539
|
+
super(value)
|
|
540
|
+
// an own property: React (dev) rejects ref objects without one
|
|
541
|
+
Object.defineProperty(this, 'current', {
|
|
542
|
+
get () { return inRender() ? this._value : this.value },
|
|
543
|
+
set (v) { this.set(v) },
|
|
544
|
+
enumerable: true,
|
|
545
|
+
})
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// ---- API -------------------------------------------------------------------
|
|
550
|
+
|
|
551
|
+
/** `signal(0)`: a read-write signal (`.value`, `.set`, `.update`). */
|
|
552
|
+
function signal (value, options) { return new Signal(value, options) }
|
|
553
|
+
|
|
554
|
+
/** `memo(() => a.value + b.value)`: a lazily derived, cached signal. */
|
|
555
|
+
function memo (fn, options) { return new Memo(fn, options) }
|
|
556
|
+
|
|
557
|
+
/** `effect(() => ...)`: runs now and on changes; `fn` may return a cleanup. Returns `dispose`. */
|
|
558
|
+
function effect (fn) {
|
|
559
|
+
const e = new Effect(fn)
|
|
560
|
+
return () => e.dispose()
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/** Run `fn`, deferring effects until it returns (several writes, one update). */
|
|
564
|
+
function batch (fn) {
|
|
565
|
+
batchDepth++
|
|
566
|
+
try {
|
|
567
|
+
return fn()
|
|
568
|
+
} finally {
|
|
569
|
+
batchDepth--
|
|
570
|
+
flush()
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/** Run `fn` outside any owning effect: effects it creates live until disposed. */
|
|
575
|
+
function unowned (fn) {
|
|
576
|
+
const prev = owner
|
|
577
|
+
owner = null
|
|
578
|
+
try {
|
|
579
|
+
return fn()
|
|
580
|
+
} finally {
|
|
581
|
+
owner = prev
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/** Run `fn` without subscribing to what it reads. */
|
|
586
|
+
function untrack (fn) {
|
|
587
|
+
const prev = observer
|
|
588
|
+
observer = null
|
|
589
|
+
try {
|
|
590
|
+
return fn()
|
|
591
|
+
} finally {
|
|
592
|
+
observer = prev
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/** Leptos / Solid style: `const [count, setCount] = createSignal(0)`. */
|
|
597
|
+
function createSignal (value, options) {
|
|
598
|
+
const s = new Signal(value, options)
|
|
599
|
+
return [() => s.value, (v) => s.set(typeof v === 'function' ? v(s._value) : v)]
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const isSignal = (x) => x instanceof Signal || x instanceof Memo
|
|
603
|
+
|
|
604
|
+
/** A ref (`{ current }`) whose changes effects and memos can follow. */
|
|
605
|
+
function ref (value) { return new Ref(value) }
|
|
606
|
+
|
|
607
|
+
module.exports = {
|
|
608
|
+
Signal,
|
|
609
|
+
Memo,
|
|
610
|
+
Effect,
|
|
611
|
+
signal,
|
|
612
|
+
memo,
|
|
613
|
+
computed: memo,
|
|
614
|
+
effect,
|
|
615
|
+
batch,
|
|
616
|
+
untrack,
|
|
617
|
+
unowned,
|
|
618
|
+
createSignal,
|
|
619
|
+
isSignal,
|
|
620
|
+
startTransition,
|
|
621
|
+
urgent,
|
|
622
|
+
inTransition,
|
|
623
|
+
onTransitionDone,
|
|
624
|
+
scheduleDeferred,
|
|
625
|
+
Tracker,
|
|
626
|
+
Ref,
|
|
627
|
+
ref,
|
|
628
|
+
}
|