Solid

No extra package: a Solid store mirrors the pulse, and reconcile updates only the fields that changed. Every effect and DOM node depends on exactly what it reads.

The definition

note.ts
import {
  array, createWebSocketConnector, definePulse, object,
  type ModelFromDefinition, type Pulse,
} from "pulsify-core";

export const Note = definePulse("Note", object({
  title: "string",
  tags: array("string"),
  author: object({ name: "string" }),
})).materialize();
export type NotePulse = Pulse<ModelFromDefinition<typeof Note>>;

export const remote = createWebSocketConnector("wss://sync.example.com/sync");

The helper

createPulseStore.ts
import { onCleanup } from "solid-js";
import { createStore, reconcile } from "solid-js/store";
import { snapshotPulse, subscribePulse, type Pulse } from "pulsify-core";

/** A read-only store that follows the pulse. Write to the pulse itself. */
export function createPulseStore<P extends Pulse<any>>(pulse: P) {
  const [store, setStore] = createStore(snapshotPulse(pulse));
  onCleanup(subscribePulse(pulse, (p) => setStore(reconcile(snapshotPulse(p)))));
  return store;
}

Each local or remote change takes a snapshot and reconciles it into the store. Rich text reads as its plain text, a sub-pulse as its ID. The cost is one snapshot per change; split very large documents into sub-pulses.

Render and edit

Editor.tsx
import { For } from "solid-js";
import { createPulseStore } from "./createPulseStore";
import type { NotePulse } from "./note";

export function Editor(props: { pulse: NotePulse }) {
  const note = createPulseStore(props.pulse);
  return (
    <>
      <input value={note.title} onInput={(e) => (props.pulse.title = e.currentTarget.value)} />
      <p>by {note.author.name}</p>
      <ul>
        <For each={note.tags}>{(tag) => <li>{tag}</li>}</For>
      </ul>
      <button onClick={() => props.pulse.tags.push("new")}>Add tag</button>
    </>
  );
}

Read from the store, write to the pulse. A write commits, notifies the store synchronously and syncs to every peer.

Load on the client

src/components/NotePage.tsx
// src/components/NotePage.tsx: imported with clientOnly, so it never runs on the server.
import { useParams } from "@solidjs/router";
import { createResource, onCleanup, Show } from "solid-js";
import { disconnectPulse, loadPulse } from "pulsify-core";
import { Editor } from "./Editor";
import { Note, remote } from "./note";

export default function NotePage() {
  const params = useParams();
  const [pulse] = createResource(() => params.id, (id) => loadPulse(Note, id, { connectors: [remote] }));
  onCleanup(() => pulse() && disconnectPulse(pulse()!));
  return (
    <Show when={pulse()} keyed fallback={<p>Loading…</p>}>
      {(note) => <Editor pulse={note} />}
    </Show>
  );
}
src/routes/notes/[id].tsx
// src/routes/notes/[id].tsx
import { clientOnly } from "@solidjs/start";

const NotePage = clientOnly(() => import("~/components/NotePage"));

export default function Route() {
  return <NotePage fallback={<p>Loading…</p>} />;
}