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

Add Signal.Buffer alongside Signal.State and Signal.Computed #243

Open
dead-claudia opened this issue Oct 26, 2024 · 0 comments
Open

Add Signal.Buffer alongside Signal.State and Signal.Computed #243

dead-claudia opened this issue Oct 26, 2024 · 0 comments

Comments

@dead-claudia
Copy link
Contributor

dead-claudia commented Oct 26, 2024

Currently, in order to deal with events without losing them, while still being able to subscribe to events as they come, you need to do something like this:

// Extends `Signal.Computed` so you can watch it.
class BufferSignal extends Signal.Computed {
    constructor(opts) {
        super(() => {
            this.#state.get()
            const buf = this.#buffer
            this.#buffer = []
            return buf
        }, {
            ....opts,
            equals: () => false,
        })
    }

    #buffer = []
    #state = new Signal.State()

    push(...values) {
        this.#buffer.push(...values)
        this.#state.set(undefined)
    }
}

This kind of notification followed by pulling is a very common pattern. To name some Web APIs that are essentially exposing this very model, but using events instead of watchers:

  • MutationObserver
  • IntersectionObserver
  • PerformanceObserver
  • ResizeObserver
  • PressureObserver
  • ReportingObserver
  • Pointer events' event.getCoalescedEvents()
  • Pointer events' event.getPredictedEvents()
  • WebGPU's GPUQueue, indirectly (though it comes with perf risks)

Here's a couple other similar patterns I've seen elsewhere:

  • Node's object streams' legacy "readable" event + stream.read() idiom
  • The push notification -> poll for changes mechanism often used to keep apps up to date (like in Matrix) is little more than a highly async version of this.

As you might imagine from the initial code snippet, there's room for optimization that implementors are uniquely positioned to perform.

  • The inner computed and state can be fully elided. Instead, the array can be pushed to and returned directly.
  • No need to compute the equals: () => false, making pushes almost instant.

Userland can implement these as well, but at a cost: you can no longer .watch the signal directly. So the only way to both make it fast and make it compatible with watchers is to make it a built-in.

(Also, it'd be a lot easier for HTML to use it if it's built-in.)


Note: while I mentioned a type of stream as an example of this idiom, I am not proposing any sort of stream be designed in terms of signals. That is out of scope, and I'm skeptical it's even a good idea.

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

1 participant