svelte-realtime 0.4.20 → 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 +112 -18
- package/client.d.ts +23 -1
- package/client.js +76 -11
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -147,8 +147,6 @@ export const messages = live.stream('messages', async (ctx) => {
|
|
|
147
147
|
|
|
148
148
|
{#if $messages === undefined}
|
|
149
149
|
<p>Loading...</p>
|
|
150
|
-
{:else if $messages?.error}
|
|
151
|
-
<p>Failed to load: {$messages.error.message}</p>
|
|
152
150
|
{:else}
|
|
153
151
|
{#each $messages as msg (msg.id)}
|
|
154
152
|
<p><b>{msg.userId}:</b> {msg.text}</p>
|
|
@@ -378,21 +376,19 @@ When the WebSocket reconnects, streams automatically refetch initial data and re
|
|
|
378
376
|
|
|
379
377
|
## Error handling
|
|
380
378
|
|
|
381
|
-
|
|
379
|
+
The data store value never changes shape. It is always your data type or `undefined` while loading. Errors and connection status live on separate reactive stores so a network failure can never crash your UI:
|
|
382
380
|
|
|
383
|
-
|
|
|
384
|
-
|
|
385
|
-
| `undefined` |
|
|
386
|
-
| `
|
|
387
|
-
| `
|
|
381
|
+
| Property | Type | Description |
|
|
382
|
+
|---|---|---|
|
|
383
|
+
| `$store` | `T \| undefined` | Your data. Never replaced by an error object. On failure, the last loaded value is preserved. |
|
|
384
|
+
| `store.error` | `Readable<RpcError \| null>` | Current error, or `null` when healthy. |
|
|
385
|
+
| `store.status` | `Readable<'loading' \| 'connected' \| 'reconnecting' \| 'error'>` | Connection status. |
|
|
388
386
|
|
|
389
|
-
Handle
|
|
387
|
+
Handle loading in your template:
|
|
390
388
|
|
|
391
389
|
```svelte
|
|
392
390
|
{#if $messages === undefined}
|
|
393
391
|
<p>Loading...</p>
|
|
394
|
-
{:else if $messages?.error}
|
|
395
|
-
<p>Failed: {$messages.error.message}</p>
|
|
396
392
|
{:else}
|
|
397
393
|
{#each $messages as msg (msg.id)}
|
|
398
394
|
<p>{msg.text}</p>
|
|
@@ -400,6 +396,27 @@ Handle all three in your template:
|
|
|
400
396
|
{/if}
|
|
401
397
|
```
|
|
402
398
|
|
|
399
|
+
To show errors, subscribe to the `.error` store:
|
|
400
|
+
|
|
401
|
+
```svelte
|
|
402
|
+
<script>
|
|
403
|
+
import { messages } from '$live/chat';
|
|
404
|
+
|
|
405
|
+
const err = messages.error;
|
|
406
|
+
const status = messages.status;
|
|
407
|
+
</script>
|
|
408
|
+
|
|
409
|
+
{#if $err}
|
|
410
|
+
<p>Error: {$err.message} ({$err.code})</p>
|
|
411
|
+
{/if}
|
|
412
|
+
|
|
413
|
+
{#if $status === 'reconnecting'}
|
|
414
|
+
<p>Reconnecting...</p>
|
|
415
|
+
{/if}
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
Defensive patterns like `($store ?? []).filter(...)` work correctly because `$store` is always an array or `undefined`.
|
|
419
|
+
|
|
403
420
|
For RPC calls, errors are thrown as `RpcError` with a `code` field:
|
|
404
421
|
|
|
405
422
|
```js
|
|
@@ -421,22 +438,23 @@ try {
|
|
|
421
438
|
When the adapter's `ready()` promise rejects (terminal close codes 1008, 4401, 4403, exhausted retries, or explicit `close()`), svelte-realtime:
|
|
422
439
|
|
|
423
440
|
- Rejects all pending RPCs immediately with `RpcError('CONNECTION_CLOSED', ...)`
|
|
424
|
-
- Sets
|
|
441
|
+
- Sets `.error` on all active stream stores (the data value is preserved)
|
|
425
442
|
- Drains the offline queue with errors
|
|
426
443
|
|
|
427
444
|
RPCs called after a terminal close reject immediately without sending.
|
|
428
445
|
|
|
429
446
|
### Reusable error boundary component
|
|
430
447
|
|
|
431
|
-
For Svelte 5, you can build a reusable boundary that handles
|
|
448
|
+
For Svelte 5, you can build a reusable boundary that handles loading and error states:
|
|
432
449
|
|
|
433
450
|
```svelte
|
|
434
451
|
<!-- src/lib/StreamView.svelte -->
|
|
435
452
|
<script>
|
|
436
|
-
/** @type {{ store:
|
|
453
|
+
/** @type {{ store: any, children: import('svelte').Snippet, loading?: import('svelte').Snippet, error?: import('svelte').Snippet<[any]> }} */
|
|
437
454
|
let { store, children, loading, error } = $props();
|
|
438
455
|
|
|
439
456
|
let value = $derived($store);
|
|
457
|
+
const err = store.error;
|
|
440
458
|
</script>
|
|
441
459
|
|
|
442
460
|
{#if value === undefined}
|
|
@@ -445,11 +463,11 @@ For Svelte 5, you can build a reusable boundary that handles all three stream st
|
|
|
445
463
|
{:else}
|
|
446
464
|
<p>Loading...</p>
|
|
447
465
|
{/if}
|
|
448
|
-
{:else if
|
|
466
|
+
{:else if $err}
|
|
449
467
|
{#if error}
|
|
450
|
-
{@render error(
|
|
468
|
+
{@render error($err)}
|
|
451
469
|
{:else}
|
|
452
|
-
<p>Error: {
|
|
470
|
+
<p>Error: {$err.message}</p>
|
|
453
471
|
{/if}
|
|
454
472
|
{:else}
|
|
455
473
|
{@render children()}
|
|
@@ -492,7 +510,7 @@ With default slots, the minimal version is just:
|
|
|
492
510
|
</StreamView>
|
|
493
511
|
```
|
|
494
512
|
|
|
495
|
-
This removes the `{#if}/{:else
|
|
513
|
+
This removes the `{#if}/{:else}` boilerplate from every page that uses a stream.
|
|
496
514
|
|
|
497
515
|
---
|
|
498
516
|
|
|
@@ -673,6 +691,32 @@ export async function load({ platform, locals }) {
|
|
|
673
691
|
|
|
674
692
|
The hydrated store still subscribes for live updates on first render. It keeps the SSR data visible instead of showing `undefined` during the initial fetch. Guards still run during `.load()` calls. Pass `{ user }` as the second argument if your guard or init function needs user data.
|
|
675
693
|
|
|
694
|
+
For dynamic streams (streams with a topic function), call the stream first to get the store, then hydrate:
|
|
695
|
+
|
|
696
|
+
```js
|
|
697
|
+
// src/routes/team/[id]/+page.server.js
|
|
698
|
+
export async function load({ platform, locals, params }) {
|
|
699
|
+
const { invitations } = await import('$live/invitation');
|
|
700
|
+
const data = await invitations.load(platform, { args: [params.id], user: locals.user });
|
|
701
|
+
return { invitations: data };
|
|
702
|
+
}
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
```svelte
|
|
706
|
+
<!-- src/routes/team/[id]/+page.svelte -->
|
|
707
|
+
<script>
|
|
708
|
+
import { invitations } from '$live/invitation';
|
|
709
|
+
import { page } from '$app/state';
|
|
710
|
+
let { data } = $props();
|
|
711
|
+
|
|
712
|
+
const invites = invitations(page.params.id).hydrate(data.invitations);
|
|
713
|
+
</script>
|
|
714
|
+
|
|
715
|
+
{#each $invites as invite (invite.id)}
|
|
716
|
+
<p>{invite.email}</p>
|
|
717
|
+
{/each}
|
|
718
|
+
```
|
|
719
|
+
|
|
676
720
|
---
|
|
677
721
|
|
|
678
722
|
## Batching
|
|
@@ -890,6 +934,7 @@ Call `configure()` once at app startup. The hooks fire on state transitions only
|
|
|
890
934
|
| Option | Description |
|
|
891
935
|
|---|---|
|
|
892
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. |
|
|
893
938
|
| `onConnect()` | Called when the WebSocket connection opens after a reconnect |
|
|
894
939
|
| `onDisconnect()` | Called when the WebSocket connection closes |
|
|
895
940
|
| `beforeReconnect()` | Called before each reconnection attempt (can be async) |
|
|
@@ -941,6 +986,53 @@ The native client passes the token in the URL:
|
|
|
941
986
|
configure({ url: 'wss://my-sveltekit-app.com/ws?token=...' });
|
|
942
987
|
```
|
|
943
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
|
+
|
|
944
1036
|
---
|
|
945
1037
|
|
|
946
1038
|
## Combine stores
|
|
@@ -2013,6 +2105,8 @@ Import from `svelte-realtime/client`.
|
|
|
2013
2105
|
|
|
2014
2106
|
| Method/Property | Description |
|
|
2015
2107
|
|---|---|
|
|
2108
|
+
| `error` | `Readable<RpcError \| null>` -- current error, or `null` when healthy |
|
|
2109
|
+
| `status` | `Readable<'loading' \| 'connected' \| 'reconnecting' \| 'error'>` -- connection status |
|
|
2016
2110
|
| `optimistic(event, data)` | Apply instant UI update, returns rollback function |
|
|
2017
2111
|
| `hydrate(initialData)` | Pre-populate with SSR data |
|
|
2018
2112
|
| `loadMore(...extraArgs)` | Load next page (cursor-based) |
|
package/client.d.ts
CHANGED
|
@@ -45,7 +45,9 @@ export function __rpc(path: string): ((...args: any[]) => Promise<any>) & {
|
|
|
45
45
|
* - `undefined` while loading
|
|
46
46
|
* - The initial data once loaded
|
|
47
47
|
* - Automatically updated with live pub/sub events
|
|
48
|
-
*
|
|
48
|
+
*
|
|
49
|
+
* Errors never replace the store value. On failure, the last known data
|
|
50
|
+
* is preserved and the error is available via the `.error` store.
|
|
49
51
|
*
|
|
50
52
|
* @param path - Stream path (e.g. `'chat/messages'`)
|
|
51
53
|
* @param options - Merge strategy and options
|
|
@@ -57,6 +59,10 @@ export function __rpc(path: string): ((...args: any[]) => Promise<any>) & {
|
|
|
57
59
|
* SSR hydration, and cursor-based pagination.
|
|
58
60
|
*/
|
|
59
61
|
export interface StreamStore<T = any> extends Readable<T> {
|
|
62
|
+
/** Reactive store holding the current error, or null when healthy. Errors never replace the data store value. */
|
|
63
|
+
readonly error: Readable<RpcError | null>;
|
|
64
|
+
/** Reactive store holding the connection status: 'loading', 'connected', 'reconnecting', or 'error'. */
|
|
65
|
+
readonly status: Readable<'loading' | 'connected' | 'reconnecting' | 'error'>;
|
|
60
66
|
/** Apply an instant UI update. Returns a rollback function. */
|
|
61
67
|
optimistic(event: string, data: any): () => void;
|
|
62
68
|
/** Pre-populate with SSR data to avoid loading spinners. */
|
|
@@ -175,6 +181,22 @@ export function configure(config: {
|
|
|
175
181
|
* @example 'wss://my-app.com/ws'
|
|
176
182
|
*/
|
|
177
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;
|
|
178
200
|
/** Called when the WebSocket connection opens (not on initial connect, only reconnects). */
|
|
179
201
|
onConnect?(): void;
|
|
180
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
|
|
|
@@ -508,6 +536,28 @@ function _createStream(path, options, dynamicArgs) {
|
|
|
508
536
|
let currentValue;
|
|
509
537
|
const store = writable(undefined);
|
|
510
538
|
|
|
539
|
+
/** @type {RpcError | null} */
|
|
540
|
+
let _error = null;
|
|
541
|
+
const _errorStore = writable(null);
|
|
542
|
+
|
|
543
|
+
/** @type {'loading' | 'connected' | 'reconnecting' | 'error'} */
|
|
544
|
+
let _status = 'loading';
|
|
545
|
+
const _statusStore = writable(/** @type {'loading' | 'connected' | 'reconnecting' | 'error'} */ ('loading'));
|
|
546
|
+
|
|
547
|
+
function _setError(/** @type {RpcError} */ err) {
|
|
548
|
+
_error = err;
|
|
549
|
+
_errorStore.set(err);
|
|
550
|
+
_status = 'error';
|
|
551
|
+
_statusStore.set('error');
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function _clearError() {
|
|
555
|
+
if (_error !== null) {
|
|
556
|
+
_error = null;
|
|
557
|
+
_errorStore.set(null);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
511
561
|
/** @type {string | null} */
|
|
512
562
|
let topic = null;
|
|
513
563
|
|
|
@@ -841,7 +891,7 @@ function _createStream(path, options, dynamicArgs) {
|
|
|
841
891
|
function fetchAndSubscribe() {
|
|
842
892
|
if (fetching) return;
|
|
843
893
|
if (_terminated) {
|
|
844
|
-
|
|
894
|
+
_setError(new RpcError('CONNECTION_CLOSED', 'Connection permanently closed'));
|
|
845
895
|
return;
|
|
846
896
|
}
|
|
847
897
|
fetching = true;
|
|
@@ -871,13 +921,13 @@ function _createStream(path, options, dynamicArgs) {
|
|
|
871
921
|
pending.delete(id);
|
|
872
922
|
pendingId = null;
|
|
873
923
|
fetching = false;
|
|
874
|
-
|
|
924
|
+
_setError(new RpcError('DISCONNECTED', 'Connection interrupted (device sleep)'));
|
|
875
925
|
return;
|
|
876
926
|
}
|
|
877
927
|
pending.delete(id);
|
|
878
928
|
pendingId = null;
|
|
879
929
|
fetching = false;
|
|
880
|
-
|
|
930
|
+
_setError(new RpcError('TIMEOUT', `Stream '${path}' timed out after 30s`));
|
|
881
931
|
}, _getTimeout());
|
|
882
932
|
|
|
883
933
|
pending.set(id, {
|
|
@@ -886,6 +936,9 @@ function _createStream(path, options, dynamicArgs) {
|
|
|
886
936
|
fetching = false;
|
|
887
937
|
pendingId = null;
|
|
888
938
|
_reconnectAttempts = 0;
|
|
939
|
+
_clearError();
|
|
940
|
+
_status = 'connected';
|
|
941
|
+
_statusStore.set('connected');
|
|
889
942
|
topic = response.topic || null;
|
|
890
943
|
|
|
891
944
|
// Track sequence number for replay
|
|
@@ -985,7 +1038,7 @@ function _createStream(path, options, dynamicArgs) {
|
|
|
985
1038
|
reject(err) {
|
|
986
1039
|
fetching = false;
|
|
987
1040
|
pendingId = null;
|
|
988
|
-
|
|
1041
|
+
_setError(err instanceof RpcError ? err : new RpcError('STREAM_ERROR', err?.message || 'Stream failed'));
|
|
989
1042
|
},
|
|
990
1043
|
timer
|
|
991
1044
|
});
|
|
@@ -1035,6 +1088,10 @@ function _createStream(path, options, dynamicArgs) {
|
|
|
1035
1088
|
buffer = [];
|
|
1036
1089
|
currentValue = undefined;
|
|
1037
1090
|
store.set(undefined);
|
|
1091
|
+
_error = null;
|
|
1092
|
+
_errorStore.set(null);
|
|
1093
|
+
_status = 'loading';
|
|
1094
|
+
_statusStore.set('loading');
|
|
1038
1095
|
_index.clear();
|
|
1039
1096
|
_history = [];
|
|
1040
1097
|
_historyIndex = -1;
|
|
@@ -1046,6 +1103,8 @@ function _createStream(path, options, dynamicArgs) {
|
|
|
1046
1103
|
let _pendingCleanup = false;
|
|
1047
1104
|
|
|
1048
1105
|
return {
|
|
1106
|
+
error: { subscribe: _errorStore.subscribe },
|
|
1107
|
+
status: { subscribe: _statusStore.subscribe },
|
|
1049
1108
|
subscribe(fn) {
|
|
1050
1109
|
if (subCount++ === 0) {
|
|
1051
1110
|
if (_pendingCleanup) {
|
|
@@ -1064,6 +1123,8 @@ function _createStream(path, options, dynamicArgs) {
|
|
|
1064
1123
|
return;
|
|
1065
1124
|
}
|
|
1066
1125
|
if (s === 'open' && subCount > 0) {
|
|
1126
|
+
_status = 'reconnecting';
|
|
1127
|
+
_statusStore.set('reconnecting');
|
|
1067
1128
|
if (_reconnectTimer) clearTimeout(_reconnectTimer);
|
|
1068
1129
|
let delay;
|
|
1069
1130
|
if (_reconnectAttempts < 2) {
|
|
@@ -1093,7 +1154,7 @@ function _createStream(path, options, dynamicArgs) {
|
|
|
1093
1154
|
if (conn && typeof conn.ready === 'function') {
|
|
1094
1155
|
conn.ready().catch((/** @type {any} */ err) => {
|
|
1095
1156
|
if (subCount > 0) {
|
|
1096
|
-
|
|
1157
|
+
_setError(new RpcError(err?.code || 'CONNECTION_CLOSED', err?.message || 'Connection permanently closed'));
|
|
1097
1158
|
}
|
|
1098
1159
|
});
|
|
1099
1160
|
}
|
|
@@ -1526,7 +1587,7 @@ function _checkArgs(path, args) {
|
|
|
1526
1587
|
* @typedef {{ path: string, args: any[], queuedAt: number, resolve: Function, reject: Function }} OfflineEntry
|
|
1527
1588
|
*/
|
|
1528
1589
|
|
|
1529
|
-
/** @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 } }} */
|
|
1530
1591
|
let _clientConfig = {};
|
|
1531
1592
|
|
|
1532
1593
|
/** @type {boolean} */
|
|
@@ -1544,13 +1605,17 @@ let _replayingQueue = false;
|
|
|
1544
1605
|
/**
|
|
1545
1606
|
* Configure client-side connection hooks and offline queue.
|
|
1546
1607
|
*
|
|
1547
|
-
* @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
|
|
1548
1609
|
*/
|
|
1549
1610
|
export function configure(config) {
|
|
1550
1611
|
_clientConfig = config;
|
|
1551
1612
|
|
|
1552
|
-
if (config.url) {
|
|
1553
|
-
|
|
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);
|
|
1554
1619
|
}
|
|
1555
1620
|
|
|
1556
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"
|