@moqtap/trace 0.1.3 → 0.3.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 +95 -0
- package/SPEC.md +388 -102
- package/dist/index.cjs +463 -54
- package/dist/index.d.cts +260 -5
- package/dist/index.d.ts +260 -5
- package/dist/index.js +462 -53
- package/package.json +2 -2
- package/src/binary.ts +469 -57
- package/src/cbor-scan.ts +185 -0
- package/src/index.ts +18 -1
- package/src/recorder.ts +10 -0
- package/src/types.ts +207 -2
package/src/cbor-scan.ts
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Measure one CBOR item without decoding it.
|
|
3
|
+
*
|
|
4
|
+
* A `.moqtrace` file is a CBOR sequence, and a segmented one splices a
|
|
5
|
+
* preamble between two of its items. Finding those boundaries means knowing
|
|
6
|
+
* where each item ends — and knowing it *only* at real item boundaries. A
|
|
7
|
+
* plain byte scan for the magic would also match the same eight bytes sitting
|
|
8
|
+
* inside a captured payload, and would split a trace that was never segmented.
|
|
9
|
+
*
|
|
10
|
+
* Measuring is cheaper than decoding: a byte or text string is skipped by its
|
|
11
|
+
* declared length rather than read, so the cost follows the structure of the
|
|
12
|
+
* data and not its size.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** The item at the given offset ran past the end of the buffer. */
|
|
16
|
+
export const INCOMPLETE = null
|
|
17
|
+
|
|
18
|
+
/** Thrown when the bytes are not a well-formed CBOR item at all. */
|
|
19
|
+
export class MalformedCborError extends Error {
|
|
20
|
+
readonly offset: number
|
|
21
|
+
|
|
22
|
+
constructor(message: string, offset: number) {
|
|
23
|
+
super(`${message} at byte ${offset}`)
|
|
24
|
+
this.name = 'MalformedCborError'
|
|
25
|
+
this.offset = offset
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const MAJOR_UNSIGNED = 0
|
|
30
|
+
const MAJOR_NEGATIVE = 1
|
|
31
|
+
const MAJOR_BYTES = 2
|
|
32
|
+
const MAJOR_TEXT = 3
|
|
33
|
+
const MAJOR_ARRAY = 4
|
|
34
|
+
const MAJOR_MAP = 5
|
|
35
|
+
const MAJOR_TAG = 6
|
|
36
|
+
const MAJOR_SIMPLE = 7
|
|
37
|
+
|
|
38
|
+
const AI_ONE_BYTE = 24
|
|
39
|
+
const AI_TWO_BYTES = 25
|
|
40
|
+
const AI_FOUR_BYTES = 26
|
|
41
|
+
const AI_EIGHT_BYTES = 27
|
|
42
|
+
const AI_INDEFINITE = 31
|
|
43
|
+
const BREAK = 0xff
|
|
44
|
+
|
|
45
|
+
/** Bytes that follow the initial byte for each additional-information value. */
|
|
46
|
+
function headerExtraBytes(additionalInfo: number, offset: number): number {
|
|
47
|
+
if (additionalInfo < AI_ONE_BYTE) return 0
|
|
48
|
+
switch (additionalInfo) {
|
|
49
|
+
case AI_ONE_BYTE:
|
|
50
|
+
return 1
|
|
51
|
+
case AI_TWO_BYTES:
|
|
52
|
+
return 2
|
|
53
|
+
case AI_FOUR_BYTES:
|
|
54
|
+
return 4
|
|
55
|
+
case AI_EIGHT_BYTES:
|
|
56
|
+
return 8
|
|
57
|
+
case AI_INDEFINITE:
|
|
58
|
+
return 0
|
|
59
|
+
default:
|
|
60
|
+
throw new MalformedCborError(`reserved additional information ${additionalInfo}`, offset)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Read the argument encoded in the item header. */
|
|
65
|
+
function readArgument(bytes: Uint8Array, offset: number, additionalInfo: number): number {
|
|
66
|
+
if (additionalInfo < AI_ONE_BYTE) return additionalInfo
|
|
67
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
|
|
68
|
+
switch (additionalInfo) {
|
|
69
|
+
case AI_ONE_BYTE:
|
|
70
|
+
return view.getUint8(offset + 1)
|
|
71
|
+
case AI_TWO_BYTES:
|
|
72
|
+
return view.getUint16(offset + 1, false)
|
|
73
|
+
case AI_FOUR_BYTES:
|
|
74
|
+
return view.getUint32(offset + 1, false)
|
|
75
|
+
case AI_EIGHT_BYTES: {
|
|
76
|
+
const value = view.getBigUint64(offset + 1, false)
|
|
77
|
+
// A length this large cannot be indexed anyway; treating it as a number
|
|
78
|
+
// is safe because the caller only ever compares it against the buffer.
|
|
79
|
+
return Number(value)
|
|
80
|
+
}
|
|
81
|
+
default:
|
|
82
|
+
throw new MalformedCborError(`reserved additional information ${additionalInfo}`, offset)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Advance past the item at `offset`, returning the offset just after it, or
|
|
88
|
+
* {@link INCOMPLETE} if the buffer ends part-way through.
|
|
89
|
+
*
|
|
90
|
+
* @param allowBreak whether a break code is a legal item here — true only
|
|
91
|
+
* directly inside an indefinite-length container.
|
|
92
|
+
*/
|
|
93
|
+
function skipItem(bytes: Uint8Array, offset: number, allowBreak: boolean): number | null {
|
|
94
|
+
if (offset >= bytes.length) return INCOMPLETE
|
|
95
|
+
|
|
96
|
+
const initial = bytes[offset] as number
|
|
97
|
+
if (initial === BREAK) {
|
|
98
|
+
if (!allowBreak) {
|
|
99
|
+
throw new MalformedCborError('break outside an indefinite-length item', offset)
|
|
100
|
+
}
|
|
101
|
+
return offset + 1
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const major = initial >> 5
|
|
105
|
+
const additionalInfo = initial & 0x1f
|
|
106
|
+
const extra = headerExtraBytes(additionalInfo, offset)
|
|
107
|
+
if (offset + 1 + extra > bytes.length) return INCOMPLETE
|
|
108
|
+
|
|
109
|
+
const indefinite = additionalInfo === AI_INDEFINITE
|
|
110
|
+
if (indefinite && (major === MAJOR_UNSIGNED || major === MAJOR_NEGATIVE || major === MAJOR_TAG)) {
|
|
111
|
+
throw new MalformedCborError(`major type ${major} cannot be indefinite-length`, offset)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const afterHeader = offset + 1 + extra
|
|
115
|
+
|
|
116
|
+
switch (major) {
|
|
117
|
+
case MAJOR_UNSIGNED:
|
|
118
|
+
case MAJOR_NEGATIVE:
|
|
119
|
+
return afterHeader
|
|
120
|
+
|
|
121
|
+
case MAJOR_BYTES:
|
|
122
|
+
case MAJOR_TEXT: {
|
|
123
|
+
if (!indefinite) {
|
|
124
|
+
const length = readArgument(bytes, offset, additionalInfo)
|
|
125
|
+
const end = afterHeader + length
|
|
126
|
+
return end > bytes.length ? INCOMPLETE : end
|
|
127
|
+
}
|
|
128
|
+
// Indefinite strings are a run of definite-length chunks, then a break.
|
|
129
|
+
let cursor = afterHeader
|
|
130
|
+
for (;;) {
|
|
131
|
+
if (cursor >= bytes.length) return INCOMPLETE
|
|
132
|
+
if (bytes[cursor] === BREAK) return cursor + 1
|
|
133
|
+
const next = skipItem(bytes, cursor, false)
|
|
134
|
+
if (next === INCOMPLETE) return INCOMPLETE
|
|
135
|
+
cursor = next
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
case MAJOR_ARRAY:
|
|
140
|
+
case MAJOR_MAP: {
|
|
141
|
+
const perEntry = major === MAJOR_MAP ? 2 : 1
|
|
142
|
+
if (!indefinite) {
|
|
143
|
+
const count = readArgument(bytes, offset, additionalInfo) * perEntry
|
|
144
|
+
let cursor = afterHeader
|
|
145
|
+
for (let i = 0; i < count; i++) {
|
|
146
|
+
const next = skipItem(bytes, cursor, false)
|
|
147
|
+
if (next === INCOMPLETE) return INCOMPLETE
|
|
148
|
+
cursor = next
|
|
149
|
+
}
|
|
150
|
+
return cursor
|
|
151
|
+
}
|
|
152
|
+
let cursor = afterHeader
|
|
153
|
+
for (;;) {
|
|
154
|
+
if (cursor >= bytes.length) return INCOMPLETE
|
|
155
|
+
if (bytes[cursor] === BREAK) return cursor + 1
|
|
156
|
+
for (let i = 0; i < perEntry; i++) {
|
|
157
|
+
const next = skipItem(bytes, cursor, false)
|
|
158
|
+
if (next === INCOMPLETE) return INCOMPLETE
|
|
159
|
+
cursor = next
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
case MAJOR_TAG:
|
|
165
|
+
return skipItem(bytes, afterHeader, false)
|
|
166
|
+
|
|
167
|
+
case MAJOR_SIMPLE:
|
|
168
|
+
// Floats and simple values carry no content beyond the header.
|
|
169
|
+
return afterHeader
|
|
170
|
+
|
|
171
|
+
default:
|
|
172
|
+
throw new MalformedCborError(`unknown major type ${major}`, offset)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Byte length of the CBOR item beginning at `start`, or {@link INCOMPLETE} if
|
|
178
|
+
* the buffer ends before the item does.
|
|
179
|
+
*
|
|
180
|
+
* @throws {MalformedCborError} if the bytes are not a well-formed item.
|
|
181
|
+
*/
|
|
182
|
+
export function cborItemLength(bytes: Uint8Array, start: number): number | null {
|
|
183
|
+
const end = skipItem(bytes, start, false)
|
|
184
|
+
return end === INCOMPLETE ? INCOMPLETE : end - start
|
|
185
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
// Binary .moqtrace format
|
|
2
2
|
|
|
3
|
-
export type { MoqtraceWriter } from './binary.js'
|
|
3
|
+
export type { MoqtraceWriter, ReadOptions } from './binary.js'
|
|
4
4
|
export {
|
|
5
5
|
createMoqtraceWriter,
|
|
6
|
+
FORMAT_VERSION,
|
|
6
7
|
readMoqtrace,
|
|
7
8
|
readMoqtraceHeader,
|
|
9
|
+
readMoqtraceSegments,
|
|
10
|
+
SUPPORTED_VERSIONS,
|
|
11
|
+
TruncatedTraceError,
|
|
8
12
|
writeMoqtrace,
|
|
13
|
+
writeMoqtraceSegments,
|
|
9
14
|
} from './binary.js'
|
|
10
15
|
// JSON (convenience)
|
|
11
16
|
export { traceToJSON } from './json.js'
|
|
@@ -17,16 +22,28 @@ export { createRecorder } from './recorder.js'
|
|
|
17
22
|
export type {
|
|
18
23
|
AnnotationEvent,
|
|
19
24
|
ControlMessageEvent,
|
|
25
|
+
DerivationKind,
|
|
20
26
|
DetailLevel,
|
|
27
|
+
DropPolicy,
|
|
21
28
|
ObjectHeaderEvent,
|
|
22
29
|
ObjectPayloadEvent,
|
|
30
|
+
PeerConnectedEvent,
|
|
31
|
+
PeerDisconnectedEvent,
|
|
32
|
+
PeerRole,
|
|
23
33
|
Perspective,
|
|
24
34
|
RecorderOptions,
|
|
35
|
+
SamplingInfo,
|
|
36
|
+
SegmentInfo,
|
|
37
|
+
Side,
|
|
25
38
|
StateChangeEvent,
|
|
26
39
|
StreamClosedEvent,
|
|
27
40
|
StreamOpenedEvent,
|
|
41
|
+
SubscriptionDerivationEvent,
|
|
42
|
+
SubscriptionRef,
|
|
28
43
|
Trace,
|
|
29
44
|
TraceErrorEvent,
|
|
30
45
|
TraceEvent,
|
|
31
46
|
TraceHeader,
|
|
47
|
+
UnknownEvent,
|
|
32
48
|
} from './types.js'
|
|
49
|
+
export { TRACE_ID_LENGTH } from './types.js'
|
package/src/recorder.ts
CHANGED
|
@@ -58,6 +58,16 @@ const DETAIL_RANK: Record<DetailLevel, number> = {
|
|
|
58
58
|
export function createRecorder(options: RecorderOptions): TraceRecorder {
|
|
59
59
|
const detail = options.detail
|
|
60
60
|
const detailRank = DETAIL_RANK[detail]
|
|
61
|
+
// A reader must tolerate a detail level it does not know; a recorder cannot.
|
|
62
|
+
// Asked for a level this build does not implement, it would either capture
|
|
63
|
+
// less than the caller believes — writing a header that claims a detail the
|
|
64
|
+
// events do not have — or capture more, which for the payload-bearing
|
|
65
|
+
// levels is a privacy failure. Neither is worth a trace, so refuse instead.
|
|
66
|
+
if (detailRank === undefined) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`Unknown detail level '${detail}': cannot record at a level this build does not implement`,
|
|
69
|
+
)
|
|
70
|
+
}
|
|
61
71
|
const maxEvents = options.maxEvents ?? 100_000
|
|
62
72
|
const clock = options.clock ?? (() => Math.round(performance.now() * 1000))
|
|
63
73
|
const messageTypeId = options.messageTypeId ?? (() => 0)
|
package/src/types.ts
CHANGED
|
@@ -1,22 +1,110 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Any string other than the ones named beside it.
|
|
3
|
+
*
|
|
4
|
+
* Unions built with this keep editor completion for the values this version
|
|
5
|
+
* knows, while still accepting a value it does not. Nothing in the format
|
|
6
|
+
* rejects an unrecognised string: new perspectives, detail levels and drop
|
|
7
|
+
* policies may be added without a version bump, and a reader that refused one
|
|
8
|
+
* would refuse a file it can otherwise read in full.
|
|
9
|
+
*/
|
|
10
|
+
type OtherString = string & Record<never, never>
|
|
11
|
+
|
|
12
|
+
export type DetailLevel =
|
|
13
|
+
| 'control'
|
|
14
|
+
| 'headers'
|
|
15
|
+
| 'headers+sizes'
|
|
16
|
+
| 'headers+data'
|
|
17
|
+
| 'full'
|
|
18
|
+
| OtherString
|
|
19
|
+
|
|
20
|
+
export type Perspective = 'client' | 'server' | 'observer' | 'relay-tap' | OtherString
|
|
21
|
+
|
|
22
|
+
/** Drop strategy a sampled source applied when it could not keep up. */
|
|
23
|
+
export type DropPolicy = 'head' | 'tail' | 'sampled' | OtherString
|
|
24
|
+
|
|
25
|
+
/** Length of a trace ID, in bytes. Fixed by the format. */
|
|
26
|
+
export const TRACE_ID_LENGTH = 16
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Per-segment metadata.
|
|
30
|
+
*
|
|
31
|
+
* Present only in segmented traces. A single-shot file must not carry it —
|
|
32
|
+
* its absence is what tells a reader that sequence numbers and timestamps are
|
|
33
|
+
* file-global rather than segment-local.
|
|
34
|
+
*/
|
|
35
|
+
export interface SegmentInfo {
|
|
36
|
+
/** 0-based sequence number of this segment within the stream. */
|
|
37
|
+
readonly sequence: number
|
|
38
|
+
/** Nominal segment duration in milliseconds. A hint; the real one may differ. */
|
|
39
|
+
readonly durationMs?: number
|
|
40
|
+
/** Opaque identifier shared by every segment of the same logical stream. */
|
|
41
|
+
readonly streamId?: string
|
|
42
|
+
/** True if this segment continues a previous one with the same `streamId`. */
|
|
43
|
+
readonly continues?: boolean
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Sampling and filtering metadata. Present only when events were dropped or
|
|
48
|
+
* filtered at the source; its absence means the trace is complete relative to
|
|
49
|
+
* its detail level.
|
|
50
|
+
*/
|
|
51
|
+
export interface SamplingInfo {
|
|
52
|
+
/** Fraction of source events retained, in (0, 1]. */
|
|
53
|
+
readonly effectiveRate?: number
|
|
54
|
+
/** Per-segment cap that triggered drops, if rate-limited. */
|
|
55
|
+
readonly maxEventsPerSec?: number
|
|
56
|
+
/** Drop strategy applied when the cap was exceeded. */
|
|
57
|
+
readonly dropPolicy?: DropPolicy
|
|
58
|
+
/** Cumulative events dropped since `startTime`. */
|
|
59
|
+
readonly droppedTotal?: number
|
|
60
|
+
/** Events dropped within this segment only. */
|
|
61
|
+
readonly droppedSegment?: number
|
|
62
|
+
/** Source-side filter rule that selected events. */
|
|
63
|
+
readonly rule?: string
|
|
64
|
+
/** Language the rule is written in ('prefix', 'glob', 'cel'). */
|
|
65
|
+
readonly ruleLang?: string
|
|
66
|
+
/** Event type IDs the drop policy was applied to. */
|
|
67
|
+
readonly appliesTo?: number[]
|
|
68
|
+
}
|
|
3
69
|
|
|
4
70
|
export interface TraceHeader {
|
|
5
71
|
readonly protocol: string
|
|
6
72
|
readonly perspective: Perspective
|
|
7
73
|
readonly detail: DetailLevel
|
|
74
|
+
/**
|
|
75
|
+
* Recording start (Unix epoch milliseconds). In a segmented trace this is
|
|
76
|
+
* the segment's start, not the stream's.
|
|
77
|
+
*/
|
|
8
78
|
readonly startTime: number
|
|
9
79
|
readonly endTime?: number
|
|
10
80
|
readonly transport?: string
|
|
11
81
|
readonly source?: string
|
|
12
82
|
readonly endpoint?: string
|
|
13
83
|
readonly sessionId?: string
|
|
84
|
+
/** Present only when this header begins one segment of a segmented stream. */
|
|
85
|
+
readonly segment?: SegmentInfo
|
|
86
|
+
/** Present only when events were dropped or filtered at the source. */
|
|
87
|
+
readonly sampling?: SamplingInfo
|
|
14
88
|
readonly custom?: Record<string, unknown>
|
|
15
89
|
}
|
|
16
90
|
|
|
17
91
|
interface BaseEvent {
|
|
92
|
+
/**
|
|
93
|
+
* Monotonically increasing sequence number. Segment-local in a segmented
|
|
94
|
+
* trace, so ordering across segments is `(segment.sequence, seq)`.
|
|
95
|
+
*/
|
|
18
96
|
readonly seq: number
|
|
97
|
+
/** Microseconds since the containing segment's `startTime`. */
|
|
19
98
|
readonly timestamp: number
|
|
99
|
+
/**
|
|
100
|
+
* Which peer this event pertains to.
|
|
101
|
+
*
|
|
102
|
+
* Required when the perspective is `'relay-tap'`, where one trace covers
|
|
103
|
+
* many concurrent sessions; omitted otherwise. The identifier is
|
|
104
|
+
* source-local: the same string in two traces from different sources does
|
|
105
|
+
* not name the same peer.
|
|
106
|
+
*/
|
|
107
|
+
readonly peer?: string
|
|
20
108
|
}
|
|
21
109
|
|
|
22
110
|
export interface ControlMessageEvent extends BaseEvent {
|
|
@@ -25,6 +113,15 @@ export interface ControlMessageEvent extends BaseEvent {
|
|
|
25
113
|
readonly messageType: number
|
|
26
114
|
readonly message: Record<string, unknown>
|
|
27
115
|
readonly raw?: Uint8Array
|
|
116
|
+
/**
|
|
117
|
+
* QUIC stream the message travelled on.
|
|
118
|
+
*
|
|
119
|
+
* Optional, and only meaningful for a recorder that knows it. It matters
|
|
120
|
+
* from draft-17, where the control plane is no longer one stream: each
|
|
121
|
+
* request gets its own bidirectional stream and responses carry no request
|
|
122
|
+
* id, so the stream is the only thing tying a response to its request.
|
|
123
|
+
*/
|
|
124
|
+
readonly streamId?: bigint
|
|
28
125
|
}
|
|
29
126
|
|
|
30
127
|
export interface StreamOpenedEvent extends BaseEvent {
|
|
@@ -76,6 +173,110 @@ export interface AnnotationEvent extends BaseEvent {
|
|
|
76
173
|
readonly data: unknown
|
|
77
174
|
}
|
|
78
175
|
|
|
176
|
+
/** Role a peer is acting in at connection time. */
|
|
177
|
+
export type PeerRole = 'publisher' | 'subscriber' | 'both' | OtherString
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Which end of a relay a peer sits on.
|
|
181
|
+
*
|
|
182
|
+
* This is the direction the connection was made in, not the direction
|
|
183
|
+
* subscriptions flow. For subscription causality see
|
|
184
|
+
* {@link SubscriptionDerivationEvent}.
|
|
185
|
+
*/
|
|
186
|
+
export type Side = 'downstream' | 'upstream' | OtherString
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* A new peer session was established. Emitted only by `'relay-tap'` sources;
|
|
190
|
+
* the peer it identifies is on the event's `peer` field.
|
|
191
|
+
*/
|
|
192
|
+
export interface PeerConnectedEvent extends BaseEvent {
|
|
193
|
+
readonly type: 'peer-connected'
|
|
194
|
+
readonly endpoint?: string
|
|
195
|
+
readonly transport?: string
|
|
196
|
+
readonly role?: PeerRole
|
|
197
|
+
readonly side?: Side
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** A peer session ended. Emitted only by `'relay-tap'` sources. */
|
|
201
|
+
export interface PeerDisconnectedEvent extends BaseEvent {
|
|
202
|
+
readonly type: 'peer-disconnected'
|
|
203
|
+
readonly errorCode: number
|
|
204
|
+
readonly reason?: string
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* A `(peer, request id)` pair naming one subscription on one peer.
|
|
209
|
+
*
|
|
210
|
+
* Request ids are unique only within a peer's session, so neither half
|
|
211
|
+
* identifies a subscription on its own.
|
|
212
|
+
*/
|
|
213
|
+
export interface SubscriptionRef {
|
|
214
|
+
readonly peer: string
|
|
215
|
+
readonly requestId: bigint
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** How an upstream subscription came to serve a downstream one. */
|
|
219
|
+
export type DerivationKind = 'created' | 'shared' | OtherString
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* An upstream subscription was created or extended in causal response to
|
|
223
|
+
* downstream ones. Emitted only by `'relay-tap'` sources.
|
|
224
|
+
*
|
|
225
|
+
* This is the primitive multi-hop correlation is built from: a collector
|
|
226
|
+
* reconstructs an end-to-end tree from these links plus the trace ids
|
|
227
|
+
* propagated along them.
|
|
228
|
+
*
|
|
229
|
+
* The four timestamps share the `timestamp` field's timebase — the emitting
|
|
230
|
+
* source's own clock. Differences between them are meaningful with no
|
|
231
|
+
* cross-hop clock agreement, which is the point: they measure how long one
|
|
232
|
+
* hop took. Never subtract one source's timestamp from another's.
|
|
233
|
+
*
|
|
234
|
+
* A source emits the event as soon as the downstream SUBSCRIBE arrives,
|
|
235
|
+
* carrying whichever timestamps it has, and may emit it again for the same
|
|
236
|
+
* pair as the rest arrive. Treat a later event for the same pair as an update
|
|
237
|
+
* to the earlier one, not as a second derivation.
|
|
238
|
+
*/
|
|
239
|
+
export interface SubscriptionDerivationEvent extends BaseEvent {
|
|
240
|
+
readonly type: 'subscription-derivation'
|
|
241
|
+
readonly upstream: SubscriptionRef
|
|
242
|
+
/** Every downstream subscription currently served by `upstream`. */
|
|
243
|
+
readonly downstream: SubscriptionRef[]
|
|
244
|
+
readonly kind: DerivationKind
|
|
245
|
+
/**
|
|
246
|
+
* Trace id propagated along the subscription chain: exactly
|
|
247
|
+
* {@link TRACE_ID_LENGTH} raw bytes, never hex- or base64-encoded, so two
|
|
248
|
+
* implementations produce identical values for the same chain.
|
|
249
|
+
*/
|
|
250
|
+
readonly traceId?: Uint8Array
|
|
251
|
+
/** Track namespace the subscription targets, one entry per field. */
|
|
252
|
+
readonly namespace?: Uint8Array[]
|
|
253
|
+
readonly trackName?: Uint8Array
|
|
254
|
+
/** When the downstream SUBSCRIBE was received. */
|
|
255
|
+
readonly tDownstreamReceived?: number
|
|
256
|
+
/** When the upstream SUBSCRIBE was sent. Absent for a terminal subscription. */
|
|
257
|
+
readonly tUpstreamSent?: number
|
|
258
|
+
/** When SUBSCRIBE_OK arrived from upstream. */
|
|
259
|
+
readonly tUpstreamOkReceived?: number
|
|
260
|
+
/** When SUBSCRIBE_OK was sent downstream. */
|
|
261
|
+
readonly tDownstreamOkSent?: number
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* An event whose type this version of the package does not know.
|
|
266
|
+
*
|
|
267
|
+
* New event types may be added without a format version bump, so a reader
|
|
268
|
+
* that dropped or relabelled them would make one tool's ignorance permanent
|
|
269
|
+
* for everything downstream of it. The fields are kept verbatim, so an
|
|
270
|
+
* unknown event survives a read-modify-write round trip intact.
|
|
271
|
+
*/
|
|
272
|
+
export interface UnknownEvent extends BaseEvent {
|
|
273
|
+
readonly type: 'unknown'
|
|
274
|
+
/** The `"e"` discriminant that was read. */
|
|
275
|
+
readonly eventType: number
|
|
276
|
+
/** Every key from the event map other than `n`, `t`, `p` and `e`. */
|
|
277
|
+
readonly fields: Record<string, unknown>
|
|
278
|
+
}
|
|
279
|
+
|
|
79
280
|
export type TraceEvent =
|
|
80
281
|
| ControlMessageEvent
|
|
81
282
|
| StreamOpenedEvent
|
|
@@ -85,6 +286,10 @@ export type TraceEvent =
|
|
|
85
286
|
| StateChangeEvent
|
|
86
287
|
| TraceErrorEvent
|
|
87
288
|
| AnnotationEvent
|
|
289
|
+
| PeerConnectedEvent
|
|
290
|
+
| PeerDisconnectedEvent
|
|
291
|
+
| SubscriptionDerivationEvent
|
|
292
|
+
| UnknownEvent
|
|
88
293
|
|
|
89
294
|
export interface Trace {
|
|
90
295
|
readonly header: TraceHeader
|