Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Clarification request: (Semantic) equivalence of push-only and push/pull models #233

Open
dehmer opened this issue Jul 17, 2024 · 1 comment

Comments

@dehmer
Copy link

dehmer commented Jul 17, 2024

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]
  })
})
@NullVoxPopuli
Copy link
Collaborator

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants