svelte-adapter-uws 0.6.0-next.13 → 0.6.0-next.15
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 +1 -0
- package/client-runtime.js +109 -0
- package/client.js +49 -46
- package/files/_init.js +3 -3
- package/files/cookies.js +1 -1
- package/files/handler.js +98 -58
- package/files/index.js +15 -14
- package/files/runtime.js +94 -0
- package/files/utils.js +129 -9
- package/index.d.ts +63 -0
- package/index.js +2 -2
- package/package.json +9 -2
- package/plugins/cursor/client.js +7 -6
- package/plugins/cursor/decode.js +8 -6
- package/plugins/cursor/server.js +21 -20
- package/plugins/dedup/server.js +9 -7
- package/plugins/groups/client.js +2 -1
- package/plugins/lock/server.js +5 -3
- package/plugins/presence/client.js +14 -13
- package/plugins/presence/server.js +6 -5
- package/plugins/ratelimit/server.js +17 -15
- package/plugins/replay/client.js +2 -1
- package/plugins/session/server.js +13 -11
- package/plugins/throttle/server.js +11 -9
- package/safe-url.d.ts +109 -0
- package/safe-url.js +472 -0
- package/testing.js +1360 -1349
- package/vite.js +24 -2
package/README.md
CHANGED
|
@@ -442,6 +442,7 @@ These options control how the server handles misbehaving or slow clients at the
|
|
|
442
442
|
- `maxConcurrent` caps how many upgrades may be in flight at once. Crossed requests get a fast `503 Service Unavailable` before any per-request work, so a connection storm can be shed without spending CPU on TLS, header parsing, or cookie decoding. Set this just above your steady-state in-flight count to act as a circuit breaker.
|
|
443
443
|
- `perTickBudget` caps how many actual `res.upgrade()` calls run per Node.js event-loop tick. Once the budget is spent, subsequent calls are deferred via `setImmediate` so the loop is not starved by 10K synchronous handshakes from one I/O batch. Pre-upgrade work (rate limit, origin check, hook dispatch) still runs in the original tick; only the hand-off to the C++ upgrade path is paced. Start with `64` and adjust based on your peak burst envelope.
|
|
444
444
|
- `waitingRoom` upgrades the over-capacity rejection from a bare `503` to a content-negotiated waiting room: a browser navigation gets a self-polling HTML holding page that auto-reloads when capacity frees, while a WebSocket upgrade or non-HTML client keeps a `503` with a jittered `Retry-After`. On by default once `maxConcurrent > 0`; set `waitingRoom: false` for the exact bare `503`. The page polls a read-only `/__admit-check` endpoint (`202` with a queue-depth/ETA body while full, `200` when capacity exists) that consumes no gate slot. Tune with `waitingRoom: { path, admitCheckPath, retryAfterSeconds, pollIntervalMs, template }`.
|
|
445
|
+
- `cursorLane` reserves a fraction of `maxConcurrent` (default `0.25`, at least one slot) for a deprioritised cursor-only upgrade lane - a second WebSocket that requests the `svelte-realtime-cursor` subprotocol. A cursor upgrade is admitted only while both the main ceiling and the cursor sub-budget have room, so a flood of cursor connects can never starve main-WebSocket admission; the main lane never waits on the cursor sub-budget. The cursor lane is refused first and, under `siege`, refused entirely - always with a bare `503` (never the holding page, since the cursor connection is not a browser). Omit `cursorLane` to disable the lane: the second counter never increments and admission is unchanged. Set it with `cursorLane: { fraction }`.
|
|
445
446
|
|
|
446
447
|
```js
|
|
447
448
|
adapter({
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Injectable runtime environment for the BROWSER client bundle: the clock, RNG
|
|
2
|
+
// and timers that the client reads through named helpers instead of touching the
|
|
3
|
+
// native primitives directly. Same helper API and env shape as the server-side
|
|
4
|
+
// runtime module so call sites are identical, but backed by browser globals (no
|
|
5
|
+
// node: imports) so the client bundles can import it. A simulator running the
|
|
6
|
+
// client code under node still drives it via setRuntimeEnv.
|
|
7
|
+
//
|
|
8
|
+
// The browser clock is a DIRECT wall read (the clients never had a 1Hz cache,
|
|
9
|
+
// so a direct read preserves their existing behavior). `performance` is used for
|
|
10
|
+
// monotonic duration math when present; a fake-timer Date mock therefore
|
|
11
|
+
// propagates straight into now() with no extra bridge.
|
|
12
|
+
|
|
13
|
+
const _hasPerf = typeof globalThis.performance !== 'undefined' && typeof globalThis.performance.now === 'function';
|
|
14
|
+
// Snapshot at load: the wall time at performance.now() === 0. Adding
|
|
15
|
+
// performance.now() yields a monotonic ms-since-epoch value immune to clock steps.
|
|
16
|
+
const _processStartEpoch = _hasPerf ? Date.now() - globalThis.performance.now() : 0;
|
|
17
|
+
const _webcrypto = (typeof globalThis.crypto !== 'undefined') ? globalThis.crypto : undefined;
|
|
18
|
+
|
|
19
|
+
// v4-shaped fallback when Web Crypto randomUUID is unavailable (non-secure
|
|
20
|
+
// context / old browser). Uses the env RNG so a seeded run still reproduces it.
|
|
21
|
+
function _uuidFallback(rngFloat) {
|
|
22
|
+
let out = '';
|
|
23
|
+
for (let i = 0; i < 36; i++) {
|
|
24
|
+
if (i === 8 || i === 13 || i === 18 || i === 23) { out += '-'; continue; }
|
|
25
|
+
if (i === 14) { out += '4'; continue; }
|
|
26
|
+
const r = (rngFloat() * 16) | 0;
|
|
27
|
+
out += (i === 19 ? ((r & 0x3) | 0x8) : r).toString(16);
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// One frozen environment object, one stable hidden class. In a real browser
|
|
33
|
+
// `current === defaultEnv` for the whole page lifetime (no override ever
|
|
34
|
+
// installs), so V8 sees a monomorphic shape and inlines the helpers to the
|
|
35
|
+
// native primitives - zero measurable overhead on the hot path.
|
|
36
|
+
const defaultEnv = Object.freeze({
|
|
37
|
+
clock: Object.freeze({
|
|
38
|
+
now: () => Date.now(), // exact wall clock; direct read
|
|
39
|
+
monotonic: _hasPerf ? () => _processStartEpoch + globalThis.performance.now() : () => Date.now(), // duration math
|
|
40
|
+
wallEpoch: () => Date.now() // exact wall clock; process-identity baseline
|
|
41
|
+
}),
|
|
42
|
+
rng: Object.freeze({
|
|
43
|
+
float: () => Math.random(),
|
|
44
|
+
u32: () => (Math.random() * 0x100000000) >>> 0,
|
|
45
|
+
uuid: (_webcrypto && typeof _webcrypto.randomUUID === 'function')
|
|
46
|
+
? () => _webcrypto.randomUUID()
|
|
47
|
+
: () => _uuidFallback(() => Math.random()),
|
|
48
|
+
bytes: (_webcrypto && typeof _webcrypto.getRandomValues === 'function')
|
|
49
|
+
? (n) => _webcrypto.getRandomValues(new Uint8Array(n))
|
|
50
|
+
: (n) => { const a = new Uint8Array(n); for (let i = 0; i < n; i++) a[i] = (Math.random() * 256) | 0; return a; }
|
|
51
|
+
}),
|
|
52
|
+
timers: Object.freeze({
|
|
53
|
+
set: (cb, ms, ...a) => setTimeout(cb, ms, ...a),
|
|
54
|
+
setInterval: (cb, ms, ...a) => setInterval(cb, ms, ...a),
|
|
55
|
+
// No setImmediate in the browser: a zero-delay macrotask is the closest.
|
|
56
|
+
setImmediate: (cb, ...a) => setTimeout(cb, 0, ...a),
|
|
57
|
+
clear: (h) => clearTimeout(h),
|
|
58
|
+
clearInterval: (h) => clearInterval(h),
|
|
59
|
+
queueMicrotask: (typeof globalThis.queueMicrotask === 'function')
|
|
60
|
+
? (cb) => globalThis.queueMicrotask(cb)
|
|
61
|
+
: (cb) => Promise.resolve().then(cb)
|
|
62
|
+
}),
|
|
63
|
+
tz: undefined // effective timezone for cron evaluation; undefined = real local TZ
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
let current = defaultEnv;
|
|
67
|
+
|
|
68
|
+
// The named helpers are the ONLY thing client code imports. Each is a one-line
|
|
69
|
+
// read over `current` - monomorphic in a real browser, inlined by V8.
|
|
70
|
+
export const now = () => current.clock.now();
|
|
71
|
+
export const monotonicNow = () => current.clock.monotonic();
|
|
72
|
+
export const wallEpoch = () => current.clock.wallEpoch();
|
|
73
|
+
export const randomFloat = () => current.rng.float();
|
|
74
|
+
export const randomU32 = () => current.rng.u32();
|
|
75
|
+
export const randomUuid = () => current.rng.uuid();
|
|
76
|
+
export const randomBytes = (n) => current.rng.bytes(n);
|
|
77
|
+
export const setTimer = (cb, ms, ...a) => current.timers.set(cb, ms, ...a);
|
|
78
|
+
export const setIntervalTimer = (cb, ms, ...a) => current.timers.setInterval(cb, ms, ...a);
|
|
79
|
+
export const setImmediateTimer = (cb, ...a) => current.timers.setImmediate(cb, ...a);
|
|
80
|
+
export const clearTimer = (h) => current.timers.clear(h);
|
|
81
|
+
export const clearIntervalTimer = (h) => current.timers.clearInterval(h);
|
|
82
|
+
export const microtask = (cb) => current.timers.queueMicrotask(cb);
|
|
83
|
+
export const effectiveTimeZone = () => current.tz;
|
|
84
|
+
|
|
85
|
+
// Install a virtual environment (the simulator/test harness only). Refuses under
|
|
86
|
+
// a node production build unless explicitly forced, so a stray call can never
|
|
87
|
+
// swap the clock under a live deployment; in a real browser there is no process
|
|
88
|
+
// and nothing calls this anyway. A partial env merges over the native defaults,
|
|
89
|
+
// so a harness can override just the clock and keep native rng/timers.
|
|
90
|
+
export function setRuntimeEnv(env, opts) {
|
|
91
|
+
const force = opts && opts.force === true;
|
|
92
|
+
if (typeof process !== 'undefined' && process.env && process.env.NODE_ENV === 'production' && !force) {
|
|
93
|
+
throw new Error('client runtime: setRuntimeEnv refused in production (pass { force: true } only inside a controlled simulation harness)');
|
|
94
|
+
}
|
|
95
|
+
current = Object.freeze({
|
|
96
|
+
clock: Object.freeze({ ...defaultEnv.clock, ...(env && env.clock) }),
|
|
97
|
+
rng: Object.freeze({ ...defaultEnv.rng, ...(env && env.rng) }),
|
|
98
|
+
timers: Object.freeze({ ...defaultEnv.timers, ...(env && env.timers) }),
|
|
99
|
+
tz: env && Object.prototype.hasOwnProperty.call(env, 'tz') ? env.tz : defaultEnv.tz
|
|
100
|
+
});
|
|
101
|
+
return current;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Restore the native environment. Cheap wholesale reassignment (no per-field
|
|
105
|
+
// mutation), so the hidden class stays stable.
|
|
106
|
+
export function resetRuntimeEnv() { current = defaultEnv; }
|
|
107
|
+
|
|
108
|
+
// Read-only accessor for the active env (test/sim introspection only).
|
|
109
|
+
export function getRuntimeEnv() { return current; }
|
package/client.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { writable, derived } from 'svelte/store';
|
|
2
2
|
import { parseBinaryFrame, requestNFrame } from './files/wire.js';
|
|
3
|
+
import { now, randomFloat, setTimer, setIntervalTimer, clearTimer, clearIntervalTimer, microtask } from './client-runtime.js';
|
|
3
4
|
|
|
4
5
|
/** @type {ReturnType<typeof createConnection> | null} */
|
|
5
6
|
let singleton = null;
|
|
@@ -290,7 +291,7 @@ export function ready() {
|
|
|
290
291
|
function cleanup() {
|
|
291
292
|
if (settled) return;
|
|
292
293
|
settled = true;
|
|
293
|
-
|
|
294
|
+
microtask(() => {
|
|
294
295
|
statusUnsub?.();
|
|
295
296
|
permaUnsub?.();
|
|
296
297
|
});
|
|
@@ -403,20 +404,20 @@ export function crud(topic, initial = [], options = {}) {
|
|
|
403
404
|
let list = [...initial];
|
|
404
405
|
/** @type {Map<string, number>} */
|
|
405
406
|
const timestamps = new Map();
|
|
406
|
-
const
|
|
407
|
+
const seededAt = now();
|
|
407
408
|
for (const item of initial) {
|
|
408
|
-
timestamps.set(keyOf(item),
|
|
409
|
+
timestamps.set(keyOf(item), seededAt);
|
|
409
410
|
}
|
|
410
411
|
|
|
411
412
|
const output = writable(list);
|
|
412
413
|
/** @type {(() => void) | null} */
|
|
413
414
|
let sourceUnsub = null;
|
|
414
|
-
/** @type {ReturnType<typeof
|
|
415
|
+
/** @type {ReturnType<typeof setIntervalTimer> | null} */
|
|
415
416
|
let sweepTimer = null;
|
|
416
417
|
let subCount = 0;
|
|
417
418
|
|
|
418
419
|
function sweep() {
|
|
419
|
-
const cutoff =
|
|
420
|
+
const cutoff = now() - /** @type {number} */ (maxAge);
|
|
420
421
|
let changed = false;
|
|
421
422
|
for (const [id, ts] of timestamps) {
|
|
422
423
|
if (ts < cutoff) {
|
|
@@ -437,21 +438,21 @@ export function crud(topic, initial = [], options = {}) {
|
|
|
437
438
|
if (data == null || typeof data !== 'object') return;
|
|
438
439
|
const id = keyOf(data);
|
|
439
440
|
if (evt === 'deleted') timestamps.delete(id);
|
|
440
|
-
else timestamps.set(id,
|
|
441
|
+
else timestamps.set(id, now());
|
|
441
442
|
list = applyCrudReducer(list, evt, data, arrayCrudStorage, reducerOpts);
|
|
442
443
|
output.set(list);
|
|
443
444
|
});
|
|
444
|
-
sweepTimer =
|
|
445
|
+
sweepTimer = setIntervalTimer(sweep, Math.max(maxAge / 2, 1000));
|
|
445
446
|
}
|
|
446
447
|
|
|
447
448
|
function stop() {
|
|
448
449
|
if (sourceUnsub) { sourceUnsub(); sourceUnsub = null; }
|
|
449
|
-
if (sweepTimer) {
|
|
450
|
+
if (sweepTimer) { clearIntervalTimer(sweepTimer); sweepTimer = null; }
|
|
450
451
|
list = [...initial];
|
|
451
|
-
const
|
|
452
|
+
const seededAt = now();
|
|
452
453
|
timestamps.clear();
|
|
453
454
|
for (const item of initial) {
|
|
454
|
-
timestamps.set(keyOf(item),
|
|
455
|
+
timestamps.set(keyOf(item), seededAt);
|
|
455
456
|
}
|
|
456
457
|
output.set(list);
|
|
457
458
|
}
|
|
@@ -508,20 +509,20 @@ export function lookup(topic, initial = [], options = {}) {
|
|
|
508
509
|
let map = { ...initialMap };
|
|
509
510
|
/** @type {Map<string, number>} */
|
|
510
511
|
const timestamps = new Map();
|
|
511
|
-
const
|
|
512
|
+
const seededAt = now();
|
|
512
513
|
for (const id in initialMap) {
|
|
513
|
-
timestamps.set(id,
|
|
514
|
+
timestamps.set(id, seededAt);
|
|
514
515
|
}
|
|
515
516
|
|
|
516
517
|
const output = writable(map);
|
|
517
518
|
/** @type {(() => void) | null} */
|
|
518
519
|
let sourceUnsub = null;
|
|
519
|
-
/** @type {ReturnType<typeof
|
|
520
|
+
/** @type {ReturnType<typeof setIntervalTimer> | null} */
|
|
520
521
|
let sweepTimer = null;
|
|
521
522
|
let subCount = 0;
|
|
522
523
|
|
|
523
524
|
function sweep() {
|
|
524
|
-
const cutoff =
|
|
525
|
+
const cutoff = now() - /** @type {number} */ (maxAge);
|
|
525
526
|
let changed = false;
|
|
526
527
|
for (const [id, ts] of timestamps) {
|
|
527
528
|
if (ts < cutoff) {
|
|
@@ -544,7 +545,7 @@ export function lookup(topic, initial = [], options = {}) {
|
|
|
544
545
|
if (data == null || typeof data !== 'object') return;
|
|
545
546
|
const id = keyOf(data);
|
|
546
547
|
if (evt === 'deleted') timestamps.delete(id);
|
|
547
|
-
else timestamps.set(id,
|
|
548
|
+
else timestamps.set(id, now());
|
|
548
549
|
const next = applyCrudReducer(map, evt, data, recordCrudStorage, reducerOpts);
|
|
549
550
|
if (next === map) return;
|
|
550
551
|
map = next;
|
|
@@ -552,17 +553,17 @@ export function lookup(topic, initial = [], options = {}) {
|
|
|
552
553
|
});
|
|
553
554
|
// Sweep at half the maxAge interval for responsive cleanup
|
|
554
555
|
// without burning cycles on very short intervals
|
|
555
|
-
sweepTimer =
|
|
556
|
+
sweepTimer = setIntervalTimer(sweep, Math.max(maxAge / 2, 1000));
|
|
556
557
|
}
|
|
557
558
|
|
|
558
559
|
function stop() {
|
|
559
560
|
if (sourceUnsub) { sourceUnsub(); sourceUnsub = null; }
|
|
560
|
-
if (sweepTimer) {
|
|
561
|
+
if (sweepTimer) { clearIntervalTimer(sweepTimer); sweepTimer = null; }
|
|
561
562
|
map = { ...initialMap };
|
|
562
|
-
const
|
|
563
|
+
const seededAt = now();
|
|
563
564
|
timestamps.clear();
|
|
564
565
|
for (const id in initialMap) {
|
|
565
|
-
timestamps.set(id,
|
|
566
|
+
timestamps.set(id, seededAt);
|
|
566
567
|
}
|
|
567
568
|
output.set(map);
|
|
568
569
|
}
|
|
@@ -638,8 +639,8 @@ export function once(topic, event, options) {
|
|
|
638
639
|
function cleanup() {
|
|
639
640
|
if (settled) return;
|
|
640
641
|
settled = true;
|
|
641
|
-
if (timer)
|
|
642
|
-
|
|
642
|
+
if (timer) clearTimer(timer);
|
|
643
|
+
microtask(() => unsub());
|
|
643
644
|
}
|
|
644
645
|
|
|
645
646
|
const unsub = store.subscribe((data) => {
|
|
@@ -652,7 +653,7 @@ export function once(topic, event, options) {
|
|
|
652
653
|
}
|
|
653
654
|
});
|
|
654
655
|
if (timeout !== undefined) {
|
|
655
|
-
timer =
|
|
656
|
+
timer = setTimer(() => {
|
|
656
657
|
cleanup();
|
|
657
658
|
reject(new Error(`once('${topic}'${event ? `, '${event}'` : ''}) timed out after ${timeout}ms`));
|
|
658
659
|
}, timeout);
|
|
@@ -716,21 +717,23 @@ export function classifyCloseCode(code) {
|
|
|
716
717
|
* cap by attempt 6) and gentle enough that a brief restart resolves
|
|
717
718
|
* before the user notices.
|
|
718
719
|
*
|
|
719
|
-
* Pure
|
|
720
|
-
* reproducible assertions in tests.
|
|
720
|
+
* Pure given an explicit `randFactor`: no I/O, no globals. Pass a fixed
|
|
721
|
+
* value for reproducible assertions in tests.
|
|
721
722
|
*
|
|
722
|
-
* The default `
|
|
723
|
-
*
|
|
723
|
+
* The default `randFactor` is the runtime float source: this value is
|
|
724
|
+
* reconnect-backoff jitter, used to spread retries across a fleet so a
|
|
724
725
|
* server restart does not hit a thundering-herd. Not security-relevant -
|
|
725
|
-
* the randFactor never crosses a trust boundary
|
|
726
|
+
* the randFactor never crosses a trust boundary - so the runtime source is
|
|
727
|
+
* the right primitive; routing it through the runtime also lets a seeded
|
|
728
|
+
* harness reproduce the reconnect schedule exactly.
|
|
726
729
|
*
|
|
727
730
|
* @param {number} base base interval in ms (e.g. 3000)
|
|
728
731
|
* @param {number} maxDelay cap in ms (e.g. 300000)
|
|
729
732
|
* @param {number} attempt zero-based attempt counter
|
|
730
|
-
* @param {number} [randFactor] random factor in [0, 1); defaults to
|
|
733
|
+
* @param {number} [randFactor] random factor in [0, 1); defaults to randomFloat()
|
|
731
734
|
* @returns {number}
|
|
732
735
|
*/
|
|
733
|
-
export function nextReconnectDelay(base, maxDelay, attempt, randFactor =
|
|
736
|
+
export function nextReconnectDelay(base, maxDelay, attempt, randFactor = randomFloat()) {
|
|
734
737
|
const capped = Math.min(base * Math.pow(2.2, attempt), maxDelay);
|
|
735
738
|
return capped * (0.75 + randFactor * 0.5);
|
|
736
739
|
}
|
|
@@ -758,9 +761,9 @@ function createConnection(options) {
|
|
|
758
761
|
/** @type {WebSocket | null} */
|
|
759
762
|
let ws = null;
|
|
760
763
|
|
|
761
|
-
/** @type {ReturnType<typeof
|
|
764
|
+
/** @type {ReturnType<typeof setTimer> | null} */
|
|
762
765
|
let reconnectTimer = null;
|
|
763
|
-
/** @type {ReturnType<typeof
|
|
766
|
+
/** @type {ReturnType<typeof setIntervalTimer> | null} */
|
|
764
767
|
let activityTimer = null;
|
|
765
768
|
|
|
766
769
|
/** @type {Promise<boolean> | null} deduped in-flight auth preflight */
|
|
@@ -777,7 +780,7 @@ function createConnection(options) {
|
|
|
777
780
|
let hiddenDisconnect = false;
|
|
778
781
|
// Timestamp of the last message received from the server. Used to detect
|
|
779
782
|
// zombie connections - cases where onclose was suppressed by browser throttling.
|
|
780
|
-
let lastServerMessage =
|
|
783
|
+
let lastServerMessage = now();
|
|
781
784
|
// 2.5x the server's 120s idle timeout. If the server has been completely
|
|
782
785
|
// silent for this long while the socket appears open, it is likely a zombie.
|
|
783
786
|
const SERVER_TIMEOUT_MS = 150000;
|
|
@@ -929,7 +932,7 @@ function createConnection(options) {
|
|
|
929
932
|
let _onFlowDegraded = null;
|
|
930
933
|
|
|
931
934
|
function _flowFresh() {
|
|
932
|
-
return _flowAvail > 0 &&
|
|
935
|
+
return _flowAvail > 0 && now() < _flowExpiresAt;
|
|
933
936
|
}
|
|
934
937
|
function _setFlowDegraded(d) {
|
|
935
938
|
if (d === _flowDegraded) return;
|
|
@@ -963,7 +966,7 @@ function createConnection(options) {
|
|
|
963
966
|
// Apply a fresh window and drain the queue in FIFO order.
|
|
964
967
|
function _applyFlowWindow(count, ttlMs) {
|
|
965
968
|
_flowActive = true;
|
|
966
|
-
_flowExpiresAt =
|
|
969
|
+
_flowExpiresAt = now() + ttlMs;
|
|
967
970
|
_flowAvail = count;
|
|
968
971
|
// A fresh window clears the replenish latch so the next low-water
|
|
969
972
|
// crossing can ask for more again.
|
|
@@ -1210,7 +1213,7 @@ function createConnection(options) {
|
|
|
1210
1213
|
|
|
1211
1214
|
ws.onopen = () => {
|
|
1212
1215
|
attempt = 0;
|
|
1213
|
-
lastServerMessage =
|
|
1216
|
+
lastServerMessage = now();
|
|
1214
1217
|
failureStore.set(null);
|
|
1215
1218
|
setStatusOpen();
|
|
1216
1219
|
if (debug) console.log('[ws] connected');
|
|
@@ -1308,7 +1311,7 @@ function createConnection(options) {
|
|
|
1308
1311
|
}
|
|
1309
1312
|
|
|
1310
1313
|
ws.onmessage = (rawEvent) => {
|
|
1311
|
-
lastServerMessage =
|
|
1314
|
+
lastServerMessage = now();
|
|
1312
1315
|
try {
|
|
1313
1316
|
// Inbound binary demux, ahead of the JSON path. A 0x03 frame is
|
|
1314
1317
|
// a binary topic PAYLOAD: resolve its numeric topic-id to a name,
|
|
@@ -1504,7 +1507,7 @@ function createConnection(options) {
|
|
|
1504
1507
|
}
|
|
1505
1508
|
const delay = nextReconnectDelay(reconnectInterval, maxReconnectInterval, attempt);
|
|
1506
1509
|
attempt++;
|
|
1507
|
-
reconnectTimer =
|
|
1510
|
+
reconnectTimer = setTimer(() => {
|
|
1508
1511
|
reconnectTimer = null;
|
|
1509
1512
|
doConnect();
|
|
1510
1513
|
}, delay);
|
|
@@ -1538,7 +1541,7 @@ function createConnection(options) {
|
|
|
1538
1541
|
if (ws?.readyState !== WebSocket.OPEN) return;
|
|
1539
1542
|
if (!pendingSubscribes) {
|
|
1540
1543
|
pendingSubscribes = [];
|
|
1541
|
-
|
|
1544
|
+
microtask(flushPendingSubscribes);
|
|
1542
1545
|
}
|
|
1543
1546
|
pendingSubscribes.push(topic);
|
|
1544
1547
|
}
|
|
@@ -1649,7 +1652,7 @@ function createConnection(options) {
|
|
|
1649
1652
|
// that never mount). Safe: if another wrapper for the same topic has
|
|
1650
1653
|
// an active subscriber, topicRefCounts will be non-empty and we skip.
|
|
1651
1654
|
const ownStore = store;
|
|
1652
|
-
|
|
1655
|
+
microtask(() => {
|
|
1653
1656
|
if (subs === 0 && !topicRefCounts.has(topic) && topicStores.get(topic) === ownStore) {
|
|
1654
1657
|
topicStores.delete(topic);
|
|
1655
1658
|
}
|
|
@@ -1698,7 +1701,7 @@ function createConnection(options) {
|
|
|
1698
1701
|
store = writable(null);
|
|
1699
1702
|
eventStores.set(key, store);
|
|
1700
1703
|
const ownStore = store;
|
|
1701
|
-
|
|
1704
|
+
microtask(() => {
|
|
1702
1705
|
if (subs === 0 && !topicRefCounts.has(topic) && eventStores.get(key) === ownStore) {
|
|
1703
1706
|
eventStores.delete(key);
|
|
1704
1707
|
}
|
|
@@ -1800,11 +1803,11 @@ function createConnection(options) {
|
|
|
1800
1803
|
intentionallyClosed = true;
|
|
1801
1804
|
permaClosedStore.set(true);
|
|
1802
1805
|
if (reconnectTimer) {
|
|
1803
|
-
|
|
1806
|
+
clearTimer(reconnectTimer);
|
|
1804
1807
|
reconnectTimer = null;
|
|
1805
1808
|
}
|
|
1806
1809
|
if (activityTimer) {
|
|
1807
|
-
|
|
1810
|
+
clearIntervalTimer(activityTimer);
|
|
1808
1811
|
activityTimer = null;
|
|
1809
1812
|
}
|
|
1810
1813
|
if (visibilityHandler && typeof document !== 'undefined') {
|
|
@@ -1853,7 +1856,7 @@ function createConnection(options) {
|
|
|
1853
1856
|
hiddenDisconnect = false;
|
|
1854
1857
|
attempt = 0;
|
|
1855
1858
|
if (reconnectTimer) {
|
|
1856
|
-
|
|
1859
|
+
clearTimer(reconnectTimer);
|
|
1857
1860
|
reconnectTimer = null;
|
|
1858
1861
|
}
|
|
1859
1862
|
doConnect();
|
|
@@ -1867,9 +1870,9 @@ function createConnection(options) {
|
|
|
1867
1870
|
// on mobile after wake from sleep). Force a close so onclose fires and the
|
|
1868
1871
|
// normal reconnect path takes over.
|
|
1869
1872
|
if (typeof window !== 'undefined') {
|
|
1870
|
-
activityTimer =
|
|
1871
|
-
if (ws?.readyState === WebSocket.OPEN &&
|
|
1872
|
-
if (debug) console.log('[ws] server silent for',
|
|
1873
|
+
activityTimer = setIntervalTimer(() => {
|
|
1874
|
+
if (ws?.readyState === WebSocket.OPEN && now() - lastServerMessage > SERVER_TIMEOUT_MS) {
|
|
1875
|
+
if (debug) console.log('[ws] server silent for', now() - lastServerMessage, 'ms, reconnecting');
|
|
1873
1876
|
ws.close();
|
|
1874
1877
|
}
|
|
1875
1878
|
}, 30000);
|
package/files/_init.js
CHANGED
|
@@ -29,14 +29,14 @@ import fs from 'node:fs';
|
|
|
29
29
|
import path from 'node:path';
|
|
30
30
|
import { fileURLToPath } from 'node:url';
|
|
31
31
|
import { Readable } from 'node:stream';
|
|
32
|
-
import {
|
|
32
|
+
import { monotonicNow } from './runtime.js';
|
|
33
33
|
import { Server } from 'SERVER';
|
|
34
34
|
import { manifest, base } from 'MANIFEST';
|
|
35
35
|
|
|
36
36
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
37
37
|
const asset_dir = `${__dirname}/client${base}`;
|
|
38
38
|
|
|
39
|
-
const _t_init =
|
|
39
|
+
const _t_init = monotonicNow();
|
|
40
40
|
|
|
41
41
|
/** @type {import('@sveltejs/kit').Server} */
|
|
42
42
|
export const server = new Server(manifest);
|
|
@@ -46,4 +46,4 @@ await server.init({
|
|
|
46
46
|
read: (file) => /** @type {ReadableStream} */ (Readable.toWeb(fs.createReadStream(`${asset_dir}/${file}`)))
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
-
console.log(`SvelteKit server initialized in ${(
|
|
49
|
+
console.log(`SvelteKit server initialized in ${(monotonicNow() - _t_init).toFixed(1)}ms`);
|
package/files/cookies.js
CHANGED
|
@@ -149,7 +149,7 @@ export function createCookies(cookieHeader) {
|
|
|
149
149
|
delete(name, options = {}) {
|
|
150
150
|
api.set(name, '', {
|
|
151
151
|
...options,
|
|
152
|
-
expires: new Date(0),
|
|
152
|
+
expires: new Date(0), // determinism-allow: fixed Unix-epoch sentinel that forces immediate cookie deletion, not a wall-clock read
|
|
153
153
|
maxAge: 0
|
|
154
154
|
});
|
|
155
155
|
delete parsed[name];
|