I tried to re-implement our own Signal library on top of the current polyfill (v0.1.1). Our library is based on a two-phase push-only model with explicit dependency tracking. This is pretty much OK for static dependency graphs (our main applications/use cases). Effects are more or less the same as computed/derived signals, except that effects can be optionally disposed.
From my understanding
implementing push-only semantics (including effects) is not possible with the current proposal, let alone differences in runtime characteristics. Synchronous eager effects cannot be realized with Computed + Watcher.
Reasoning would be that since pulling signal values in watcher must be deferred to not read stale state, effects are inherently called asynchronous.
Thanks for your attention and please help me understand whether or not this presumption is correct.
Here is some complementary code to (maybe) better show what I'm trying to achieve.
import assert from 'assert'
import { Signal as Polyfill } from 'signal-polyfill'
import Signal from '@syncpoint/signal'
const { State, Computed } = Polyfill
const { Watcher } = Polyfill.subtle
// 'Simple' effect as proposed in different locations.
// Don't care about clean-up for brevity.
const effect = callback => {
let busy = false
const watcher = new Watcher(() => {
const pull = () => {
// Pulling immediately may result in stale state.
watcher.getPending().forEach(s => s.get())
watcher.watch() // re-watch
busy = false
}
!busy && (busy = true, queueMicrotask(pull))
})
const computed = new Computed(callback)
watcher.watch(computed)
computed.get() // pull immediately
}
describe('Polyfill', function () {
it('async/effect', async function () {
const a = new State(4)
const acc = []
effect(() => acc.push(a.get()))
const countdown = ((n) => setInterval(() => n && a.set(n--), 0))
countdown(3)
await new Promise(resolve => setTimeout(resolve, 10))
assert.deepStrictEqual(acc, [4, 3, 2, 1]) //=> [PASS]
})
it('sync/effect [presumably impossible]', function () {
const a = new State(4)
const acc = []
effect(() => acc.push(a.get()))
;[3, 2, 1].forEach(a.set.bind(a)) // no time for watcher to kick in
assert.deepStrictEqual(acc, [4, 3, 2, 1]) //=> [FAIL] actual: [4]
})
})
describe('Signal', function () {
it('on :: Signal s => (a -> *) -> s a -> (() -> Unit)', function () {
const a = Signal.of(4)
const acc = []
a.on(v => acc.push(v)) // eager, synchronous effect
;[3, 2, 1].map(a)
assert.deepStrictEqual(acc, [4, 3, 2, 1]) //=> [PASS]
})
it('scan :: Signal s => (b -> a -> b) -> b -> s a -> s b', function () {
const a = Signal.of(4)
const b = Signal.scan((acc, v) => acc.concat(v), [], a)
;[3, 2, 1].map(a)
assert.deepStrictEqual(b(), [4, 3, 2, 1]) //=> [PASS]
})
})
I tried to re-implement our own Signal library on top of the current polyfill (v0.1.1). Our library is based on a two-phase push-only model with explicit dependency tracking. This is pretty much OK for static dependency graphs (our main applications/use cases). Effects are more or less the same as computed/derived signals, except that effects can be optionally disposed.
From my understanding
Reasoning would be that since pulling signal values in watcher must be deferred to not read stale state, effects are inherently called asynchronous.
Thanks for your attention and please help me understand whether or not this presumption is correct.
Here is some complementary code to (maybe) better show what I'm trying to achieve.