@pylonsync/realtime 0.14.0
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 +30 -0
- package/dist/clock.d.ts +74 -0
- package/dist/connection.d.ts +95 -0
- package/dist/game.d.ts +80 -0
- package/dist/index.d.ts +7 -0
- package/dist/interpolation.d.ts +104 -0
- package/dist/prediction.d.ts +45 -0
- package/dist/replication.d.ts +65 -0
- package/dist/wire.d.ts +62 -0
- package/package.json +25 -0
- package/src/clock.test.ts +143 -0
- package/src/clock.ts +206 -0
- package/src/connection.test.ts +80 -0
- package/src/connection.ts +356 -0
- package/src/game.test.ts +137 -0
- package/src/game.ts +161 -0
- package/src/index.ts +7 -0
- package/src/interpolation.test.ts +163 -0
- package/src/interpolation.ts +317 -0
- package/src/prediction.test.ts +41 -0
- package/src/prediction.ts +80 -0
- package/src/replication.fixtures.json +2014 -0
- package/src/replication.test.ts +64 -0
- package/src/replication.ts +241 -0
- package/src/wire.test.ts +72 -0
- package/src/wire.ts +129 -0
- package/src/world3d.e2e.test.ts +234 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { ShardClock } from "./clock";
|
|
4
|
+
|
|
5
|
+
/** A deterministic jitter source in [0, 1). */
|
|
6
|
+
function rng(seed: number) {
|
|
7
|
+
let s = seed >>> 0;
|
|
8
|
+
return () => {
|
|
9
|
+
s = (s * 1664525 + 1013904223) >>> 0;
|
|
10
|
+
return s / 2 ** 32;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Frames for ticks `from..to` at 20 Hz, built at `tick * 50` ms, arriving
|
|
16
|
+
* after `latency` plus up to `jitter` ms.
|
|
17
|
+
*/
|
|
18
|
+
function feed(
|
|
19
|
+
clock: ShardClock,
|
|
20
|
+
from: number,
|
|
21
|
+
to: number,
|
|
22
|
+
latency: number,
|
|
23
|
+
jitter: number,
|
|
24
|
+
seed = 1,
|
|
25
|
+
) {
|
|
26
|
+
const r = rng(seed);
|
|
27
|
+
for (let tick = from; tick <= to; tick++) clock.observe(tick, tick * 50 + latency + r() * jitter);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("ShardClock", () => {
|
|
31
|
+
test("is not ready before a frame", () => {
|
|
32
|
+
const clock = new ShardClock();
|
|
33
|
+
expect(clock.ready).toBe(false);
|
|
34
|
+
expect(clock.serverTick(1000)).toBe(-1);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("anchors on the least-delayed frame, so jitter does not move it", () => {
|
|
38
|
+
const clock = new ShardClock({ tickRate: 20 });
|
|
39
|
+
feed(clock, 1, 200, 40, 60);
|
|
40
|
+
// At time t the newest tick that could have arrived was built at
|
|
41
|
+
// t - 40 ms (the smallest delay seen is close to 40).
|
|
42
|
+
const est = clock.serverTick(200 * 50 + 40 + 25);
|
|
43
|
+
expect(est).toBeGreaterThan(200.3);
|
|
44
|
+
expect(est).toBeLessThan(200.6);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("measures the tick length when the rate is unknown", () => {
|
|
48
|
+
const clock = new ShardClock();
|
|
49
|
+
const r = rng(7);
|
|
50
|
+
for (let tick = 1; tick <= 128; tick++) clock.observe(tick, tick * 33.3 + 20 + r() * 15);
|
|
51
|
+
expect(clock.tickMs).toBeGreaterThan(32.8);
|
|
52
|
+
expect(clock.tickMs).toBeLessThan(33.8);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("never goes backwards when a correction arrives", () => {
|
|
56
|
+
const clock = new ShardClock({ tickRate: 20 });
|
|
57
|
+
feed(clock, 1, 64, 100, 0);
|
|
58
|
+
let t = 64 * 50 + 100;
|
|
59
|
+
let prev = clock.serverTick(t);
|
|
60
|
+
// Latency drops by 60 ms: the estimate must move ahead, gradually.
|
|
61
|
+
for (let tick = 65; tick <= 200; tick++) {
|
|
62
|
+
clock.observe(tick, tick * 50 + 40);
|
|
63
|
+
for (let step = 0; step < 3; step++) {
|
|
64
|
+
t += 50 / 3;
|
|
65
|
+
const now = clock.serverTick(t);
|
|
66
|
+
expect(now).toBeGreaterThan(prev);
|
|
67
|
+
prev = now;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// It converged on the new, lower latency.
|
|
71
|
+
expect(clock.serverTick(200 * 50 + 40)).toBeCloseTo(200, 1);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("follows a shard whose ticks resume late after a stall", () => {
|
|
75
|
+
const clock = new ShardClock({ tickRate: 20 });
|
|
76
|
+
feed(clock, 1, 40, 50, 0);
|
|
77
|
+
// The tick loop stalled for 2 s, then kept its tick numbers going.
|
|
78
|
+
for (let tick = 41; tick <= 43; tick++) clock.observe(tick, tick * 50 + 50 + 2000);
|
|
79
|
+
expect(clock.serverTick(43 * 50 + 50 + 2000)).toBeCloseTo(43, 1);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("one late frame is jitter", () => {
|
|
83
|
+
const clock = new ShardClock({ tickRate: 20 });
|
|
84
|
+
feed(clock, 1, 40, 50, 0);
|
|
85
|
+
clock.observe(41, 41 * 50 + 50 + 2000);
|
|
86
|
+
clock.observe(42, 42 * 50 + 50);
|
|
87
|
+
expect(clock.serverTick(42 * 50 + 50)).toBeCloseTo(42, 1);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("a burst after a network stall catches up to the newest tick", () => {
|
|
91
|
+
const clock = new ShardClock({ tickRate: 20 });
|
|
92
|
+
feed(clock, 1, 40, 50, 0);
|
|
93
|
+
// Ticks 41..80 were built on time but arrive together, 2 s late.
|
|
94
|
+
const at = 40 * 50 + 50 + 2000;
|
|
95
|
+
for (let tick = 41; tick <= 80; tick++) clock.observe(tick, at);
|
|
96
|
+
// Within a tick at once; the rest is spread over the next second.
|
|
97
|
+
expect(clock.serverTick(at)).toBeGreaterThan(78.9);
|
|
98
|
+
expect(clock.serverTick(at + 1500)).toBeCloseTo(110, 1);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
for (const tickRate of [20, undefined]) {
|
|
102
|
+
test(`a network burst neither moves it back nor throws it off (rate ${tickRate ?? "measured"})`, () => {
|
|
103
|
+
const clock = new ShardClock({ tickRate });
|
|
104
|
+
const r = rng(3);
|
|
105
|
+
let prev = -Infinity;
|
|
106
|
+
let worst = 0;
|
|
107
|
+
for (let tick = 1; tick <= 300; tick++) {
|
|
108
|
+
const built = tick * 50;
|
|
109
|
+
// Ticks 100..107 are held up by a 400 ms stall and arrive together.
|
|
110
|
+
const at = tick >= 100 && tick <= 107 ? 107 * 50 + 50 : built + 50 + r() * 10;
|
|
111
|
+
clock.observe(tick, at);
|
|
112
|
+
const est = clock.serverTick(at);
|
|
113
|
+
expect(est).toBeGreaterThanOrEqual(prev);
|
|
114
|
+
prev = est;
|
|
115
|
+
// The truth at `at`: the tick being built then, less the least delay.
|
|
116
|
+
if (tick > 120) worst = Math.max(worst, Math.abs(est - (at - 50) / 50));
|
|
117
|
+
}
|
|
118
|
+
expect(worst).toBeLessThan(1);
|
|
119
|
+
expect(clock.tickMs).toBeGreaterThan(47);
|
|
120
|
+
expect(clock.tickMs).toBeLessThan(53);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
test("after a stall it steps back no further than the newest tick received", () => {
|
|
125
|
+
const clock = new ShardClock({ tickRate: 20 });
|
|
126
|
+
feed(clock, 1, 40, 50, 0);
|
|
127
|
+
const before = clock.serverTick(40 * 50 + 50 + 1999);
|
|
128
|
+
expect(before).toBeGreaterThan(79);
|
|
129
|
+
for (let tick = 41; tick <= 43; tick++) {
|
|
130
|
+
const at = tick * 50 + 50 + 2000;
|
|
131
|
+
clock.observe(tick, at);
|
|
132
|
+
expect(clock.serverTick(at)).toBeGreaterThanOrEqual(tick - 1);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("ticks that go back (a restarted shard) reset it", () => {
|
|
137
|
+
const clock = new ShardClock({ tickRate: 20 });
|
|
138
|
+
feed(clock, 1000, 1010, 30, 0);
|
|
139
|
+
clock.observe(3, 999_999);
|
|
140
|
+
expect(clock.latestTick).toBe(3);
|
|
141
|
+
expect(clock.serverTick(999_999)).toBeCloseTo(3);
|
|
142
|
+
});
|
|
143
|
+
});
|
package/src/clock.ts
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An estimate of the shard's current tick, from the ticks in the frames it
|
|
3
|
+
* sends and when they arrive.
|
|
4
|
+
*
|
|
5
|
+
* Each frame carries the tick it was built on. The frame that arrived
|
|
6
|
+
* soonest after it was built (lowest delay) anchors the estimate, so
|
|
7
|
+
* network jitter only ever makes a frame look late. The tick length is the
|
|
8
|
+
* shard's tick rate when the client knows it, or else the median slope of
|
|
9
|
+
* arrival time against tick.
|
|
10
|
+
*
|
|
11
|
+
* The estimate is continuous: when a new frame moves it, the change is
|
|
12
|
+
* spread over time instead of jumping (from `slew` of real time for small
|
|
13
|
+
* changes up to all of it). A step forward larger than `snapMs` applies at
|
|
14
|
+
* once. A step back larger than that applies only as far as the newest
|
|
15
|
+
* tick received, so it never goes back past a tick a frame carried.
|
|
16
|
+
*
|
|
17
|
+
* After three frames in a row arrive more than `snapMs` late and spaced
|
|
18
|
+
* like ticks (a shard that stalled), the older frames are dropped and the
|
|
19
|
+
* clock follows the new schedule. Frames that arrive together (a network
|
|
20
|
+
* burst) do not count.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export interface ShardClockOptions {
|
|
24
|
+
/** The shard's tick rate in ticks per second, when the client knows it. */
|
|
25
|
+
tickRate?: number;
|
|
26
|
+
/** Frames the estimate uses. Default 64. */
|
|
27
|
+
window?: number;
|
|
28
|
+
/** The least fraction of real time the estimate speeds up or slows
|
|
29
|
+
* down by to absorb a correction. Default 0.05. */
|
|
30
|
+
slew?: number;
|
|
31
|
+
/** A step forward larger than this (ms) applies at once, and a
|
|
32
|
+
* correction this large or larger runs at full rate. Default 250. */
|
|
33
|
+
snapMs?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface Sample {
|
|
37
|
+
tick: number;
|
|
38
|
+
at: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const DEFAULT_TICK_MS = 50;
|
|
42
|
+
/** Late frames in a row that mean the schedule moved. */
|
|
43
|
+
const LATE_FRAMES = 3;
|
|
44
|
+
/** Samples the tick length is measured from; fewer keep the last value. */
|
|
45
|
+
const MIN_FIT_SAMPLES = 8;
|
|
46
|
+
|
|
47
|
+
export class ShardClock {
|
|
48
|
+
private readonly fixedTickMs: number | null;
|
|
49
|
+
private readonly window: number;
|
|
50
|
+
private readonly slew: number;
|
|
51
|
+
private readonly snapMs: number;
|
|
52
|
+
private samples: Sample[] = [];
|
|
53
|
+
private measuredTickMs = DEFAULT_TICK_MS;
|
|
54
|
+
/** The newest tick seen, and the estimated arrival time of a frame for
|
|
55
|
+
* it with the lowest delay seen. */
|
|
56
|
+
private anchorTick = -1;
|
|
57
|
+
private anchorAt = 0;
|
|
58
|
+
/** Ticks the displayed estimate is ahead of the raw one; decays to 0. */
|
|
59
|
+
private offset = 0;
|
|
60
|
+
private slewedTo = 0;
|
|
61
|
+
/** Frames in a row that arrived more than `snapMs` after the anchor
|
|
62
|
+
* predicted. */
|
|
63
|
+
private late = 0;
|
|
64
|
+
|
|
65
|
+
constructor(options: ShardClockOptions = {}) {
|
|
66
|
+
const rate = options.tickRate;
|
|
67
|
+
this.fixedTickMs = rate !== undefined && rate > 0 && Number.isFinite(rate) ? 1000 / rate : null;
|
|
68
|
+
this.window = Math.max(2, options.window ?? 64);
|
|
69
|
+
this.slew = Math.min(0.5, Math.max(0, options.slew ?? 0.05));
|
|
70
|
+
this.snapMs = options.snapMs ?? 250;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** True once a frame has arrived. */
|
|
74
|
+
get ready(): boolean {
|
|
75
|
+
return this.anchorTick >= 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The newest tick a frame carried, or -1. */
|
|
79
|
+
get latestTick(): number {
|
|
80
|
+
return this.anchorTick;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Milliseconds per tick: the configured rate, or the measured one. */
|
|
84
|
+
get tickMs(): number {
|
|
85
|
+
return this.fixedTickMs ?? this.measuredTickMs;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Forget everything (a new shard). */
|
|
89
|
+
reset(): void {
|
|
90
|
+
this.samples = [];
|
|
91
|
+
this.measuredTickMs = DEFAULT_TICK_MS;
|
|
92
|
+
this.anchorTick = -1;
|
|
93
|
+
this.anchorAt = 0;
|
|
94
|
+
this.offset = 0;
|
|
95
|
+
this.slewedTo = 0;
|
|
96
|
+
this.late = 0;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Record a frame for `tick` that arrived at `at` (ms, monotonic). */
|
|
100
|
+
observe(tick: number, at: number): void {
|
|
101
|
+
if (tick < this.anchorTick) {
|
|
102
|
+
// Ticks went back: the shard restarted, or this is another shard.
|
|
103
|
+
this.reset();
|
|
104
|
+
}
|
|
105
|
+
const shown = this.ready ? this.serverTick(at) : null;
|
|
106
|
+
const newestBefore = this.anchorTick;
|
|
107
|
+
|
|
108
|
+
// The shard's schedule moved later (it stalled, or the path got
|
|
109
|
+
// slower for good) when several frames in a row arrive late by more
|
|
110
|
+
// than `snapMs`, spaced like ticks. One late frame is jitter, and
|
|
111
|
+
// frames that arrive together are a network burst: the shard kept its
|
|
112
|
+
// schedule. After a move, the samples from before it would anchor the
|
|
113
|
+
// estimate too early, so they go.
|
|
114
|
+
const prev = this.samples[this.samples.length - 1];
|
|
115
|
+
if (this.ready && prev && tick > prev.tick) {
|
|
116
|
+
const expected = this.anchorAt + (tick - this.anchorTick) * this.tickMs;
|
|
117
|
+
const spaced = at - prev.at >= 0.5 * this.tickMs * (tick - prev.tick);
|
|
118
|
+
this.late = spaced && at - expected > this.snapMs ? this.late + 1 : 0;
|
|
119
|
+
if (this.late >= LATE_FRAMES) {
|
|
120
|
+
this.samples = this.samples.slice(-(LATE_FRAMES - 1));
|
|
121
|
+
this.late = 0;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const last = this.samples[this.samples.length - 1];
|
|
126
|
+
if (last && last.tick === tick) {
|
|
127
|
+
// Another frame for the same tick: keep the earlier arrival.
|
|
128
|
+
if (at >= last.at) return;
|
|
129
|
+
last.at = at;
|
|
130
|
+
} else {
|
|
131
|
+
this.samples.push({ tick, at });
|
|
132
|
+
if (this.samples.length > this.window) this.samples.shift();
|
|
133
|
+
}
|
|
134
|
+
if (this.fixedTickMs === null) this.fitTickMs();
|
|
135
|
+
|
|
136
|
+
const tickMs = this.tickMs;
|
|
137
|
+
let anchorAt = Infinity;
|
|
138
|
+
for (const s of this.samples) {
|
|
139
|
+
anchorAt = Math.min(anchorAt, s.at + (tick - s.tick) * tickMs);
|
|
140
|
+
}
|
|
141
|
+
this.anchorTick = tick;
|
|
142
|
+
this.anchorAt = anchorAt;
|
|
143
|
+
|
|
144
|
+
if (shown === null) {
|
|
145
|
+
this.offset = 0;
|
|
146
|
+
} else {
|
|
147
|
+
// Keep what a render loop sees continuous; `serverTick` decays the
|
|
148
|
+
// difference. A large step forward applies at once. A large step
|
|
149
|
+
// back applies only down to the newest tick received before this
|
|
150
|
+
// frame: past it the estimate had run ahead of every frame (a shard
|
|
151
|
+
// that stalled), so nothing drawn goes back.
|
|
152
|
+
const raw = this.raw(at);
|
|
153
|
+
this.offset = shown - raw;
|
|
154
|
+
if (Math.abs(this.offset) * tickMs > this.snapMs) {
|
|
155
|
+
this.offset = this.offset > 0 ? Math.max(0, Math.min(shown, newestBefore) - raw) : 0;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
this.slewedTo = Math.max(this.slewedTo, at);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The estimated tick the shard is on at `now` (ms, same clock as
|
|
163
|
+
* `observe`), with a fraction. Before the first frame, -1.
|
|
164
|
+
*/
|
|
165
|
+
serverTick(now: number): number {
|
|
166
|
+
if (!this.ready) return -1;
|
|
167
|
+
const elapsed = now - this.slewedTo;
|
|
168
|
+
if (elapsed > 0) {
|
|
169
|
+
// The correction runs faster the larger it is, from `slew` up to
|
|
170
|
+
// all of real time (the estimate stands still, or runs at double
|
|
171
|
+
// speed).
|
|
172
|
+
const offsetMs = Math.abs(this.offset) * this.tickMs;
|
|
173
|
+
const rate = Math.min(1, Math.max(this.slew, offsetMs / this.snapMs));
|
|
174
|
+
const step = (elapsed * rate) / this.tickMs;
|
|
175
|
+
this.offset -= Math.max(-step, Math.min(step, this.offset));
|
|
176
|
+
this.slewedTo = now;
|
|
177
|
+
}
|
|
178
|
+
return this.raw(now) + this.offset;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private raw(now: number): number {
|
|
182
|
+
return this.anchorTick + (now - this.anchorAt) / this.tickMs;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* The median slope of arrival time against tick over every pair of
|
|
187
|
+
* samples (Theil-Sen): frames that arrive together in a burst are a
|
|
188
|
+
* minority of pairs and do not pull it.
|
|
189
|
+
*/
|
|
190
|
+
private fitTickMs(): void {
|
|
191
|
+
const n = this.samples.length;
|
|
192
|
+
if (n < MIN_FIT_SAMPLES) return;
|
|
193
|
+
if (this.samples[n - 1].tick - this.samples[0].tick < 4) return;
|
|
194
|
+
const slopes: number[] = [];
|
|
195
|
+
for (let i = 0; i < n; i++) {
|
|
196
|
+
for (let j = i + 1; j < n; j++) {
|
|
197
|
+
const dt = this.samples[j].tick - this.samples[i].tick;
|
|
198
|
+
if (dt > 0) slopes.push((this.samples[j].at - this.samples[i].at) / dt);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (slopes.length === 0) return;
|
|
202
|
+
slopes.sort((a, b) => a - b);
|
|
203
|
+
const slope = slopes[slopes.length >> 1];
|
|
204
|
+
if (Number.isFinite(slope)) this.measuredTickMs = Math.min(2000, Math.max(1, slope));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { afterAll, beforeAll, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { connectShard } from "./connection";
|
|
4
|
+
|
|
5
|
+
const sockets: FakeWebSocket[] = [];
|
|
6
|
+
class FakeWebSocket {
|
|
7
|
+
static OPEN = 1;
|
|
8
|
+
readyState = 0;
|
|
9
|
+
binaryType = "blob";
|
|
10
|
+
onopen: (() => void) | null = null;
|
|
11
|
+
onmessage: ((e: { data: ArrayBuffer }) => void) | null = null;
|
|
12
|
+
onerror: (() => void) | null = null;
|
|
13
|
+
onclose: (() => void) | null = null;
|
|
14
|
+
constructor(
|
|
15
|
+
readonly url: string,
|
|
16
|
+
readonly protocols?: string[],
|
|
17
|
+
) {
|
|
18
|
+
sockets.push(this);
|
|
19
|
+
}
|
|
20
|
+
close() {
|
|
21
|
+
this.readyState = 3;
|
|
22
|
+
this.onclose?.();
|
|
23
|
+
}
|
|
24
|
+
send() {}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const realWebSocket = globalThis.WebSocket;
|
|
28
|
+
const realSetTimeout = globalThis.setTimeout;
|
|
29
|
+
const delays: number[] = [];
|
|
30
|
+
beforeAll(() => {
|
|
31
|
+
globalThis.WebSocket = FakeWebSocket as unknown as typeof WebSocket;
|
|
32
|
+
globalThis.setTimeout = ((fn: () => void, ms: number) => {
|
|
33
|
+
delays.push(ms);
|
|
34
|
+
queueMicrotask(fn);
|
|
35
|
+
return 0;
|
|
36
|
+
}) as unknown as typeof setTimeout;
|
|
37
|
+
});
|
|
38
|
+
afterAll(() => {
|
|
39
|
+
globalThis.WebSocket = realWebSocket;
|
|
40
|
+
globalThis.setTimeout = realSetTimeout;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const settle = () => new Promise((r) => realSetTimeout(r, 0));
|
|
44
|
+
|
|
45
|
+
test("a ticket function gives each connection attempt a new ticket", async () => {
|
|
46
|
+
let n = 0;
|
|
47
|
+
const errors: string[] = [];
|
|
48
|
+
const client = connectShard("zone", {
|
|
49
|
+
subscriberId: "p1",
|
|
50
|
+
baseUrl: "h",
|
|
51
|
+
ticket: async () => {
|
|
52
|
+
n += 1;
|
|
53
|
+
if (n === 2) throw new Error("ticket service down");
|
|
54
|
+
return `t${n}`;
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
client.onError((e) => errors.push(e.message));
|
|
58
|
+
await settle();
|
|
59
|
+
expect(sockets[0].protocols).toEqual(["ticket.t1"]);
|
|
60
|
+
|
|
61
|
+
// The socket drops; the next ticket fails, so it retries with backoff.
|
|
62
|
+
sockets[0].close();
|
|
63
|
+
await settle();
|
|
64
|
+
await settle();
|
|
65
|
+
expect(errors).toContain("ticket service down");
|
|
66
|
+
expect(sockets.length).toBe(2);
|
|
67
|
+
expect(sockets[1].protocols).toEqual(["ticket.t3"]);
|
|
68
|
+
expect(delays).toEqual([500, 1000]);
|
|
69
|
+
client.close();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("send returns 0 and sends nothing while the socket is not open", () => {
|
|
73
|
+
const errors: string[] = [];
|
|
74
|
+
const client = connectShard("zone", { subscriberId: "p1", baseUrl: "h", ticket: "fixed" });
|
|
75
|
+
client.onError((e) => errors.push(e.message));
|
|
76
|
+
expect(sockets[sockets.length - 1].protocols).toEqual(["ticket.fixed"]);
|
|
77
|
+
expect(client.send({ a: 1 })).toBe(0);
|
|
78
|
+
expect(errors[0]).toContain("not open");
|
|
79
|
+
client.close();
|
|
80
|
+
});
|