# Realtime

> Socket events that invalidate or patch cached entries with ./live, and one poller and one socket per browser with ./tabs.

Page: https://svelte-lean.vercel.app/query/realtime · Markdown: https://svelte-lean.vercel.app/query/realtime.md · Index: https://svelte-lean.vercel.app/llms.txt

Server events change the cache through declared rules; a browser with several tabs open polls and listens once.

> **Note** `./live` and `./tabs` are experimental: the API can still change in a minor release. Stability per entry point: [Entry points](https://svelte-lean.vercel.app/query.md#entry-points).

## Setup

`src/routes/+layout.svelte`

```svelte
<!-- src/routes/+layout.svelte -->
<script lang="ts">
	import { createClient, setClient } from '@svelte-lean/query';
	import { http } from '@svelte-lean/query/http';
	import { live } from '@svelte-lean/query/live';
	import { tabs } from '@svelte-lean/query/tabs';
	import { can, session } from '$api/session.svelte';
	import { memberRules } from './rules';
	import { eventSource } from './source';

	let { children } = $props();

	setClient(
		createClient({
			transport: http({ base: '/' }),
			scope: () => [session.workspace],
			can,
			modules: [
				live(eventSource('/events'), memberRules),
				// One poller and one socket per browser; tabs of another user never share.
				tabs({ session: () => (session.userId === null ? null : String(session.userId)) })
			]
		})
	);
</script>

{@render children()}
```

Both are modules of the client. Call sites do not change: a list that is open polls and receives events with the same `createQuery`.

## Live source

`src/lib/live-source.ts`

```ts
// src/lib/live-source.ts — a LiveSource over server-sent events: one EventSource per channel.
// A Pusher, Reverb or WebSocket client is wrapped the same way.
import type { LiveSource } from '@svelte-lean/query/live';

export function eventSource(base: string): LiveSource {
	return {
		subscribe({ channel, event }, deliver, ready) {
			const events = new EventSource(`${base}/${encodeURIComponent(channel)}`);
			const listener = (message: MessageEvent<string>) => deliver(JSON.parse(message.data));
			events.addEventListener(event, listener);
			events.addEventListener('open', () => ready(), { once: true });
			return () => {
				events.removeEventListener(event, listener);
				events.close();
			};
		}
	};
}
```

A `LiveSource` subscribes to one channel and event, delivers payloads and calls `ready()` once the server confirmed. A Pusher, Reverb or WebSocket client is wrapped the same way; an optional `watch()` reports the connection, and a reconnect reseeds.

## Rules

| Kind | Shape | Effect |
| --- | --- | --- |
| invalidate | `rule(tag, …)` | The event says something changed; observed entries with the tag refetch, coalesced per burst; unobserved ones are marked stale. |
| patch an entity | `patch: (m) => patch(tag(…), …)` | The event carries the row; every cached page, list and detail holding it is updated without a request. |
| patch an entry | `patch: (m) => patch([endpoint, params], …)` | The event carries a field of one entry; that entry is changed without a request. |

### Invalidate

`poke, then pull`

```ts
// Poke, then pull: the observed lists tagged Members refetch, coalesced per burst.
rule('Members', { channel: 'members', event: 'member.created' }),
```

### Patch

`the event carries the row`

```ts
// The event carries the row: every cached page, list and detail holding it is patched.
rule('Members', {
	channel: 'members',
	event: 'member.updated',
	message: type<{ member: Member }>(),
	patch: (m) => patch(tag('Member', m.member.id), () => m.member)
}),
```

### Patch one entry

`one field of one entry`

```ts
// Change one field of the one cached detail the message names.
rule(getMember, {
	channel: (params) => `members.${params.id}`,
	event: 'presence.changed',
	message: type<{ id: number; status: Member['status'] }>(),
	patch: (m) => patch([getMember, { id: m.id }], (member) => ({ ...member, status: m.status }))
})
```

The rule's `patch` calls the same `patch()` as the rest of the application. Write the absolute new value, never a relative change, so an event applied twice gives the same result. `message` validates the payload; an event that fails it is dropped. A rule with `patch` invalidates nothing unless it says `invalidate`.

### Behavior

- Nothing subscribes until an entry a rule targets is observed.
- Channels are reference-counted and kept for `linger` after the last observer left.
- A confirmed subscription and a reconnect invalidate the rule's targets (`reseed`), so events missed while away are not lost.
- A burst of events is one refetch per observed entry, with at most one request in flight per entry.

| Option of live() | Default | Effect |
| --- | --- | --- |
| `window` | `'100ms'` | Coalescing window per burst of events. |
| `maxWindow` | `'2s'` | Ceiling of the window under sustained bursts. |
| `linger` | `'1.5s'` | How long a channel stays after its last observer left. |

## Tabs

With `tabs()`, one tab polls each polled entry and holds the live connection (Web Locks); the others receive what it learns over a `BroadcastChannel`: invalidations (a save in one tab refreshes the lists of the others), changed values and forwarded events.

| Option | Type | Default | Effect |
| --- | --- | --- | --- |
| `session` | `() => string \| null` | `none` | Partition beside the client scope; tabs of another session never share. null: this tab works alone. |
| `share` | `number \| false` | `16384` | Largest value (JSON text length) sent with a changed entry; above it the other tabs refetch. |
| `name` | `string` | `'slq'` | Namespace of the channel and lock names. |

- A visible tab takes a poll over from a hidden one; a closed, crashed or frozen leader is replaced.
- Tabs of another session (or another scope) never hear each other.
- Without `BroadcastChannel` or Web Locks every tab works alone, as without the module.

### Measured

A list polled every two seconds, open in several pages of one browser context (benchmark P6, production build, median of five runs):

| Pages | List requests per minute | Live connections |
| --- | --- | --- |
| 1 tab, with ./tabs | 30 | 1 |
| 2 tabs, with ./tabs | 30 | 1 |
| 5 tabs, with ./tabs | 30 | 1 |
| 5 tabs, without the module | 150 | 5 |

Method and environment: [Performance](https://svelte-lean.vercel.app/query/performance.md).

## Reference

- [@svelte-lean/query/live](https://svelte-lean.vercel.app/query/api/live.md)
- [@svelte-lean/query/tabs](https://svelte-lean.vercel.app/query/api/tabs.md)
- [Testing](https://svelte-lean.vercel.app/query/testing.md): `fakeSource()` emits events in tests
