GitShow/developit/mixed-signals
developit

mixed-signals

Use Preact Models + Signals from a server as if they lived on the client.

by developit
preactsignals
Star on GitHubForkWebsitenpm

TypeScript

45 stars10 forks9 contributorsActive · 1w agoSince 2026v0.5.0MIT

Meet the team

See all 9 on GitHub →
developit
developit60 contributions
JoviDeCroock
JoviDeCroock18 contributions
github-actions[bot]Bot
github-actions[bot]6 contributions
felipeleusin
felipeleusin4 contributions
CopilotBot
Copilot3 contributions
nkabrich
nkabrich2 contributions
airhorns
airhorns1 contribution
jed
jed1 contribution

Languages

View on GitHub →
TypeScript97.7%
JavaScript2.3%

Commit activity

Last 12 weeks · 58 commits

Full graph →

Community health

2 of 6 standards met

Community profile →
42
✓README✓License○Contributing○Code of Conduct○Issue Template○PR Template

Recent PRs & issues

Active · 10 in progress · Last activity 1w ago
See all on GitHub →
felipeleusin
Reacquire held models before resubscribingOpenPR

Why A client can retain a model returned by an RPC while temporarily observing none of its fields. After the last , the server may evict its registrations, but observing the same facade again currently sends only with old signal ids. Unobserved models omitted from a reconnect root have the same problem when the server process has not changed. Client reflection now marks those models stale and requests their existing refresh before subscribing to their rebound field ids. The same facade and field signals receive the fresh snapshot and subsequent live updates. This reuses existing stale-model tracking and the existing wire protocol. The refresh is conservative: resuming observation after the batched unwatch adds an round trip even if the server has not evicted the model yet. Quick remounts that cancel , models hydrated by the root, and sealed signals do not cause idle reacquisition. Concurrent field observations share a refresh; unavailable models stay stale without sending watches to old ids, and a later observation can retry. Refresh completion preserves queued unwatches and ignores work superseded by another reconnect. The server must still resolve stable model markers through its instance registry or application reconstruction logic. This PR does not add a server eviction policy. Validation Added 18 wire-level lifecycle tests covering idle reactivation, same-process and process-change reconnects, transport reopen, request ordering, field identity, subsequent updates, failure/retry, observer disposal, queued unwatches, and sealed signals. The primary regressions fail against the original reflection implementation. : 287 tests passed across 11 files. , , and passed. Includes architecture documentation and a patch changeset.

felipeleusin · 2d ago
airhorns
Send full values and models after unwatchOpenPR

Why? After a client unwatches a signal, it can discard the model that contains it. The server still remembers sending that model, so it can later send only its ID and leave the client with missing fields. What does this PR accomplish? now unsubscribes and clears the server's record of that client's signal value and related models. The next watch sends a full value, even if unchanged. Models include their fields when sent again. Example 1. Open a product and read its signal in an effect. 2. Navigate away. The effect stops, the client sends , and garbage collection can remove the product model. 3. Reopen the product. The server now sends its fields again, so is available. Scope and verification The server keeps the actual model instances and their methods. Other subscriptions and other clients keep working. Once a full value has been sent, ordinary delta updates resume. The reset includes nested models and models that share a signal. Full model payloads include their signal values, including signals inside arrays or plain objects. This change requires an message. Garbage collection itself sends no message, so collection of a model that was never watched remains outside this fix. A process-transport check with the unpatched published client confirmed that navigating A → B → garbage collection → A restores the model and its code against both server implementations.

airhorns · 5d ago
developit
Add 'The Impossible Orchestra' multi-realm demo scaffold for mixed-signalsOpenPR

Motivation Introduce a multi-realm demo to demonstrate semantics where remote Signals and model methods feel local while ownership boundaries remain explicit. Provide a clear product/architecture plan and implementor guidance to prototype audio, visual, broker, and UI realms in a single vertical slice. Scaffold app- and transport-level pieces so future implementation work can hook into existing RPC/transport/model abstractions. Description Add a full demo scaffold under including , , and that describe product goals, architecture rules, and implementation milestones. Implement realm models and roots using for , , and realms in plus worker RPC glue in and and a broker runtime in with . Add client-side reflected models and RPC client setup in along with small view/component wrappers that read snapshots and call reflected methods (e.g. , , ). Add transport helpers and loopback utilities in and shared API/type contracts in , and update to include for type-checking. Testing No automated tests were executed as part of this scaffolding change. was updated to include the path so these files will be picked up by the repository typechecks/CI when run. Codex Task

developit · 1mo ago

Recent fixes

View closed PRs →
felipeleusin
Let the server mark signals final so clients stop subscribingMergedPR

Why Rendering settled historical data creates live Signals for every reflected field, and observing them queues large watch frames even though the server already knows those values can never change. The server should express that invariant directly so clients avoid inert subscriptions without relying on application-specific heuristics or signal-core internals. Final API The public API remains . A signal already final when serialized is . A signal finalized later is announced as , queued and debounced for one second. Existing update modes remain unchanged: omitted = replace, , , and . The only new mode is . Sealing removes the server subscription immediately. The client preserves the current value, drops watched and active state, and sends no . Forwarders remember upstream seals, relay them to current recipients, and answer later watches locally. Finality remains permanent across signal state transfer and reconnects. Final signals continue to serialize inline because they are not strongly retained by watch state. Implementation The server coalesces pending seals behind one timer while immediately tearing down subscriptions. The client handles as an update mode without changing the established delta encodings. Forwarding rewrites, relays, and caches the seal state. Architecture documentation, generated API documentation, tests, and the changeset reflect the final wire contract. Verification (269 tests), , , , and pass.

felipeleusin · 2w ago
developit
Send a bare ref when a client already holds a signal valueMergedPR

Why Serializing a signal always inlined its value, so any value that reached a client twice was paid for twice. The natural way to answer "give me the diff" without shipping a second copy is to return the reflected signal itself — . But the reply inlined the whole payload again, right next to the update the client had already received. In Claudex that was a 474 KB diff crossing the wire twice in adjacent frames. What now emits a bare (no ) when it can prove the client already holds that exact value: 1. the last value sent to that client for that signal is identical, and 2. the client still has a live subscription to it. Both halves matter. The last-sent value says the bytes arrived; the subscription says the client still has somewhere to put them — watched signals are held strongly client-side (), so a subscription rules out the signal having been collected out of the client's cache and the ref resolving to a fresh signal. The client reviver already handled -less refs, so only the server half was missing. Model refresh () deliberately re-inlines a model's own signals — a refresh means the client stopped trusting its copy, and a ref back to that copy is worthless. That's what is for; two existing tests catch it if you remove it. Measured The e2e test returns a 50,000-char signal from an RPC method. Before: ~50,024 bytes. After: 20 bytes, value intact and still live (a later server-side mutation propagates). Suite 260/260, typecheck and lint clean.

developit · 1mo ago
felipeleusin
Detect stale transports while calls are pendingMergedPR

Problem A connection can go half-open: the network path is gone (laptop sleep/wake, NAT or LB idle expiry, server died without sending a FIN) but no event fires, sometimes for minutes. TCP only notices when retransmits give up, and browsers don't expose WebSocket ping/pong to JavaScript, so from the page's point of view the socket just goes quiet. A made on such a socket sits in the map forever: nothing rejects it until a close event or an explicit . We hit this in production. A tab held a dead socket for ~4 minutes before the close event finally arrived, and a call sat pending the whole time with no error surfaced anywhere. The README already tells callers to wrap in their own timeout; has the same gap, but it can't be fixed from outside the library — is private, and an app-level wrapper around a call can't tell a slow response from a dead transport. Fix Treat the transport as disconnected when the oldest pending call and the newest inbound frame are both older than (default 30s). This goes through the existing path (pending calls reject, rejects, subscription replay arms) plus a call to the transport's new optional so the dead socket is torn down. Host reconnect logic picks up from there the same as for a real close. Requiring both clocks to be old is what keeps false positives out: any inbound frame (a result for another call, a signal update, a root snapshot) resets the clock, so a slow response on an otherwise live connection is fine a call sent on a long-quiet but healthy connection starts its own clock, so old silence doesn't instantly condemn it Details: checks are deadline-scheduled, not polled. Calls and inbound frames only push the deadline later, so an armed timer never needs rescheduling; it wakes, recomputes, and sleeps the difference. No timer exists while nothing is pending, and it's unref'd so it won't hold a Node process open. is optional and additive; existing transports are unaffected. disables detection. Default This is on by default: a call that hangs forever is a bug for every network consumer, and the silence gate makes false positives unlikely. The case that loses is a transport that can't go half-open but can legitimately block — e.g. a MessagePort to a worker doing 30s+ of synchronous work while a call is pending. That's what is for, and the TSDoc calls it out. Happy to flip this to opt-in if you'd rather ship it that way; the mechanism is the same. Testing Six new tests in : rejection + transport close after total silence, deferral when inbound frames arrive, disarming while nothing is pending, , the 30s default, and interaction with . Full suite passes (237). , , and are clean; README regenerated with . 🤖 Generated with Claude Code

felipeleusin · 1mo ago
Structured data for AI agents

Repository: developit/mixed-signals. Description: Use Preact Models + Signals from a server as if they lived on the client. Stars: 45, Forks: 10. Primary language: TypeScript. Languages: TypeScript (97.7%), JavaScript (2.3%). License: MIT. Homepage: https://npm.im/mixed-signals Topics: preact, signals. Latest release: v0.5.0 (1w ago). Open PRs: 10, open issues: 0. Last activity: 1w ago. Community health: 42%. Top contributors: developit, JoviDeCroock, github-actions[bot], felipeleusin, Copilot, nkabrich, airhorns, jed, infiton.

·@ofershap

Replace github.com with gitshow.dev