Notify guides
06 · Vue, Svelte and plain JavaScript

Vue, Svelte and plain JavaScript.

@productcraft/notify-js holds everything stateful: tokens, the real-time stream, the inbox cache, counts, optimistic marks and preferences. Bring your own rendering.

npm install @productcraft/notify-js. Your server needs the token endpoint from the React guide, step 1.


1

The inbox store

notify.inbox follows the Svelte store contract: subscribe calls your listener at once and on every change and returns the unsubscribe function, so $inbox works in Svelte as is. Tokens stay in memory, never in localStorage, cookies or URLs.

inbox.ts
import { createNotifyClient } from "@productcraft/notify-js";

const notify = createNotifyClient({
  getToken: () => fetch("/api/notify-token").then((r) => r.text()),
});

const badge = document.querySelector<HTMLElement>("#badge")!;
const list = document.querySelector<HTMLUListElement>("#inbox")!;

// Runs at once and on every change. The first subscriber loads the first
// page and opens the real-time stream; the last one to leave closes it.
const stop = notify.inbox.subscribe(({ items, counts }) => {
  badge.textContent = counts.unread ? String(counts.unread) : "";
  list.replaceChildren(
    ...items.map((item) => {
      const li = document.createElement("li");
      li.textContent = item.title;
      li.dataset.read = item.read_at ? "true" : "false";
      li.onclick = () => void notify.inbox.markRead([item.id]);
      return li;
    }),
  );
});

notify.inbox.onNew((item) => console.log("new:", item.title));
document.querySelector("#mark-all")?.addEventListener("click", () => void notify.inbox.markAllRead());

// On sign-out:
export function signOut() {
  stop();
  notify.close();
}

2

A Vue composable

composables/useInbox.ts
// composables/useInbox.ts
import { onScopeDispose, shallowRef } from "vue";
import { createNotifyClient, type NotifyBrowserClient } from "@productcraft/notify-js";

let client: NotifyBrowserClient | undefined;
const getClient = () =>
  (client ??= createNotifyClient({ getToken: () => fetch("/api/notify-token").then((r) => r.text()) }));

export function useInbox() {
  const notify = getClient();
  const state = shallowRef(notify.inbox.getState());
  const stop = notify.inbox.subscribe((s) => (state.value = s));
  onScopeDispose(stop);
  return { state, markRead: (ids: string[]) => notify.inbox.markRead(ids), markAllRead: () => notify.inbox.markAllRead() };
}

3

Real-time behaviour

  • One stream per client (Server-Sent Events over fetch, so the token goes in a header). It reconnects with backoff and catches up on what it missed.
  • After three failed connections it polls the unread count every 30 seconds until the stream is back. It closes while the tab is hidden.
  • When the token expires or is revoked it asks your getToken again; parallel requests share one call.