svelte-realtime 0.4.21 → 0.4.22
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 +48 -0
- package/client.d.ts +16 -0
- package/client.js +38 -6
- package/package.json +2 -2
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
|
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.22",
|
|
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,7 +69,7 @@
|
|
|
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
75
|
"vitest": "^4.0.18"
|