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
getTokenagain; parallel requests share one call.