svelte-realtime 0.4.21 → 0.4.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -5
- package/client.d.ts +16 -0
- package/client.js +38 -6
- package/package.json +3 -2
- package/server.d.ts +35 -8
- package/server.js +12 -5
package/README.md
CHANGED
|
@@ -934,6 +934,7 @@ Call `configure()` once at app startup. The hooks fire on state transitions only
|
|
|
934
934
|
| Option | Description |
|
|
935
935
|
|---|---|
|
|
936
936
|
| `url` | Full WebSocket URL for cross-origin or native app usage (e.g. `'wss://api.example.com/ws'`) |
|
|
937
|
+
| `auth` | `true` (or a custom path) to enable an HTTP preflight before each WebSocket upgrade so cookies set by the server's `authenticate` hook ride a normal HTTP response. Required behind Cloudflare Tunnel and other proxies that drop `Set-Cookie` on 101 responses. Requires `svelte-adapter-uws` >= 0.4.12. |
|
|
937
938
|
| `onConnect()` | Called when the WebSocket connection opens after a reconnect |
|
|
938
939
|
| `onDisconnect()` | Called when the WebSocket connection closes |
|
|
939
940
|
| `beforeReconnect()` | Called before each reconnection attempt (can be async) |
|
|
@@ -985,6 +986,53 @@ The native client passes the token in the URL:
|
|
|
985
986
|
configure({ url: 'wss://my-sveltekit-app.com/ws?token=...' });
|
|
986
987
|
```
|
|
987
988
|
|
|
989
|
+
### Refreshing session cookies on connect (Cloudflare Tunnel and friends)
|
|
990
|
+
|
|
991
|
+
Cloudflare Tunnel and other strict edge proxies silently drop the `Set-Cookie` header on WebSocket `101 Switching Protocols` responses. The connection appears to open server-side, then the client immediately sees `close 1006` and never receives a single frame. The classic symptom for this in production: WebSockets work locally and on a bare server, then break the moment you put Cloudflare in front.
|
|
992
|
+
|
|
993
|
+
Fix it in three pieces:
|
|
994
|
+
|
|
995
|
+
1. Export an `authenticate` hook from `src/hooks.ws.{js,ts}`. It runs as a normal HTTP `POST /__ws/auth` before every upgrade (including reconnects), so cookies you set ride a `204 No Content` response that proxies route correctly.
|
|
996
|
+
2. Opt into the client preflight with `configure({ auth: true })`.
|
|
997
|
+
3. Use `svelte-adapter-uws` >= 0.4.12.
|
|
998
|
+
|
|
999
|
+
```js
|
|
1000
|
+
// src/hooks.ws.js
|
|
1001
|
+
export { message, close, unsubscribe } from 'svelte-realtime/server';
|
|
1002
|
+
|
|
1003
|
+
export function upgrade({ cookies }) {
|
|
1004
|
+
const session = validateSession(cookies.session_id);
|
|
1005
|
+
return session ? { id: session.userId, name: session.name } : false;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
export function authenticate({ cookies }) {
|
|
1009
|
+
const session = validateSession(cookies.get('session_id'));
|
|
1010
|
+
if (!session) return false;
|
|
1011
|
+
|
|
1012
|
+
if (shouldRotate(session)) {
|
|
1013
|
+
cookies.set('session_id', rotate(session), {
|
|
1014
|
+
httpOnly: true,
|
|
1015
|
+
secure: true,
|
|
1016
|
+
sameSite: 'lax',
|
|
1017
|
+
path: '/'
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
return { id: session.userId, name: session.name };
|
|
1021
|
+
}
|
|
1022
|
+
```
|
|
1023
|
+
|
|
1024
|
+
```svelte
|
|
1025
|
+
<!-- src/routes/+layout.svelte -->
|
|
1026
|
+
<script>
|
|
1027
|
+
import { configure } from 'svelte-realtime/client';
|
|
1028
|
+
configure({ auth: true });
|
|
1029
|
+
</script>
|
|
1030
|
+
```
|
|
1031
|
+
|
|
1032
|
+
The client coalesces concurrent connects into a single in-flight preflight, treats `4xx` as terminal, and falls back to normal reconnect backoff on `5xx` and network errors.
|
|
1033
|
+
|
|
1034
|
+
> **Detector:** if the client sees two consecutive WebSocket open->close cycles inside one second with no traffic, it logs a one-shot `console.warn` pointing at this section. That is the Cloudflare-Tunnel-eating-cookies fingerprint.
|
|
1035
|
+
|
|
988
1036
|
---
|
|
989
1037
|
|
|
990
1038
|
## Combine stores
|
|
@@ -1171,22 +1219,51 @@ export const message = createMessage({
|
|
|
1171
1219
|
|
|
1172
1220
|
Opt-in instrumentation for RPC calls, stream subscriptions, and cron executions. Zero overhead if not called.
|
|
1173
1221
|
|
|
1222
|
+
`live.metrics(registry)` is a one-time setup call. The top of `src/hooks.ws.{js,ts}` is a natural place, since it loads once when the server boots. Pair it with the `createMetrics()` registry from `svelte-adapter-uws-extensions/prometheus`:
|
|
1223
|
+
|
|
1174
1224
|
```js
|
|
1225
|
+
// src/hooks.ws.js
|
|
1175
1226
|
import { live } from 'svelte-realtime/server';
|
|
1176
|
-
import {
|
|
1227
|
+
import { createMetrics } from 'svelte-adapter-uws-extensions/prometheus';
|
|
1228
|
+
|
|
1229
|
+
const metrics = createMetrics();
|
|
1230
|
+
|
|
1231
|
+
live.metrics({
|
|
1232
|
+
counter: ({ name, help, labelNames }) => metrics.counter(name, help, labelNames),
|
|
1233
|
+
histogram: ({ name, help, labelNames }) => metrics.histogram(name, help, labelNames),
|
|
1234
|
+
gauge: ({ name, help, labelNames }) => metrics.gauge(name, help, labelNames)
|
|
1235
|
+
});
|
|
1177
1236
|
|
|
1178
|
-
|
|
1179
|
-
|
|
1237
|
+
export { message, close, unsubscribe } from 'svelte-realtime/server';
|
|
1238
|
+
```
|
|
1239
|
+
|
|
1240
|
+
Mount the metrics endpoint on your uWS app (typically in `svelte.config.js` or wherever you build the app):
|
|
1241
|
+
|
|
1242
|
+
```js
|
|
1243
|
+
app.get('/metrics', metrics.handler);
|
|
1180
1244
|
```
|
|
1181
1245
|
|
|
1182
|
-
|
|
1246
|
+
The six-line shim adapts realtime's options-object call shape to the extensions registry's positional create methods. Once a metric is registered, every increment, observation, and gauge update flows directly to the extensions registry, so the emitted Prometheus output is exactly what `metrics.serialize()` produces.
|
|
1247
|
+
|
|
1248
|
+
### Registered metrics
|
|
1249
|
+
|
|
1183
1250
|
- `svelte_realtime_rpc_total` -- RPC call count by path and status
|
|
1184
1251
|
- `svelte_realtime_rpc_duration_seconds` -- RPC latency by path
|
|
1185
1252
|
- `svelte_realtime_rpc_errors_total` -- RPC errors by path and code
|
|
1186
|
-
- `svelte_realtime_stream_subscriptions` -- active stream subscription gauge
|
|
1253
|
+
- `svelte_realtime_stream_subscriptions` -- active stream subscription gauge
|
|
1187
1254
|
- `svelte_realtime_cron_total` -- cron execution count by path and status
|
|
1188
1255
|
- `svelte_realtime_cron_errors_total` -- cron errors by path
|
|
1189
1256
|
|
|
1257
|
+
### Registry shape
|
|
1258
|
+
|
|
1259
|
+
`live.metrics()` accepts any object exposing:
|
|
1260
|
+
|
|
1261
|
+
- `counter({ name, help, labelNames }) -> { inc(labels?) }`
|
|
1262
|
+
- `histogram({ name, help, labelNames }) -> { observe(labels, valueSeconds) }`
|
|
1263
|
+
- `gauge({ name, help }) -> { inc(), dec() }`
|
|
1264
|
+
|
|
1265
|
+
If you prefer `prom-client`, wire it the same way: `counter: ({ name, help, labelNames }) => new client.Counter({ name, help, labelNames, registers: [register] })`, and likewise for `Histogram` and `Gauge`.
|
|
1266
|
+
|
|
1190
1267
|
---
|
|
1191
1268
|
|
|
1192
1269
|
## Circuit breaker
|
package/client.d.ts
CHANGED
|
@@ -181,6 +181,22 @@ export function configure(config: {
|
|
|
181
181
|
* @example 'wss://my-app.com/ws'
|
|
182
182
|
*/
|
|
183
183
|
url?: string;
|
|
184
|
+
/**
|
|
185
|
+
* Run an HTTP preflight before each WebSocket upgrade so cookies set by the
|
|
186
|
+
* server's `authenticate` hook ride a normal HTTP response (not a 101 Switching
|
|
187
|
+
* Protocols frame). Required behind Cloudflare Tunnel and other strict edge
|
|
188
|
+
* proxies that silently drop `Set-Cookie` on WebSocket upgrades.
|
|
189
|
+
*
|
|
190
|
+
* Pass `true` to use the default `/__ws/auth` path, or a string to override it
|
|
191
|
+
* (must match the adapter's `websocket.authPath` option).
|
|
192
|
+
*
|
|
193
|
+
* Requires `svelte-adapter-uws` >= 0.4.12 and an `authenticate` export in
|
|
194
|
+
* `src/hooks.ws.{js,ts}`.
|
|
195
|
+
*
|
|
196
|
+
* @default false
|
|
197
|
+
* @example configure({ auth: true })
|
|
198
|
+
*/
|
|
199
|
+
auth?: boolean | string;
|
|
184
200
|
/** Called when the WebSocket connection opens (not on initial connect, only reconnects). */
|
|
185
201
|
onConnect?(): void;
|
|
186
202
|
/** Called when the WebSocket connection closes. */
|
package/client.js
CHANGED
|
@@ -133,22 +133,50 @@ function ensureListener() {
|
|
|
133
133
|
/**
|
|
134
134
|
* Attach a disconnect listener once.
|
|
135
135
|
* Rejects all in-flight RPCs (already sent) with DISCONNECTED.
|
|
136
|
+
* Also detects the Cloudflare-Tunnel "Set-Cookie on 101" symptom: repeated
|
|
137
|
+
* fast open->close cycles with no time spent in the open state.
|
|
136
138
|
*/
|
|
137
139
|
function ensureDisconnectListener() {
|
|
138
140
|
if (disconnectListenerAttached) return;
|
|
139
141
|
disconnectListenerAttached = true;
|
|
140
142
|
|
|
143
|
+
let lastOpenAt = 0;
|
|
144
|
+
let fastCloseCount = 0;
|
|
145
|
+
let cfTunnelWarned = false;
|
|
146
|
+
|
|
141
147
|
status.subscribe((s) => {
|
|
142
148
|
if (s === 'closed') {
|
|
143
|
-
const code = s === 'closed' ? 'DISCONNECTED' : 'CONNECTION_CLOSED';
|
|
144
149
|
for (const [id, entry] of pending) {
|
|
145
150
|
pending.delete(id);
|
|
146
151
|
if (entry.timer) clearTimeout(entry.timer);
|
|
147
|
-
entry.reject(new RpcError(
|
|
152
|
+
entry.reject(new RpcError('DISCONNECTED', 'WebSocket connection lost'));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (lastOpenAt > 0) {
|
|
156
|
+
const openDuration = Date.now() - lastOpenAt;
|
|
157
|
+
lastOpenAt = 0;
|
|
158
|
+
if (openDuration < 1000) {
|
|
159
|
+
fastCloseCount++;
|
|
160
|
+
if (fastCloseCount >= 2 && !cfTunnelWarned && !_clientConfig.auth) {
|
|
161
|
+
cfTunnelWarned = true;
|
|
162
|
+
console.warn(
|
|
163
|
+
'[svelte-realtime] WebSocket opened then closed in ' + openDuration + 'ms ' +
|
|
164
|
+
'with no traffic, repeatedly. This is the classic Cloudflare-Tunnel ' +
|
|
165
|
+
'"Set-Cookie on 101" symptom: the proxy silently drops cookies on ' +
|
|
166
|
+
'WebSocket upgrade responses.\n' +
|
|
167
|
+
' Fix: add `configure({ auth: true })` on the client and an ' +
|
|
168
|
+
'`authenticate` hook in `hooks.ws.js` (svelte-adapter-uws >= 0.4.12).\n' +
|
|
169
|
+
' See: https://svti.me/cf-cookies'
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
fastCloseCount = 0;
|
|
174
|
+
}
|
|
148
175
|
}
|
|
149
176
|
}
|
|
150
177
|
if (s === 'open') {
|
|
151
178
|
_terminated = false;
|
|
179
|
+
lastOpenAt = Date.now();
|
|
152
180
|
}
|
|
153
181
|
});
|
|
154
182
|
|
|
@@ -1559,7 +1587,7 @@ function _checkArgs(path, args) {
|
|
|
1559
1587
|
* @typedef {{ path: string, args: any[], queuedAt: number, resolve: Function, reject: Function }} OfflineEntry
|
|
1560
1588
|
*/
|
|
1561
1589
|
|
|
1562
|
-
/** @type {{ url?: string, onConnect?: () => void, onDisconnect?: () => void, timeout?: number, offline?: { queue?: boolean, maxQueue?: number, maxAge?: number, replay?: 'sequential' | 'batch' | ((queue: OfflineEntry[]) => OfflineEntry[]), beforeReplay?: (call: { path: string, args: any[], queuedAt: number }) => boolean, onReplayError?: (call: { path: string, args: any[], queuedAt: number }, error: any) => void } }} */
|
|
1590
|
+
/** @type {{ url?: string, auth?: boolean | string, onConnect?: () => void, onDisconnect?: () => void, timeout?: number, offline?: { queue?: boolean, maxQueue?: number, maxAge?: number, replay?: 'sequential' | 'batch' | ((queue: OfflineEntry[]) => OfflineEntry[]), beforeReplay?: (call: { path: string, args: any[], queuedAt: number }) => boolean, onReplayError?: (call: { path: string, args: any[], queuedAt: number }, error: any) => void } }} */
|
|
1563
1591
|
let _clientConfig = {};
|
|
1564
1592
|
|
|
1565
1593
|
/** @type {boolean} */
|
|
@@ -1577,13 +1605,17 @@ let _replayingQueue = false;
|
|
|
1577
1605
|
/**
|
|
1578
1606
|
* Configure client-side connection hooks and offline queue.
|
|
1579
1607
|
*
|
|
1580
|
-
* @param {{ url?: string, onConnect?: () => void, onDisconnect?: () => void, offline?: { queue?: boolean, maxQueue?: number, maxAge?: number, replay?: 'sequential' | 'batch' | ((queue: OfflineEntry[]) => OfflineEntry[]), beforeReplay?: (call: { path: string, args: any[], queuedAt: number }) => boolean, onReplayError?: (call: { path: string, args: any[], queuedAt: number }, error: any) => void } }} config
|
|
1608
|
+
* @param {{ url?: string, auth?: boolean | string, onConnect?: () => void, onDisconnect?: () => void, offline?: { queue?: boolean, maxQueue?: number, maxAge?: number, replay?: 'sequential' | 'batch' | ((queue: OfflineEntry[]) => OfflineEntry[]), beforeReplay?: (call: { path: string, args: any[], queuedAt: number }) => boolean, onReplayError?: (call: { path: string, args: any[], queuedAt: number }, error: any) => void } }} config
|
|
1581
1609
|
*/
|
|
1582
1610
|
export function configure(config) {
|
|
1583
1611
|
_clientConfig = config;
|
|
1584
1612
|
|
|
1585
|
-
if (config.url) {
|
|
1586
|
-
|
|
1613
|
+
if (config.url !== undefined || config.auth !== undefined) {
|
|
1614
|
+
/** @type {{ url?: string, auth?: boolean | string }} */
|
|
1615
|
+
const connectArgs = {};
|
|
1616
|
+
if (config.url !== undefined) connectArgs.url = config.url;
|
|
1617
|
+
if (config.auth !== undefined) connectArgs.auth = config.auth;
|
|
1618
|
+
_connect(connectArgs);
|
|
1587
1619
|
}
|
|
1588
1620
|
|
|
1589
1621
|
if (!_configListenerAttached) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "svelte-realtime",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.23",
|
|
4
4
|
"description": "Realtime RPC and reactive subscriptions for SvelteKit, built on svelte-adapter-uws",
|
|
5
5
|
"author": "Kevin Radziszewski",
|
|
6
6
|
"license": "MIT",
|
|
@@ -69,9 +69,10 @@
|
|
|
69
69
|
"peerDependencies": {
|
|
70
70
|
"@sveltejs/kit": "^2.0.0",
|
|
71
71
|
"svelte": "^4.0.0 || ^5.0.0",
|
|
72
|
-
"svelte-adapter-uws": ">=0.4.
|
|
72
|
+
"svelte-adapter-uws": ">=0.4.12"
|
|
73
73
|
},
|
|
74
74
|
"devDependencies": {
|
|
75
|
+
"svelte-adapter-uws-extensions": "^0.4.2",
|
|
75
76
|
"vitest": "^4.0.18"
|
|
76
77
|
},
|
|
77
78
|
"keywords": [
|
package/server.d.ts
CHANGED
|
@@ -182,6 +182,25 @@ export interface CreateMessageOptions {
|
|
|
182
182
|
onUnhandled?(ws: WebSocket<any>, data: ArrayBuffer, platform: Platform): void;
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
+
/**
|
|
186
|
+
* Shape accepted by `live.metrics()`. Any object matching this contract works
|
|
187
|
+
* (hand-rolled, prom-client adapter, or the `createMetrics()` registry from
|
|
188
|
+
* `svelte-adapter-uws-extensions/prometheus` wrapped to forward options as
|
|
189
|
+
* positional args -- see the README "Prometheus metrics" section).
|
|
190
|
+
*/
|
|
191
|
+
export interface MetricsRegistry {
|
|
192
|
+
counter(opts: { name: string; help: string; labelNames?: string[] }): {
|
|
193
|
+
inc(labels?: Record<string, string | number>): void;
|
|
194
|
+
};
|
|
195
|
+
histogram(opts: { name: string; help: string; labelNames?: string[]; buckets?: number[] }): {
|
|
196
|
+
observe(labels: Record<string, string | number>, valueSeconds: number): void;
|
|
197
|
+
};
|
|
198
|
+
gauge(opts: { name: string; help: string; labelNames?: string[] }): {
|
|
199
|
+
inc(): void;
|
|
200
|
+
dec(): void;
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
185
204
|
/**
|
|
186
205
|
* Mark a function as RPC-callable over WebSocket.
|
|
187
206
|
*
|
|
@@ -561,21 +580,29 @@ export namespace live {
|
|
|
561
580
|
function webhook(topic: string, config: WebhookConfig): WebhookHandler;
|
|
562
581
|
|
|
563
582
|
/**
|
|
564
|
-
* Opt-in Prometheus metrics integration.
|
|
565
|
-
*
|
|
566
|
-
* and instruments RPC calls, stream subscriptions, and cron executions.
|
|
583
|
+
* Opt-in Prometheus metrics integration. Instruments RPC calls, stream
|
|
584
|
+
* subscriptions, and cron executions. Zero overhead if never called.
|
|
567
585
|
*
|
|
568
|
-
*
|
|
586
|
+
* Call once at server start (e.g. the top of `src/hooks.ws.{js,ts}`).
|
|
587
|
+
* See the README "Prometheus metrics" section for a working example
|
|
588
|
+
* that pairs this with `createMetrics()` from
|
|
589
|
+
* `svelte-adapter-uws-extensions/prometheus`.
|
|
569
590
|
*
|
|
570
|
-
* @param registry -
|
|
591
|
+
* @param registry - Object matching the {@link MetricsRegistry} shape
|
|
571
592
|
*
|
|
572
593
|
* @example
|
|
573
594
|
* ```js
|
|
574
|
-
* import {
|
|
575
|
-
*
|
|
595
|
+
* import { createMetrics } from 'svelte-adapter-uws-extensions/prometheus';
|
|
596
|
+
*
|
|
597
|
+
* const metrics = createMetrics();
|
|
598
|
+
* live.metrics({
|
|
599
|
+
* counter: ({ name, help, labelNames }) => metrics.counter(name, help, labelNames),
|
|
600
|
+
* histogram: ({ name, help, labelNames }) => metrics.histogram(name, help, labelNames),
|
|
601
|
+
* gauge: ({ name, help, labelNames }) => metrics.gauge(name, help, labelNames)
|
|
602
|
+
* });
|
|
576
603
|
* ```
|
|
577
604
|
*/
|
|
578
|
-
function metrics(registry:
|
|
605
|
+
function metrics(registry: MetricsRegistry): void;
|
|
579
606
|
|
|
580
607
|
/**
|
|
581
608
|
* Wrap a stream initFn call with a circuit breaker.
|
package/server.js
CHANGED
|
@@ -1338,13 +1338,20 @@ live.room = function room(config) {
|
|
|
1338
1338
|
let _metricsInstruments = null;
|
|
1339
1339
|
|
|
1340
1340
|
/**
|
|
1341
|
-
* Opt-in Prometheus metrics integration.
|
|
1342
|
-
*
|
|
1343
|
-
* and instruments RPC calls, stream subscriptions, and cron executions.
|
|
1341
|
+
* Opt-in Prometheus metrics integration. Instruments RPC calls, stream
|
|
1342
|
+
* subscriptions, and cron executions. Zero overhead if never called.
|
|
1344
1343
|
*
|
|
1345
|
-
*
|
|
1344
|
+
* Call once at server start (e.g. the top of `src/hooks.ws.{js,ts}`).
|
|
1346
1345
|
*
|
|
1347
|
-
*
|
|
1346
|
+
* The registry is any object exposing:
|
|
1347
|
+
* counter({ name, help, labelNames }) -> { inc(labels?) }
|
|
1348
|
+
* histogram({ name, help, labelNames }) -> { observe(labels, valueSeconds) }
|
|
1349
|
+
* gauge({ name, help }) -> { inc(), dec() }
|
|
1350
|
+
*
|
|
1351
|
+
* See the README "Prometheus metrics" section for a working example that
|
|
1352
|
+
* pairs this with `createMetrics()` from `svelte-adapter-uws-extensions/prometheus`.
|
|
1353
|
+
*
|
|
1354
|
+
* @param {any} registry - Object with counter, histogram, and gauge factories
|
|
1348
1355
|
*/
|
|
1349
1356
|
live.metrics = function metrics(registry) {
|
|
1350
1357
|
_metricsInstruments = {
|