-
Notifications
You must be signed in to change notification settings - Fork 0
Description
This issue consolidates design requirements and implementation direction for three key primitives that expand JODS into a full-stack reactive state platform:
- #5
jods.persist(...)– storage adapter to persist store state to disk, memory, or remote. - #7
jods.sync(...)– bidirectional state replication across environments (client ↔ server, peer ↔ peer). - #9
jods.stream(...)– one-way async feed of external data (SSE, HTTP streams, etc.) into a store.
Together with defineStore() and withJods() (Remix), these complete the lifecycle of: initialize → mutate → hydrate → persist → sync → stream.
✅ Shared Design Principles
-
Unified Sink Model: All incoming state updates—whether local mutations, synced diffs, streamed payloads, or restored snapshots—should flow through a common reconciliation pathway. This enables
onUpdate,signals, and devtools to behave consistently regardless of source. -
Schema-Driven Trust Boundary: If a
defineStore()uses a Zod schema, it should act as the boundary for validating all incoming external data. Invalid updates should be ignored and optionally logged. -
No Dropped State: Streaming or syncing should never drop updates by default. If necessary for performance (e.g. UI rendering), downstream systems can batch. But the reactive core should maintain every change for time-travel/debugging.
-
Pluggable, Imperative API: All primitives should be low-level, composable JS calls (
persist(store, opts)) rather than builder chains (persist(...).andSync(...)) to preserve flexibility, composability, and Unix-style separation of concerns.
1. 🧠 jods.persist(store, opts?) - #5
Purpose: Store state to localStorage, IndexedDB, sessionStorage, or a remote adapter.
Core Behavior:
- On init, loads persisted state and merges into store.
- Subscribes to store updates and saves them (debounced).
- Should be opt-in per store or partial (with a filter).
- Can use schema versioning to manage breaking changes.
Key Features:
- Supports both sync and async storage engines.
- Optional
versionfield ondefineStore()could seed a cache key (e.g.,jods:cart:v1.2.0) and rotate on breaking schema changes. - Default error behavior: catch + log invalid saves; never crash app.
- Hook support:
onLoad,onSaveError.
Example:
jods.persist(store, {
storage: localStorage,
key: 'cart',
version: '1.0.2',
filter: (key) => !key.startsWith('secure_'),
});2. 🔄 jods.sync(socket, store, opts?) - #7
Purpose: Two-way syncing of state across network boundaries (WS, postMessage, devtools, etc.)
Core Behavior:
- Wraps a
SocketLikeinterface (with.send()+ onmessage). - Diffs outbound updates (via
onUpdate) and sends as patches. - Receives remote patches, validates, and applies via unified sink.
- Optionally handshake on connect to align divergent stores.
Must Support:
- Partial syncing (via key allow-list).
- Conflict resolution via timestamps (even in v1).
- Error logging, not crashing (e.g., schema validation fails).
- Events:
onPatchRejected,onConnectionLost.
Example:
jods.sync(socket, store, {
allowKeys: ['items', 'note'],
mode: 'peer', // future: 'primary', 'readOnly'
});Conflict Strategy:
- Each patch carries a timestamp.
- Local patches can queue if in-flight.
- Remote patches are ordered and applied only if newer.
3. 📡 jods.stream(readableStream | generator, store, opts?) - #9
Purpose: One-way push of streamed external data into a store (HTTP streams, SSE, etc.)
Core Behavior:
- Consumes JSON stream chunks or structured diffs.
- Applies them as patches (or replaces) on arrival.
- Default behavior: validate each chunk against schema.
Design Notes:
- Support
{ type: 'patch' | 'replace' }per chunk. - Optionally batch in
requestAnimationFrameto avoid UI jank. - Expose
jods.applyPatch()for manual patch usage.
Example:
await jods.stream(eventSource, store, {
validate: true,
batch: true,
onError: (err) => logger.warn('Stream parse failed', err),
});** Expose jods.applyPatch() alludes to the design principle from earlier about exposing the internals for advanced usage.
🧪 Devtools Integration
Existing:
[src/react/debugger.tsx](https://github.com/clamstew/jods/blob/main/src/react/debugger.tsx)
-
Expand debugger to visualize:
- Incoming patches (sync or stream)
- Source (local, persisted, streamed, remote)
- Rejected patches
- Versioning or hydration flows
-
Add dev command to simulate patches manually
-
Possibly: a debug
transportLogfor showing outbound and inbound event traffic
📚 Documentation Plan
New sections on the docs site should include:
-
Core Concepts: transport layer, schema validation, sync vs stream vs persist
-
How-To Guides:
- Offline caching with
jods.persist - Real-time sync between tabs or clients with
jods.sync - Streaming updates from an async function or HTTP response
- Offline caching with
-
Examples:
- Chat app (sync)
- Live dashboard (stream)
- Offline cart (persist)
- Remix integration walkthrough
🔬 Implementation & Testing Strategy
-
Use
[jods-remix-starter](https://github.com/clamstew/jods-remix-starter)to test:- Persist + Sync together (ensure no feedback loops)
- Sync + Stream on same store
- Schema versioning / migration edge cases
-
Plan to build:
jods.transport.test.tsjods-remix.e2e.tsPlaywright flows (multi-tab sync, SSR → stream handoff)
-
Cursor can be used to scaffold these workflows iteratively.
🚫 Not in Scope Yet
We anticipate building toward these capabilities, but they are not included in the initial implementation of persist, stream, or sync:
- CRDT/OT Conflict Resolution – Future work may implement true peer-to-peer conflict resolution via conflict-free replicated data types (CRDT) or operational transforms (OT).
- Chained API (
persist(...).andSync(...)) – While expressive, this approach limits composability and is harder to scale. We'll revisit if dev feedback demands it. - Server-Pushed Transforms – Applying a patch or command that maps to a defined action handler or reducer (e.g., transform via
cart.addItem) is a more opinionated abstraction and could appear in v2+. - Persistent Retry Queues – For now, we assume sync happens only when connected. An offline queue or sync-on-reconnect flow (e.g., local writes buffered until network resumes) could follow in a dedicated feature.
📌 TL;DR
This issue defines the shape of JODS' transport architecture. V1 of persist, stream, and sync aim to be composable primitives that respect schema boundaries, maintain signal-based updates, and push state coherently across runtime boundaries—without relying on last-write-wins or dropping updates.
This enables JODS to truly become the "active model" layer of a Remix app, with Remix as the router/controller and JODS as the data layer—coherent, reactive, and cross-context.