npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

svelte-adapter-uws-extensions

v0.4.0

Published

Redis and Postgres extensions for svelte-adapter-uws - distributed pub/sub, replay buffers, presence tracking, rate limiting, groups, and DB change notifications

Readme

svelte-adapter-uws-extensions

Redis and Postgres extensions for svelte-adapter-uws.

The core adapter keeps everything in-process memory. That works great for single-server deployments, but the moment you scale to multiple instances you need shared state. This package provides drop-in replacements backed by Redis and Postgres, with the same API shapes you already know from the core plugins.

What you get

  • Distributed pub/sub - platform.publish() reaches all instances, not just the local one
  • Persistent replay buffers - messages survive restarts, backed by Redis sorted sets or a Postgres table
  • Cross-instance presence - who's online across your entire fleet, with multi-tab dedup
  • Distributed rate limiting - token bucket enforced across all instances via atomic Lua script
  • Distributed broadcast groups - named groups with membership and roles that span instances
  • Shared cursor state - ephemeral positions (cursors, selections, drawing strokes) visible across instances
  • Database change notifications - Postgres LISTEN/NOTIFY forwarded straight to WebSocket clients
  • Prometheus metrics - expose extension metrics for scraping, zero overhead when disabled

Table of contents

Getting started

Clients

Redis extensions

Postgres extensions

Observability

Reliability

Operations

More


Getting started

Installation

npm install svelte-adapter-uws-extensions ioredis

Postgres support is optional:

npm install pg

Requires svelte-adapter-uws >= 0.2.0 as a peer dependency.


Clients

Redis client

Factory that wraps ioredis with lifecycle management. All Redis extensions accept this client.

// src/lib/server/redis.js
import { createRedisClient } from 'svelte-adapter-uws-extensions/redis';

export const redis = createRedisClient({
  url: 'redis://localhost:6379',
  keyPrefix: 'myapp:'   // optional, prefixes all keys
});

Options

| Option | Default | Description | |---|---|---| | url | 'redis://localhost:6379' | Redis connection URL | | keyPrefix | '' | Prefix for all keys | | autoShutdown | true | Disconnect on sveltekit:shutdown | | options | {} | Extra ioredis options |

API

| Method | Description | |---|---| | redis.redis | The underlying ioredis instance | | redis.key(k) | Returns keyPrefix + k | | redis.duplicate(overrides?) | New connection with same config. Pass ioredis options to override defaults. | | redis.quit() | Gracefully disconnect all connections |


Postgres client

Factory that wraps pg Pool with lifecycle management.

// src/lib/server/pg.js
import { createPgClient } from 'svelte-adapter-uws-extensions/postgres';

export const pg = createPgClient({
  connectionString: 'postgres://localhost:5432/mydb'
});

Options

| Option | Default | Description | |---|---|---| | connectionString | required | Postgres connection string | | autoShutdown | true | Disconnect on sveltekit:shutdown | | options | {} | Extra pg Pool options |

API

| Method | Description | |---|---| | pg.pool | The underlying pg Pool | | pg.query(text, values?) | Run a query | | pg.createClient() | New standalone pg.Client with same config (not from the pool) | | pg.end() | Gracefully close the pool |


Redis extensions

Pub/sub bus

Distributes platform.publish() calls across multiple server instances via Redis pub/sub. Each instance publishes locally AND to Redis. Incoming Redis messages are forwarded to the local platform with echo suppression (messages from the same instance are ignored).

Setup

// src/lib/server/bus.js
import { redis } from './redis.js';
import { createPubSubBus } from 'svelte-adapter-uws-extensions/redis/pubsub';

export const bus = createPubSubBus(redis);

Usage

// src/hooks.ws.js
import { bus } from '$lib/server/bus';

let distributed;

export function open(ws, { platform }) {
  // Start subscriber (idempotent, only subscribes once)
  bus.activate(platform);
  // Get a wrapped platform that publishes to Redis + local
  distributed = bus.wrap(platform);
}

export function message(ws, { data, platform }) {
  const msg = JSON.parse(Buffer.from(data).toString());
  // This publish reaches local clients AND all other instances
  distributed.publish('chat', 'message', msg);
}

Options

| Option | Default | Description | |---|---|---| | channel | 'uws:pubsub' | Redis channel name |

API

| Method | Description | |---|---| | bus.wrap(platform) | Returns a new Platform whose publish() sends to Redis + local | | bus.activate(platform) | Start the Redis subscriber (idempotent) | | bus.deactivate() | Stop the subscriber |


Replay buffer (Redis)

Same API as the core createReplay plugin, but backed by Redis sorted sets. Messages survive restarts and are shared across instances.

Setup

// src/lib/server/replay.js
import { redis } from './redis.js';
import { createReplay } from 'svelte-adapter-uws-extensions/redis/replay';

export const replay = createReplay(redis, {
  size: 500,
  ttl: 3600  // expire after 1 hour
});

Usage

// In a form action or API route
export const actions = {
  send: async ({ request, platform }) => {
    const data = Object.fromEntries(await request.formData());
    const msg = await db.createMessage(data);
    await replay.publish(platform, 'chat', 'created', msg);
  }
};
// In +page.server.js
export async function load() {
  const messages = await db.getRecentMessages();
  return { messages, seq: await replay.seq('chat') };
}
// In hooks.ws.js - handle replay requests
export async function message(ws, { data, platform }) {
  const msg = JSON.parse(Buffer.from(data).toString());
  if (msg.type === 'replay') {
    await replay.replay(ws, msg.topic, msg.since, platform);
    return;
  }
}

Options

| Option | Default | Description | |---|---|---| | size | 1000 | Max messages per topic | | ttl | 0 | Key expiry in seconds (0 = never) |

API

All methods are async (they hit Redis). The API otherwise matches the core plugin exactly:

| Method | Description | |---|---| | publish(platform, topic, event, data) | Store + broadcast | | seq(topic) | Current sequence number | | since(topic, seq) | Messages after a sequence | | replay(ws, topic, sinceSeq, platform) | Send missed messages to one client | | clear() | Delete all replay data | | clearTopic(topic) | Delete replay data for one topic |


Presence

Same API as the core createPresence plugin, but backed by Redis hashes. Presence state is shared across instances with cross-instance join/leave notifications via Redis pub/sub.

Setup

// src/lib/server/presence.js
import { redis } from './redis.js';
import { createPresence } from 'svelte-adapter-uws-extensions/redis/presence';

export const presence = createPresence(redis, {
  key: 'id',
  select: (userData) => ({ id: userData.id, name: userData.name }),
  heartbeat: 30000,
  ttl: 90
});

Usage

// src/hooks.ws.js
import { presence } from '$lib/server/presence';

export async function subscribe(ws, topic, { platform }) {
  await presence.join(ws, topic, platform);
}

export async function close(ws, { platform }) {
  await presence.leave(ws, platform);
}

Options

| Option | Default | Description | |---|---|---| | key | 'id' | Field for user dedup (multi-tab) | | select | strips __-prefixed keys | Extract public fields from userData | | heartbeat | 30000 | TTL refresh interval in ms | | ttl | 90 | Per-entry expiry in seconds. Entries from crashed instances expire individually after this period, even if other instances are still active on the same topic. |

API

| Method | Description | |---|---| | join(ws, topic, platform) | Add connection to presence | | leave(ws, platform, topic?) | Remove from a specific topic, or all topics if omitted | | sync(ws, topic, platform) | Send list without joining | | list(topic) | Get current users | | count(topic) | Count unique users | | clear() | Reset all presence state | | destroy() | Stop heartbeat and subscriber | | hooks | { subscribe, close } -- ready-made WebSocket hooks. Destructure for one-line hooks.ws.js setup. |

Zero-config hooks

Instead of writing subscribe and close handlers manually, destructure presence.hooks:

// src/hooks.ws.js
import { presence } from '$lib/server/presence';
export const { subscribe, close } = presence.hooks;

subscribe handles both regular topics (calls join) and __presence:* topics (calls sync so the client gets the current list). close calls leave.

If you need custom logic (auth gating, logging), wrap the hooks:

import { presence } from '$lib/server/presence';

export async function subscribe(ws, topic, ctx) {
  if (!ctx.platform.getUserData(ws).authenticated) return;
  await presence.hooks.subscribe(ws, topic, ctx);
}

export const { close } = presence.hooks;

Rate limiting

Same API as the core createRateLimit plugin, but backed by Redis using an atomic Lua script. Rate limits are enforced across all server instances with exactly one Redis roundtrip per consume() call.

Setup

// src/lib/server/ratelimit.js
import { redis } from './redis.js';
import { createRateLimit } from 'svelte-adapter-uws-extensions/redis/ratelimit';

export const limiter = createRateLimit(redis, {
  points: 10,
  interval: 1000,
  blockDuration: 30000
});

Usage

// src/hooks.ws.js
import { limiter } from '$lib/server/ratelimit';

export async function message(ws, { data, platform }) {
  const { allowed } = await limiter.consume(ws);
  if (!allowed) return; // drop the message
  // ... handle message
}

Options

| Option | Default | Description | |---|---|---| | points | required | Tokens available per interval | | interval | required | Refill interval in ms | | blockDuration | 0 | Auto-ban duration in ms (0 = no ban) | | keyBy | 'ip' | 'ip', 'connection', or a function |

API

All methods are async (they hit Redis). The API otherwise matches the core plugin:

| Method | Description | |---|---| | consume(ws, cost?) | Attempt to consume tokens. cost must be a positive integer. | | reset(key) | Clear the bucket for a key | | ban(key, duration?) | Manually ban a key | | unban(key) | Remove a ban | | clear() | Reset all state |


Broadcast groups

Same API as the core createGroup plugin, but membership is stored in Redis so groups work across multiple server instances. Local WebSocket tracking is maintained per-instance, and cross-instance events are relayed via Redis pub/sub.

Setup

// src/lib/server/lobby.js
import { redis } from './redis.js';
import { createGroup } from 'svelte-adapter-uws-extensions/redis/groups';

export const lobby = createGroup(redis, 'lobby', {
  maxMembers: 50,
  meta: { game: 'chess' }
});

Note: the API signature is createGroup(client, name, options) instead of createGroup(name, options) -- the Redis client is the first argument.

Usage

// src/hooks.ws.js
import { lobby } from '$lib/server/lobby';

export async function subscribe(ws, topic, { platform }) {
  if (topic === 'lobby') await lobby.join(ws, platform);
}

export async function close(ws, { platform }) {
  await lobby.leave(ws, platform);
}

Options

| Option | Default | Description | |---|---|---| | maxMembers | Infinity | Maximum members allowed (enforced atomically) | | meta | {} | Initial group metadata | | memberTtl | 120 | Member entry TTL in seconds. Entries from crashed instances expire after this period. | | onJoin | - | Called after a member joins | | onLeave | - | Called after a member leaves | | onFull | - | Called when a join is rejected (full) | | onClose | - | Called when the group is closed |

API

| Method | Description | |---|---| | join(ws, platform, role?) | Add a member (returns false if full/closed) | | leave(ws, platform) | Remove a member | | publish(platform, event, data, role?) | Broadcast to all or filter by role | | send(platform, ws, event, data) | Send to a single member | | localMembers() | Members on this instance | | count() | Total members across all instances | | has(ws) | Check membership on this instance | | getMeta() / setMeta(meta) | Read/write group metadata | | close(platform) | Dissolve the group | | destroy() | Stop the Redis subscriber |


Cursor

Same API as the core createCursor plugin, but cursor positions are shared across instances via Redis. Each instance throttles locally (same leading/trailing edge logic as the core), then relays broadcasts through Redis pub/sub so subscribers on other instances see cursor updates.

Hash entries have a TTL so stale cursors from crashed instances get cleaned up automatically.

Setup

// src/lib/server/cursors.js
import { redis } from './redis.js';
import { createCursor } from 'svelte-adapter-uws-extensions/redis/cursor';

export const cursors = createCursor(redis, {
  throttle: 50,
  select: (userData) => ({ id: userData.id, name: userData.name, color: userData.color }),
  ttl: 30
});

Usage

// src/hooks.ws.js
import { cursors } from '$lib/server/cursors';

export function message(ws, { data, platform }) {
  const msg = JSON.parse(Buffer.from(data).toString());
  if (msg.type === 'cursor') {
    cursors.update(ws, msg.topic, msg.position, platform);
  }
}

export function close(ws, { platform }) {
  cursors.remove(ws, platform);
}

Options

| Option | Default | Description | |---|---|---| | throttle | 50 | Minimum ms between broadcasts per user per topic | | select | strips __-prefixed keys | Extract user data to broadcast alongside position | | ttl | 30 | Per-entry TTL in seconds (auto-refreshed on each broadcast). Stale entries from crashed instances are filtered out individually, even if other instances are still active on the same topic. |

API

| Method | Description | |---|---| | update(ws, topic, data, platform) | Broadcast cursor position (throttled per user per topic) | | remove(ws, platform, topic?) | Remove from a specific topic, or all topics if omitted | | list(topic) | Get current positions across all instances | | clear() | Reset all local and Redis state | | destroy() | Stop the Redis subscriber and clear timers |


Postgres extensions

Replay buffer (Postgres)

Same API as the Redis replay buffer, but backed by a Postgres table. Best suited for durable audit trails or history that needs to survive longer than Redis TTLs. Sequence numbers are generated atomically via a dedicated _seq table, so they are safe across multiple server instances.

Setup

// src/lib/server/replay.js
import { pg } from './pg.js';
import { createReplay } from 'svelte-adapter-uws-extensions/postgres/replay';

export const replay = createReplay(pg, {
  table: 'ws_replay',
  size: 1000,
  ttl: 86400,       // 24 hours
  autoMigrate: true  // auto-create table
});

Schema

The table is created automatically on first use (if autoMigrate is true):

CREATE TABLE IF NOT EXISTS ws_replay (
  id BIGSERIAL PRIMARY KEY,
  topic TEXT NOT NULL,
  seq BIGINT NOT NULL,
  event TEXT NOT NULL,
  data JSONB,
  created_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_ws_replay_topic_seq ON ws_replay (topic, seq);

CREATE TABLE IF NOT EXISTS ws_replay_seq (
  topic TEXT PRIMARY KEY,
  seq BIGINT NOT NULL DEFAULT 0
);

Options

| Option | Default | Description | |---|---|---| | table | 'ws_replay' | Table name | | size | 1000 | Max messages per topic | | ttl | 0 | Row expiry in seconds (0 = never) | | autoMigrate | true | Auto-create table | | cleanupInterval | 60000 | Periodic cleanup interval in ms (0 to disable) |

API

Same as Replay buffer (Redis), plus:

| Method | Description | |---|---| | destroy() | Stop the cleanup timer |


LISTEN/NOTIFY bridge

Listens on a Postgres channel for notifications and forwards them to platform.publish(). You provide the trigger on your table -- this module handles the listening side.

Uses a standalone connection (not from the pool) since LISTEN requires a persistent connection that stays open for the lifetime of the bridge.

Setup

// src/lib/server/notify.js
import { pg } from './pg.js';
import { createNotifyBridge } from 'svelte-adapter-uws-extensions/postgres/notify';

export const bridge = createNotifyBridge(pg, {
  channel: 'table_changes',
  parse: (payload) => {
    const row = JSON.parse(payload);
    return { topic: row.table, event: row.op, data: row.data };
  }
});

Usage

// src/hooks.ws.js
import { bridge } from '$lib/server/notify';

let activated = false;

export function open(ws, { platform }) {
  if (!activated) {
    activated = true;
    bridge.activate(platform);
  }
}

Setting up the trigger

Create a trigger function and attach it to your table:

CREATE OR REPLACE FUNCTION notify_table_change() RETURNS trigger AS $$
BEGIN
  PERFORM pg_notify('table_changes', json_build_object(
    'table', TG_TABLE_NAME,
    'op', lower(TG_OP),
    'data', CASE TG_OP
      WHEN 'DELETE' THEN row_to_json(OLD)
      ELSE row_to_json(NEW)
    END
  )::text);
  RETURN COALESCE(NEW, OLD);
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER messages_notify
  AFTER INSERT OR UPDATE OR DELETE ON messages
  FOR EACH ROW EXECUTE FUNCTION notify_table_change();

Now any INSERT, UPDATE, or DELETE on the messages table will fire a notification. The bridge parses it and calls platform.publish(), which reaches all connected WebSocket clients subscribed to the topic.

The client side needs no changes -- the core crud('messages') store already handles created, updated, and deleted events.

Options

| Option | Default | Description | |---|---|---| | channel | required | Postgres LISTEN channel name | | parse | JSON with { topic, event, data } | Parse notification payload into a publish call. Return null to skip. | | autoReconnect | true | Reconnect on connection loss | | reconnectInterval | 3000 | ms between reconnect attempts |

API

| Method | Description | |---|---| | activate(platform) | Start listening (idempotent) | | deactivate() | Stop listening and release the connection |

Limitations

  • Payload is limited to 8KB by Postgres. For large rows, send the row ID in the notification and let the client fetch the full row.
  • Only fires from triggers. Changes made outside your app (manual SQL, migrations) are invisible unless you add triggers for those tables too.
  • This is not logical replication. It is simpler, works on every Postgres provider, and needs no extensions or superuser access.

Observability

Prometheus metrics

Exposes extension metrics in Prometheus text exposition format. No external dependencies. Zero overhead when not enabled -- every metric call uses optional chaining on a nullish reference, so V8 short-circuits on a single pointer check.

Setup

// src/lib/server/metrics.js
import { createMetrics } from 'svelte-adapter-uws-extensions/prometheus';

export const metrics = createMetrics({
  prefix: 'myapp_',
  mapTopic: (topic) => topic.startsWith('room:') ? 'room:*' : topic
});

Pass the metrics object to any extension via its options:

import { metrics } from './metrics.js';
import { redis } from './redis.js';
import { createPresence } from 'svelte-adapter-uws-extensions/redis/presence';
import { createPubSubBus } from 'svelte-adapter-uws-extensions/redis/pubsub';
import { createReplay } from 'svelte-adapter-uws-extensions/redis/replay';
import { createRateLimit } from 'svelte-adapter-uws-extensions/redis/ratelimit';
import { createGroup } from 'svelte-adapter-uws-extensions/redis/groups';
import { createCursor } from 'svelte-adapter-uws-extensions/redis/cursor';

export const bus = createPubSubBus(redis, { metrics });
export const presence = createPresence(redis, { metrics, key: 'id' });
export const replay = createReplay(redis, { metrics });
export const limiter = createRateLimit(redis, { points: 10, interval: 1000, metrics });
export const lobby = createGroup(redis, 'lobby', { metrics });
export const cursors = createCursor(redis, { metrics });

Mounting the endpoint

With uWebSockets.js:

app.get('/metrics', metrics.handler);

Or use metrics.serialize() to get the raw text and serve it however you like.

Options

| Option | Default | Description | |---|---|---| | prefix | '' | Prefix for all metric names | | mapTopic | identity | Map topic names to bounded label values for cardinality control | | defaultBuckets | [1, 5, 10, 25, 50, 100, 250, 500, 1000] | Default histogram buckets |

Metric names must match [a-zA-Z_:][a-zA-Z0-9_:]* and label names must match [a-zA-Z_][a-zA-Z0-9_]* (no __ prefix). Invalid names throw at registration time. HELP text containing backslashes or newlines is escaped automatically.

Cardinality control

If your topics are user-generated (e.g. room:abc123), per-topic labels will grow unbounded. Use mapTopic to collapse them:

const metrics = createMetrics({
  mapTopic: (topic) => {
    if (topic.startsWith('room:')) return 'room:*';
    if (topic.startsWith('user:')) return 'user:*';
    return topic;
  }
});

Metrics reference

Pub/sub bus

| Metric | Type | Description | |---|---|---| | pubsub_messages_relayed_total | counter | Messages relayed to Redis | | pubsub_messages_received_total | counter | Messages received from Redis | | pubsub_echo_suppressed_total | counter | Messages dropped by echo suppression | | pubsub_relay_batch_size | histogram | Relay batch size per flush |

Presence

| Metric | Type | Labels | Description | |---|---|---|---| | presence_joins_total | counter | topic | Join events | | presence_leaves_total | counter | topic | Leave events | | presence_heartbeats_total | counter | | Heartbeat refresh cycles | | presence_stale_cleaned_total | counter | | Stale entries removed by cleanup |

Replay buffer (Redis and Postgres)

| Metric | Type | Labels | Description | |---|---|---|---| | replay_publishes_total | counter | topic | Messages published | | replay_messages_replayed_total | counter | topic | Messages replayed to clients | | replay_truncations_total | counter | topic | Truncation events detected |

Rate limiting

| Metric | Type | Description | |---|---|---| | ratelimit_allowed_total | counter | Requests allowed | | ratelimit_denied_total | counter | Requests denied | | ratelimit_bans_total | counter | Bans applied |

Broadcast groups

| Metric | Type | Labels | Description | |---|---|---|---| | group_joins_total | counter | group | Join events | | group_joins_rejected_total | counter | group | Joins rejected (full) | | group_leaves_total | counter | group | Leave events | | group_publishes_total | counter | group | Publish events |

Cursor

| Metric | Type | Labels | Description | |---|---|---|---| | cursor_updates_total | counter | topic | Cursor update calls | | cursor_broadcasts_total | counter | topic | Broadcasts actually sent | | cursor_throttled_total | counter | topic | Updates deferred by throttle |

LISTEN/NOTIFY bridge

| Metric | Type | Labels | Description | |---|---|---|---| | notify_received_total | counter | channel | Notifications received | | notify_parse_errors_total | counter | channel | Parse failures | | notify_reconnects_total | counter | | Reconnect attempts |


Reliability

Circuit breaker

Prevents thundering herd when a backend goes down. When Redis or Postgres becomes unreachable, every extension that uses the breaker fails fast instead of queueing up timeouts, and fire-and-forget operations (heartbeats, relay flushes, cursor broadcasts) are skipped entirely.

Three states:

  • healthy -- everything works, requests go through
  • broken -- too many failures, requests fail fast via CircuitBrokenError
  • probing -- one request is allowed through to test if the backend is back

Setup

// src/lib/server/breaker.js
import { createCircuitBreaker } from 'svelte-adapter-uws-extensions/breaker';

export const breaker = createCircuitBreaker({
  failureThreshold: 5,
  resetTimeout: 30000,
  onStateChange: (from, to) => console.log(`circuit: ${from} -> ${to}`)
});

Pass the same breaker to all extensions that share a backend:

import { breaker } from './breaker.js';

export const bus = createPubSubBus(redis, { breaker });
export const presence = createPresence(redis, { breaker, key: 'id' });
export const replay = createReplay(redis, { breaker });
export const limiter = createRateLimit(redis, { points: 10, interval: 1000, breaker });

Failures from any extension contribute to the same breaker. When one trips it, all others fail fast.

Options

| Option | Default | Description | |---|---|---| | failureThreshold | 5 | Consecutive failures before breaking | | resetTimeout | 30000 | Ms before transitioning from broken to probing | | onStateChange | - | Called on state transitions: (from, to) => void |

API

| Method / Property | Description | |---|---| | breaker.state | 'healthy', 'broken', or 'probing' | | breaker.isHealthy | true only when state is 'healthy' | | breaker.failures | Current consecutive failure count | | breaker.guard() | Throws CircuitBrokenError if the circuit is broken | | breaker.success() | Record a successful operation | | breaker.failure() | Record a failed operation | | breaker.reset() | Force back to healthy | | breaker.destroy() | Clear internal timers |

How extensions use it

Awaited operations (join, consume, publish) call guard() before the Redis/Postgres call, success() after, and failure() in the catch block. When the circuit is broken, guard() throws CircuitBrokenError and the operation never reaches the backend.

Fire-and-forget operations (heartbeat refresh, relay flush, cursor broadcast) check isHealthy and skip entirely when the circuit is not healthy. This prevents piling up commands on a dead connection.

Error handling

import { CircuitBrokenError } from 'svelte-adapter-uws-extensions/breaker';

try {
  await replay.publish(platform, 'chat', 'msg', data);
} catch (err) {
  if (err instanceof CircuitBrokenError) {
    // Backend is down -- degrade gracefully
    platform.publish('chat', 'msg', data); // local-only delivery
  }
}

Operations

Graceful shutdown

All clients listen for the sveltekit:shutdown event and disconnect cleanly by default. You can disable this with autoShutdown: false and manage the lifecycle yourself.

// Manual shutdown
await redis.quit();
await pg.end();
presence.destroy();

Testing

npm test

Tests use in-memory mocks for Redis and Postgres, no running services needed.


Related projects

  • svelte-adapter-uws -- The core adapter this package extends. Single-process WebSocket pub/sub, presence, replay, and more for SvelteKit on uWebSockets.js.
  • svelte-realtime -- Opinionated full-stack starter built on the adapter. Auth, database, real-time CRUD, and deployment config out of the box.
  • svelte-realtime-demo -- Live demo of svelte-realtime. Try it here.

License

MIT