@stream-io/video-client 1.54.1-beta.0 → 1.55.1
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/CHANGELOG.md +21 -0
- package/dist/index.browser.es.js +9700 -8873
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +9707 -8880
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +9708 -8881
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +4 -4
- package/dist/src/StreamSfuClient.d.ts +11 -3
- package/dist/src/coordinator/connection/connection.d.ts +2 -1
- package/dist/src/reporting/ClientEventReporter.d.ts +1 -0
- package/dist/src/rtc/BasePeerConnection.d.ts +2 -12
- package/dist/src/rtc/IceTrickleBuffer.d.ts +41 -3
- package/dist/src/rtc/Publisher.d.ts +1 -1
- package/dist/src/rtc/Subscriber.d.ts +2 -1
- package/dist/src/rtc/helpers/iceCandiates.d.ts +12 -0
- package/dist/src/rtc/types.d.ts +3 -0
- package/dist/src/stats/SfuStatsReporter.d.ts +32 -1
- package/dist/src/stats/rtc/StatsTracer.d.ts +38 -8
- package/dist/src/stats/rtc/Tracer.d.ts +9 -2
- package/dist/src/stats/rtc/types.d.ts +10 -4
- package/package.json +5 -3
- package/src/Call.ts +47 -44
- package/src/StreamSfuClient.ts +36 -21
- package/src/__tests__/StreamSfuClient.test.ts +159 -1
- package/src/__tests__/StreamVideoClient.api.test.ts +123 -97
- package/src/coordinator/connection/__tests__/connection.test.ts +69 -0
- package/src/coordinator/connection/connection.ts +28 -13
- package/src/gen/video/sfu/event/events.ts +0 -1
- package/src/gen/video/sfu/models/models.ts +0 -1
- package/src/gen/video/sfu/signal_rpc/signal.client.ts +0 -1
- package/src/gen/video/sfu/signal_rpc/signal.ts +0 -1
- package/src/helpers/MediaPlaybackWatchdog.ts +1 -0
- package/src/helpers/__tests__/browsers.test.ts +12 -12
- package/src/helpers/browsers.ts +5 -5
- package/src/helpers/client-details.ts +1 -1
- package/src/reporting/ClientEventReporter.ts +17 -12
- package/src/reporting/__tests__/ClientEventReporter.test.ts +52 -0
- package/src/rtc/BasePeerConnection.ts +15 -34
- package/src/rtc/IceTrickleBuffer.ts +105 -12
- package/src/rtc/Publisher.ts +26 -19
- package/src/rtc/Subscriber.ts +71 -37
- package/src/rtc/__tests__/Call.reconnect.test.ts +45 -45
- package/src/rtc/__tests__/IceTrickleBuffer.test.ts +127 -0
- package/src/rtc/__tests__/Publisher.test.ts +76 -31
- package/src/rtc/__tests__/Subscriber.test.ts +271 -20
- package/src/rtc/helpers/__tests__/iceCandiates.test.ts +88 -0
- package/src/rtc/helpers/degradationPreference.ts +1 -0
- package/src/rtc/helpers/iceCandiates.ts +35 -0
- package/src/rtc/helpers/sdp.ts +3 -2
- package/src/rtc/helpers/tracks.ts +2 -0
- package/src/rtc/types.ts +3 -0
- package/src/stats/SfuStatsReporter.ts +149 -49
- package/src/stats/__tests__/SfuStatsReporter.test.ts +235 -0
- package/src/stats/rtc/StatsTracer.ts +90 -32
- package/src/stats/rtc/Tracer.ts +23 -2
- package/src/stats/rtc/__tests__/StatsTracer.test.ts +213 -6
- package/src/stats/rtc/__tests__/Tracer.test.ts +34 -0
- package/src/stats/rtc/types.ts +11 -4
package/src/stats/rtc/Tracer.ts
CHANGED
|
@@ -9,11 +9,13 @@ import type {
|
|
|
9
9
|
export class Tracer {
|
|
10
10
|
private buffer: TraceRecord[] = [];
|
|
11
11
|
private enabled = true;
|
|
12
|
-
|
|
12
|
+
readonly id: string | null;
|
|
13
|
+
private readonly maxBuffer: number;
|
|
13
14
|
private keys?: Map<TraceKey, boolean>;
|
|
14
15
|
|
|
15
|
-
constructor(id: string | null) {
|
|
16
|
+
constructor(id: string | null, maxBuffer: number = 2500) {
|
|
16
17
|
this.id = id;
|
|
18
|
+
this.maxBuffer = maxBuffer;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
setEnabled = (enabled: boolean) => {
|
|
@@ -25,6 +27,7 @@ export class Tracer {
|
|
|
25
27
|
trace: Trace = (tag, data) => {
|
|
26
28
|
if (!this.enabled) return;
|
|
27
29
|
this.buffer.push([tag, this.id, data, Date.now()]);
|
|
30
|
+
this.capBuffer();
|
|
28
31
|
};
|
|
29
32
|
|
|
30
33
|
traceOnce = (key: TraceKey, tag: string, data: RTCStatsDataType) => {
|
|
@@ -44,6 +47,7 @@ export class Tracer {
|
|
|
44
47
|
snapshot,
|
|
45
48
|
rollback: () => {
|
|
46
49
|
this.buffer.unshift(...snapshot);
|
|
50
|
+
this.capBuffer();
|
|
47
51
|
},
|
|
48
52
|
};
|
|
49
53
|
};
|
|
@@ -52,4 +56,21 @@ export class Tracer {
|
|
|
52
56
|
this.buffer = [];
|
|
53
57
|
this.keys?.clear();
|
|
54
58
|
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Bounds the buffer to 2500 records by dropping the oldest ones,
|
|
62
|
+
* leaving a single `traceBufferOverflow` breadcrumb at the front so the
|
|
63
|
+
* consumer knows records were dropped.
|
|
64
|
+
*/
|
|
65
|
+
private capBuffer = () => {
|
|
66
|
+
const overflow = this.buffer.length - this.maxBuffer;
|
|
67
|
+
if (overflow <= 0) return;
|
|
68
|
+
this.buffer.splice(0, overflow);
|
|
69
|
+
this.buffer[0] = [
|
|
70
|
+
'traceBufferOverflow',
|
|
71
|
+
this.id,
|
|
72
|
+
{ dropped: overflow },
|
|
73
|
+
Date.now(),
|
|
74
|
+
];
|
|
75
|
+
};
|
|
55
76
|
}
|
|
@@ -43,7 +43,8 @@ describe('StatsTracer timestamp drift correction', () => {
|
|
|
43
43
|
THRESHOLD_MS,
|
|
44
44
|
);
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
await tracer.takeSample();
|
|
47
|
+
const { delta } = tracer.getPendingDeltas().at(-1)!;
|
|
47
48
|
|
|
48
49
|
expect(delta.timestamp).toBe(WALL_NOW + 2000);
|
|
49
50
|
});
|
|
@@ -60,7 +61,8 @@ describe('StatsTracer timestamp drift correction', () => {
|
|
|
60
61
|
THRESHOLD_MS,
|
|
61
62
|
);
|
|
62
63
|
|
|
63
|
-
|
|
64
|
+
await tracer.takeSample();
|
|
65
|
+
const { delta } = tracer.getPendingDeltas().at(-1)!;
|
|
64
66
|
|
|
65
67
|
expect(delta.timestamp).toBe(WALL_NOW);
|
|
66
68
|
});
|
|
@@ -77,7 +79,8 @@ describe('StatsTracer timestamp drift correction', () => {
|
|
|
77
79
|
THRESHOLD_MS,
|
|
78
80
|
);
|
|
79
81
|
|
|
80
|
-
|
|
82
|
+
await tracer.takeSample();
|
|
83
|
+
const { delta } = tracer.getPendingDeltas().at(-1)!;
|
|
81
84
|
|
|
82
85
|
expect(delta['b'].timestamp).toBe(WALL_NOW);
|
|
83
86
|
expect(delta.timestamp).toBe(WALL_NOW + 2000);
|
|
@@ -95,7 +98,8 @@ describe('StatsTracer timestamp drift correction', () => {
|
|
|
95
98
|
THRESHOLD_MS,
|
|
96
99
|
);
|
|
97
100
|
|
|
98
|
-
|
|
101
|
+
await tracer.takeSample();
|
|
102
|
+
const { delta } = tracer.getPendingDeltas().at(-1)!;
|
|
99
103
|
|
|
100
104
|
expect(delta.timestamp).toBe(WALL_NOW + THRESHOLD_MS);
|
|
101
105
|
});
|
|
@@ -127,7 +131,8 @@ describe('StatsTracer timestamp drift correction', () => {
|
|
|
127
131
|
THRESHOLD_MS,
|
|
128
132
|
);
|
|
129
133
|
|
|
130
|
-
|
|
134
|
+
await tracer.takeSample();
|
|
135
|
+
const { delta } = tracer.getPendingDeltas().at(-1)!;
|
|
131
136
|
|
|
132
137
|
// both stale and future drift get clamped to wall time, while the
|
|
133
138
|
// within-threshold anchor stays the delta's top-level timestamp.
|
|
@@ -148,8 +153,210 @@ describe('StatsTracer timestamp drift correction', () => {
|
|
|
148
153
|
0,
|
|
149
154
|
);
|
|
150
155
|
|
|
151
|
-
|
|
156
|
+
await tracer.takeSample();
|
|
157
|
+
const { delta } = tracer.getPendingDeltas().at(-1)!;
|
|
152
158
|
|
|
153
159
|
expect(delta.timestamp).toBe(WALL_NOW + 99_999);
|
|
154
160
|
});
|
|
155
161
|
});
|
|
162
|
+
|
|
163
|
+
// Builds an RTCStatsReport from rich per-report fields (id is added for you).
|
|
164
|
+
const richReport = (
|
|
165
|
+
entries: Record<string, Record<string, unknown>>,
|
|
166
|
+
): RTCStatsReport => {
|
|
167
|
+
const map = new Map<string, RTCStats>();
|
|
168
|
+
for (const [id, fields] of Object.entries(entries)) {
|
|
169
|
+
map.set(id, { id, ...fields } as unknown as RTCStats);
|
|
170
|
+
}
|
|
171
|
+
return map as unknown as RTCStatsReport;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// A pc mock whose getStats() returns a different report on each successive call.
|
|
175
|
+
const makeSeqPc = (reports: RTCStatsReport[]): RTCPeerConnection => {
|
|
176
|
+
const getStats = vi.fn();
|
|
177
|
+
for (const r of reports) getStats.mockResolvedValueOnce(r);
|
|
178
|
+
return { getStats } as unknown as RTCPeerConnection;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// Mirrors the server-side accumulator: applies chained deltas in order,
|
|
182
|
+
// un-zeroing each report's hoisted timestamp.
|
|
183
|
+
const applyChain = (
|
|
184
|
+
baseline: Record<string, any>,
|
|
185
|
+
deltas: Array<Record<string, any>>,
|
|
186
|
+
): Record<string, any> => {
|
|
187
|
+
const acc: Record<string, any> = JSON.parse(JSON.stringify(baseline));
|
|
188
|
+
for (const delta of deltas) {
|
|
189
|
+
const top = delta.timestamp;
|
|
190
|
+
for (const [id, report] of Object.entries(delta)) {
|
|
191
|
+
if (id === 'timestamp') continue;
|
|
192
|
+
const target = (acc[id] ??= {});
|
|
193
|
+
for (const [k, v] of Object.entries(report as Record<string, unknown>)) {
|
|
194
|
+
target[k] = k === 'timestamp' && v === 0 ? top : v;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return acc;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
describe('StatsTracer pending delta chain', () => {
|
|
202
|
+
beforeEach(() => {
|
|
203
|
+
vi.useFakeTimers();
|
|
204
|
+
vi.setSystemTime(WALL_NOW);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
afterEach(() => {
|
|
208
|
+
vi.useRealTimers();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('accumulates one delta per get() in the pending chain', async () => {
|
|
212
|
+
const pc = makeSeqPc([
|
|
213
|
+
makeReport([{ id: 'a', timestamp: WALL_NOW }]),
|
|
214
|
+
makeReport([{ id: 'a', timestamp: WALL_NOW + 1000 }]),
|
|
215
|
+
]);
|
|
216
|
+
const tracer = new StatsTracer(pc, PeerType.SUBSCRIBER, new Map(), 0);
|
|
217
|
+
|
|
218
|
+
await tracer.takeSample();
|
|
219
|
+
await tracer.takeSample();
|
|
220
|
+
|
|
221
|
+
expect(tracer.getPendingDeltas()).toHaveLength(2);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('commitDeltas removes exactly the committed deltas by identity', async () => {
|
|
225
|
+
const pc = makeSeqPc([
|
|
226
|
+
makeReport([{ id: 'a', timestamp: WALL_NOW }]),
|
|
227
|
+
makeReport([{ id: 'a', timestamp: WALL_NOW + 1000 }]),
|
|
228
|
+
]);
|
|
229
|
+
const tracer = new StatsTracer(pc, PeerType.SUBSCRIBER, new Map(), 0);
|
|
230
|
+
|
|
231
|
+
await tracer.takeSample();
|
|
232
|
+
await tracer.takeSample();
|
|
233
|
+
const sent = tracer.getPendingDeltas();
|
|
234
|
+
tracer.commitDeltas(sent.slice(0, 1));
|
|
235
|
+
|
|
236
|
+
const remaining = tracer.getPendingDeltas();
|
|
237
|
+
expect(remaining).toHaveLength(1);
|
|
238
|
+
expect(remaining[0]).toBe(sent[1]);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('clearPendingDeltas empties the chain', async () => {
|
|
242
|
+
const pc = makeSeqPc([
|
|
243
|
+
makeReport([{ id: 'a', timestamp: WALL_NOW }]),
|
|
244
|
+
makeReport([{ id: 'a', timestamp: WALL_NOW + 1000 }]),
|
|
245
|
+
]);
|
|
246
|
+
const tracer = new StatsTracer(pc, PeerType.SUBSCRIBER, new Map(), 0);
|
|
247
|
+
|
|
248
|
+
await tracer.takeSample();
|
|
249
|
+
await tracer.takeSample();
|
|
250
|
+
tracer.clearPendingDeltas();
|
|
251
|
+
|
|
252
|
+
expect(tracer.getPendingDeltas()).toHaveLength(0);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('reconstructs every sample by applying the un-acked chain in order', async () => {
|
|
256
|
+
const s0 = richReport({ a: { timestamp: WALL_NOW, bytes: 100 } });
|
|
257
|
+
const s1 = richReport({ a: { timestamp: WALL_NOW + 1000, bytes: 200 } });
|
|
258
|
+
const s2 = richReport({ a: { timestamp: WALL_NOW + 2000, bytes: 350 } });
|
|
259
|
+
const tracer = new StatsTracer(
|
|
260
|
+
makeSeqPc([s0, s1, s2]),
|
|
261
|
+
PeerType.SUBSCRIBER,
|
|
262
|
+
new Map(),
|
|
263
|
+
0,
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
await tracer.takeSample();
|
|
267
|
+
await tracer.takeSample();
|
|
268
|
+
await tracer.takeSample();
|
|
269
|
+
const chain = tracer.getPendingDeltas().map((p) => p.delta);
|
|
270
|
+
|
|
271
|
+
expect(applyChain({}, chain)).toEqual({
|
|
272
|
+
a: { timestamp: WALL_NOW + 2000, bytes: 350 },
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('re-anchors with a full snapshot when the chain exceeds the cap', async () => {
|
|
277
|
+
const s0 = richReport({ a: { timestamp: WALL_NOW, bytes: 100 } });
|
|
278
|
+
const s1 = richReport({ a: { timestamp: WALL_NOW + 1000, bytes: 200 } });
|
|
279
|
+
const s2 = richReport({ a: { timestamp: WALL_NOW + 2000, bytes: 350 } });
|
|
280
|
+
const tracer = new StatsTracer(
|
|
281
|
+
makeSeqPc([s0, s1, s2]),
|
|
282
|
+
PeerType.SUBSCRIBER,
|
|
283
|
+
new Map(),
|
|
284
|
+
0,
|
|
285
|
+
2, // maxPendingDeltas
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
await tracer.takeSample();
|
|
289
|
+
await tracer.takeSample(); // chain now at the cap
|
|
290
|
+
await tracer.takeSample(); // exceeds cap -> re-anchor
|
|
291
|
+
|
|
292
|
+
const chain = tracer.getPendingDeltas();
|
|
293
|
+
expect(chain).toHaveLength(1);
|
|
294
|
+
// the single re-anchored delta is a full snapshot of S2
|
|
295
|
+
expect(applyChain({}, [chain[0].delta])).toEqual({
|
|
296
|
+
a: { timestamp: WALL_NOW + 2000, bytes: 350 },
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('keeps a re-anchored keyframe when a stale commit lands afterwards', async () => {
|
|
301
|
+
const s0 = richReport({ a: { timestamp: WALL_NOW, bytes: 100 } });
|
|
302
|
+
const s1 = richReport({ a: { timestamp: WALL_NOW + 1000, bytes: 200 } });
|
|
303
|
+
const s2 = richReport({ a: { timestamp: WALL_NOW + 2000, bytes: 350 } });
|
|
304
|
+
const tracer = new StatsTracer(
|
|
305
|
+
makeSeqPc([s0, s1, s2]),
|
|
306
|
+
PeerType.SUBSCRIBER,
|
|
307
|
+
new Map(),
|
|
308
|
+
0,
|
|
309
|
+
2,
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
await tracer.takeSample();
|
|
313
|
+
await tracer.takeSample();
|
|
314
|
+
const sent = tracer.getPendingDeltas(); // captured before re-anchor
|
|
315
|
+
await tracer.takeSample(); // re-anchors, dropping `sent`
|
|
316
|
+
tracer.commitDeltas(sent); // stale commit, must be a no-op
|
|
317
|
+
|
|
318
|
+
expect(tracer.getPendingDeltas()).toHaveLength(1);
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const deferred = <T>() => {
|
|
323
|
+
let resolve!: (v: T) => void;
|
|
324
|
+
const promise = new Promise<T>((res) => {
|
|
325
|
+
resolve = res;
|
|
326
|
+
});
|
|
327
|
+
return { promise, resolve };
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
describe('StatsTracer concurrent sampling', () => {
|
|
331
|
+
it('serializes overlapping get() calls so the chain stays in call order', async () => {
|
|
332
|
+
const dA = deferred<RTCStatsReport>();
|
|
333
|
+
const dB = deferred<RTCStatsReport>();
|
|
334
|
+
const getStats = vi
|
|
335
|
+
.fn()
|
|
336
|
+
.mockReturnValueOnce(dA.promise)
|
|
337
|
+
.mockReturnValueOnce(dB.promise);
|
|
338
|
+
const pc = { getStats } as unknown as RTCPeerConnection;
|
|
339
|
+
const tracer = new StatsTracer(pc, PeerType.SUBSCRIBER, new Map(), 0);
|
|
340
|
+
|
|
341
|
+
const pA = tracer.takeSample();
|
|
342
|
+
const pB = tracer.takeSample();
|
|
343
|
+
|
|
344
|
+
// the second sample must not start until the first completes
|
|
345
|
+
expect(getStats).toHaveBeenCalledTimes(1);
|
|
346
|
+
|
|
347
|
+
// resolve the first sample; only then should the second getStats fire
|
|
348
|
+
dA.resolve(richReport({ a: { timestamp: 1000, bytes: 100 } }));
|
|
349
|
+
await pA;
|
|
350
|
+
expect(getStats).toHaveBeenCalledTimes(2);
|
|
351
|
+
|
|
352
|
+
dB.resolve(richReport({ a: { timestamp: 2000, bytes: 200 } }));
|
|
353
|
+
await pB;
|
|
354
|
+
|
|
355
|
+
// chain applied in call order reconstructs the second (newer) sample
|
|
356
|
+
const chain = tracer.getPendingDeltas().map((p) => p.delta);
|
|
357
|
+
expect(chain).toHaveLength(2);
|
|
358
|
+
expect(applyChain({}, chain)).toEqual({
|
|
359
|
+
a: { timestamp: 2000, bytes: 200 },
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
});
|
|
@@ -54,4 +54,38 @@ describe('Tracer', () => {
|
|
|
54
54
|
const slice = tracer.take();
|
|
55
55
|
expect(slice.snapshot.length).toBe(0);
|
|
56
56
|
});
|
|
57
|
+
|
|
58
|
+
it('exposes its id via .id', () => {
|
|
59
|
+
expect(new Tracer('abc').id).toBe('abc');
|
|
60
|
+
expect(new Tracer(null).id).toBeNull();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('caps the buffer, dropping oldest and leaving an overflow marker', () => {
|
|
64
|
+
const capped = new Tracer('id', 3);
|
|
65
|
+
capped.trace('e', { n: 1 });
|
|
66
|
+
capped.trace('e', { n: 2 });
|
|
67
|
+
capped.trace('e', { n: 3 });
|
|
68
|
+
capped.trace('e', { n: 4 }); // overflows the cap of 3
|
|
69
|
+
|
|
70
|
+
const slice = capped.take();
|
|
71
|
+
expect(slice.snapshot.length).toBe(3);
|
|
72
|
+
expect(slice.snapshot[0][0]).toBe('traceBufferOverflow');
|
|
73
|
+
// newest record is retained
|
|
74
|
+
expect(slice.snapshot[slice.snapshot.length - 1][2]).toEqual({ n: 4 });
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('caps the buffer on rollback too', () => {
|
|
78
|
+
const capped = new Tracer('id', 3);
|
|
79
|
+
capped.trace('e', { n: 1 });
|
|
80
|
+
const slice = capped.take();
|
|
81
|
+
|
|
82
|
+
capped.trace('e', { n: 2 });
|
|
83
|
+
capped.trace('e', { n: 3 });
|
|
84
|
+
capped.trace('e', { n: 4 }); // buffer now at the cap of 3
|
|
85
|
+
slice.rollback(); // prepends the old record, exceeding the cap
|
|
86
|
+
|
|
87
|
+
const after = capped.take();
|
|
88
|
+
expect(after.snapshot.length).toBe(3);
|
|
89
|
+
expect(after.snapshot[0][0]).toBe('traceBufferOverflow');
|
|
90
|
+
});
|
|
57
91
|
});
|
package/src/stats/rtc/types.ts
CHANGED
|
@@ -40,12 +40,19 @@ export type ComputedStats = {
|
|
|
40
40
|
* Current stats from the RTCPeerConnection.
|
|
41
41
|
*/
|
|
42
42
|
stats: RTCStatsReport;
|
|
43
|
-
/**
|
|
44
|
-
* Delta between the current stats and the previous stats.
|
|
45
|
-
*/
|
|
46
|
-
delta: Record<any, any>;
|
|
47
43
|
/**
|
|
48
44
|
* The current iteration of the stats.
|
|
49
45
|
*/
|
|
50
46
|
performanceStats: PerformanceStats[];
|
|
51
47
|
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A single, not-yet-delivered delta-compressed stats sample.
|
|
51
|
+
* The `delta` is computed against the immediately preceding sample, so a
|
|
52
|
+
* sequence of `PendingDelta`s forms a chain that the server applies in order
|
|
53
|
+
* onto its running accumulator. `ts` is the wall-clock time the sample was taken.
|
|
54
|
+
*/
|
|
55
|
+
export type PendingDelta = {
|
|
56
|
+
delta: Record<any, any>;
|
|
57
|
+
ts: number;
|
|
58
|
+
};
|