Svelte

Wrap a pulse with a small helper built on Svelte's createSubscriber. Templates read and write it like any object, and remote changes update them.

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

src/lib/pulseState.ts
import { createSubscriber } from "svelte/reactivity";
import { subscribePulse, type Pulse } from "pulsify-core";

/** A view of the pulse whose reads Svelte tracks. Writes go to the pulse. */
export function pulseState<P extends Pulse<any>>(pulse: P): P {
  const track = createSubscriber((update) => subscribePulse(pulse, update));
  const views = new WeakMap<object, object>();
  const view = (value: object): object => {
    let proxy = views.get(value);
    if (!proxy) {
      proxy = new Proxy(value, {
        get(target, key) {
          track();
          const item = Reflect.get(target, key);
          return typeof item === "object" && item !== null && !ArrayBuffer.isView(item)
            ? view(item)
            : item;
        },
      });
      views.set(value, proxy);
    }
    return proxy;
  };
  return view(pulse) as P;
}

Every template expression that reads the pulse re-runs after a change of that pulse, and Svelte only touches the DOM nodes whose value changed. Nested objects and arrays are wrapped too, so components receiving note.author or an item of note.tags stay up to date.

Render and edit

Editor.svelte
<script lang="ts">
  import type { NotePulse } from "$lib/note";

  let { note }: { note: NotePulse } = $props();
</script>

<input bind:value={note.title} />
<p>by {note.author.name}</p>
<ul>
  {#each note.tags as tag}<li>{tag}</li>{/each}
</ul>
<button onclick={() => note.tags.push("new")}>Add tag</button>

bind:value, assignments and array methods write straight to the pulse, which syncs them to every peer.

Load on the client

src/routes/notes/[id]/+page.svelte
<script lang="ts">
  import { onMount } from "svelte";
  import { disconnectPulse, loadPulse } from "pulsify-core";
  import { page } from "$app/state";
  import { Note, remote, type NotePulse } from "$lib/note";
  import { pulseState } from "$lib/pulseState";
  import Editor from "./Editor.svelte";

  let note = $state.raw<NotePulse>();

  // onMount never runs during server rendering.
  onMount(() => {
    let pulse: NotePulse | undefined;
    loadPulse(Note, page.params.id, { connectors: [remote] }).then((loaded) => {
      pulse = loaded;
      note = pulseState(loaded);
    });
    return () => pulse && disconnectPulse(pulse);
  });
</script>

{#if note}<Editor {note} />{:else}<p>Loading…</p>{/if}

Or render the whole page in the browser:

src/routes/notes/[id]/+page.ts
// src/routes/notes/[id]/+page.ts
export const ssr = false;