@durable-streams/client-conformance-tests 0.1.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 +451 -0
- package/dist/adapters/typescript-adapter.d.ts +1 -0
- package/dist/adapters/typescript-adapter.js +586 -0
- package/dist/benchmark-runner-C_Yghc8f.js +1333 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +265 -0
- package/dist/index.d.ts +508 -0
- package/dist/index.js +4 -0
- package/dist/protocol-DyEvTHPF.d.ts +472 -0
- package/dist/protocol-qb83AeUH.js +120 -0
- package/dist/protocol.d.ts +2 -0
- package/dist/protocol.js +3 -0
- package/package.json +53 -0
- package/src/adapters/typescript-adapter.ts +848 -0
- package/src/benchmark-runner.ts +860 -0
- package/src/benchmark-scenarios.ts +311 -0
- package/src/cli.ts +294 -0
- package/src/index.ts +50 -0
- package/src/protocol.ts +656 -0
- package/src/runner.ts +1191 -0
- package/src/test-cases.ts +475 -0
- package/test-cases/consumer/cache-headers.yaml +150 -0
- package/test-cases/consumer/error-handling.yaml +108 -0
- package/test-cases/consumer/message-ordering.yaml +209 -0
- package/test-cases/consumer/offset-handling.yaml +209 -0
- package/test-cases/consumer/offset-resumption.yaml +197 -0
- package/test-cases/consumer/read-catchup.yaml +173 -0
- package/test-cases/consumer/read-longpoll.yaml +132 -0
- package/test-cases/consumer/read-sse.yaml +145 -0
- package/test-cases/consumer/retry-resilience.yaml +160 -0
- package/test-cases/consumer/streaming-equivalence.yaml +226 -0
- package/test-cases/lifecycle/dynamic-headers.yaml +147 -0
- package/test-cases/lifecycle/headers-params.yaml +117 -0
- package/test-cases/lifecycle/stream-lifecycle.yaml +148 -0
- package/test-cases/producer/append-data.yaml +142 -0
- package/test-cases/producer/batching.yaml +112 -0
- package/test-cases/producer/create-stream.yaml +87 -0
- package/test-cases/producer/error-handling.yaml +90 -0
- package/test-cases/producer/sequence-ordering.yaml +148 -0
package/src/protocol.ts
ADDED
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Protocol types for client conformance testing.
|
|
3
|
+
*
|
|
4
|
+
* This module defines the stdin/stdout protocol used for communication
|
|
5
|
+
* between the test runner and client adapters in any language.
|
|
6
|
+
*
|
|
7
|
+
* Communication is line-based JSON over stdin/stdout:
|
|
8
|
+
* - Test runner writes TestCommand as JSON line to client's stdin
|
|
9
|
+
* - Client writes TestResult as JSON line to stdout
|
|
10
|
+
* - Each command expects exactly one result
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// =============================================================================
|
|
14
|
+
// Commands (sent from test runner to client adapter via stdin)
|
|
15
|
+
// =============================================================================
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Initialize the client adapter with configuration.
|
|
19
|
+
* Must be the first command sent.
|
|
20
|
+
*/
|
|
21
|
+
export interface InitCommand {
|
|
22
|
+
type: `init`
|
|
23
|
+
/** Base URL of the reference server */
|
|
24
|
+
serverUrl: string
|
|
25
|
+
/** Optional timeout in milliseconds for operations */
|
|
26
|
+
timeoutMs?: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Create a new stream (PUT request).
|
|
31
|
+
*/
|
|
32
|
+
export interface CreateCommand {
|
|
33
|
+
type: `create`
|
|
34
|
+
/** Full URL path for the stream (relative to serverUrl) */
|
|
35
|
+
path: string
|
|
36
|
+
/** Content type for the stream */
|
|
37
|
+
contentType?: string
|
|
38
|
+
/** Optional TTL in seconds */
|
|
39
|
+
ttlSeconds?: number
|
|
40
|
+
/** Optional absolute expiry timestamp (ISO 8601) */
|
|
41
|
+
expiresAt?: string
|
|
42
|
+
/** Custom headers to include */
|
|
43
|
+
headers?: Record<string, string>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Connect to an existing stream without creating it.
|
|
48
|
+
*/
|
|
49
|
+
export interface ConnectCommand {
|
|
50
|
+
type: `connect`
|
|
51
|
+
path: string
|
|
52
|
+
headers?: Record<string, string>
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Append data to a stream (POST request).
|
|
57
|
+
*/
|
|
58
|
+
export interface AppendCommand {
|
|
59
|
+
type: `append`
|
|
60
|
+
path: string
|
|
61
|
+
/** Data to append - string for text, base64 for binary */
|
|
62
|
+
data: string
|
|
63
|
+
/** Whether data is base64 encoded binary */
|
|
64
|
+
binary?: boolean
|
|
65
|
+
/** Optional sequence number for ordering */
|
|
66
|
+
seq?: number
|
|
67
|
+
/** Custom headers to include */
|
|
68
|
+
headers?: Record<string, string>
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Read from a stream (GET request).
|
|
73
|
+
*/
|
|
74
|
+
export interface ReadCommand {
|
|
75
|
+
type: `read`
|
|
76
|
+
path: string
|
|
77
|
+
/** Starting offset (opaque string from previous reads) */
|
|
78
|
+
offset?: string
|
|
79
|
+
/** Live mode: false for catch-up only, "long-poll" or "sse" for live */
|
|
80
|
+
live?: false | `long-poll` | `sse`
|
|
81
|
+
/** Timeout for long-poll in milliseconds */
|
|
82
|
+
timeoutMs?: number
|
|
83
|
+
/** Maximum number of chunks to read (for testing) */
|
|
84
|
+
maxChunks?: number
|
|
85
|
+
/** Whether to wait until up-to-date before returning */
|
|
86
|
+
waitForUpToDate?: boolean
|
|
87
|
+
/** Custom headers to include */
|
|
88
|
+
headers?: Record<string, string>
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get stream metadata (HEAD request).
|
|
93
|
+
*/
|
|
94
|
+
export interface HeadCommand {
|
|
95
|
+
type: `head`
|
|
96
|
+
path: string
|
|
97
|
+
headers?: Record<string, string>
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Delete a stream (DELETE request).
|
|
102
|
+
*/
|
|
103
|
+
export interface DeleteCommand {
|
|
104
|
+
type: `delete`
|
|
105
|
+
path: string
|
|
106
|
+
headers?: Record<string, string>
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Shutdown the client adapter gracefully.
|
|
111
|
+
*/
|
|
112
|
+
export interface ShutdownCommand {
|
|
113
|
+
type: `shutdown`
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// =============================================================================
|
|
117
|
+
// Dynamic Headers/Params Commands
|
|
118
|
+
// =============================================================================
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Configure a dynamic header that is evaluated per-request.
|
|
122
|
+
* The adapter should store this and apply it to subsequent operations.
|
|
123
|
+
*
|
|
124
|
+
* This tests the client's ability to support header functions for scenarios
|
|
125
|
+
* like OAuth token refresh, request correlation IDs, etc.
|
|
126
|
+
*/
|
|
127
|
+
export interface SetDynamicHeaderCommand {
|
|
128
|
+
type: `set-dynamic-header`
|
|
129
|
+
/** Header name to set */
|
|
130
|
+
name: string
|
|
131
|
+
/** Type of dynamic value */
|
|
132
|
+
valueType: `counter` | `timestamp` | `token`
|
|
133
|
+
/** Initial value (for token type) */
|
|
134
|
+
initialValue?: string
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Configure a dynamic URL parameter that is evaluated per-request.
|
|
139
|
+
*/
|
|
140
|
+
export interface SetDynamicParamCommand {
|
|
141
|
+
type: `set-dynamic-param`
|
|
142
|
+
/** Param name to set */
|
|
143
|
+
name: string
|
|
144
|
+
/** Type of dynamic value */
|
|
145
|
+
valueType: `counter` | `timestamp`
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Clear all dynamic headers and params.
|
|
150
|
+
*/
|
|
151
|
+
export interface ClearDynamicCommand {
|
|
152
|
+
type: `clear-dynamic`
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// =============================================================================
|
|
156
|
+
// Benchmark Commands
|
|
157
|
+
// =============================================================================
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Execute a timed benchmark operation.
|
|
161
|
+
* The adapter times the operation internally using high-resolution timing.
|
|
162
|
+
*/
|
|
163
|
+
export interface BenchmarkCommand {
|
|
164
|
+
type: `benchmark`
|
|
165
|
+
/** Unique ID for this benchmark iteration */
|
|
166
|
+
iterationId: string
|
|
167
|
+
/** The operation to benchmark */
|
|
168
|
+
operation: BenchmarkOperation
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Benchmark operation types - what to measure.
|
|
173
|
+
*/
|
|
174
|
+
export type BenchmarkOperation =
|
|
175
|
+
| BenchmarkAppendOp
|
|
176
|
+
| BenchmarkReadOp
|
|
177
|
+
| BenchmarkRoundtripOp
|
|
178
|
+
| BenchmarkCreateOp
|
|
179
|
+
| BenchmarkThroughputAppendOp
|
|
180
|
+
| BenchmarkThroughputReadOp
|
|
181
|
+
|
|
182
|
+
export interface BenchmarkAppendOp {
|
|
183
|
+
op: `append`
|
|
184
|
+
path: string
|
|
185
|
+
/** Size in bytes - adapter generates random payload */
|
|
186
|
+
size: number
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export interface BenchmarkReadOp {
|
|
190
|
+
op: `read`
|
|
191
|
+
path: string
|
|
192
|
+
offset?: string
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export interface BenchmarkRoundtripOp {
|
|
196
|
+
op: `roundtrip`
|
|
197
|
+
path: string
|
|
198
|
+
/** Size in bytes */
|
|
199
|
+
size: number
|
|
200
|
+
/** Live mode for reading */
|
|
201
|
+
live?: `long-poll` | `sse`
|
|
202
|
+
/** Content type for SSE compatibility */
|
|
203
|
+
contentType?: string
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export interface BenchmarkCreateOp {
|
|
207
|
+
op: `create`
|
|
208
|
+
path: string
|
|
209
|
+
contentType?: string
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export interface BenchmarkThroughputAppendOp {
|
|
213
|
+
op: `throughput_append`
|
|
214
|
+
path: string
|
|
215
|
+
/** Number of messages to send */
|
|
216
|
+
count: number
|
|
217
|
+
/** Size per message in bytes */
|
|
218
|
+
size: number
|
|
219
|
+
/** Concurrency level */
|
|
220
|
+
concurrency: number
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export interface BenchmarkThroughputReadOp {
|
|
224
|
+
op: `throughput_read`
|
|
225
|
+
path: string
|
|
226
|
+
/** Expected number of JSON messages to read and parse */
|
|
227
|
+
expectedCount?: number
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* All possible commands from test runner to client.
|
|
232
|
+
*/
|
|
233
|
+
export type TestCommand =
|
|
234
|
+
| InitCommand
|
|
235
|
+
| CreateCommand
|
|
236
|
+
| ConnectCommand
|
|
237
|
+
| AppendCommand
|
|
238
|
+
| ReadCommand
|
|
239
|
+
| HeadCommand
|
|
240
|
+
| DeleteCommand
|
|
241
|
+
| ShutdownCommand
|
|
242
|
+
| SetDynamicHeaderCommand
|
|
243
|
+
| SetDynamicParamCommand
|
|
244
|
+
| ClearDynamicCommand
|
|
245
|
+
| BenchmarkCommand
|
|
246
|
+
|
|
247
|
+
// =============================================================================
|
|
248
|
+
// Results (sent from client adapter to test runner via stdout)
|
|
249
|
+
// =============================================================================
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Successful initialization result.
|
|
253
|
+
*/
|
|
254
|
+
export interface InitResult {
|
|
255
|
+
type: `init`
|
|
256
|
+
success: true
|
|
257
|
+
/** Client implementation name (e.g., "typescript", "python", "go") */
|
|
258
|
+
clientName: string
|
|
259
|
+
/** Client implementation version */
|
|
260
|
+
clientVersion: string
|
|
261
|
+
/** Supported features */
|
|
262
|
+
features?: {
|
|
263
|
+
/** Supports automatic batching */
|
|
264
|
+
batching?: boolean
|
|
265
|
+
/** Supports SSE mode */
|
|
266
|
+
sse?: boolean
|
|
267
|
+
/** Supports long-poll mode */
|
|
268
|
+
longPoll?: boolean
|
|
269
|
+
/** Supports streaming reads */
|
|
270
|
+
streaming?: boolean
|
|
271
|
+
/** Supports dynamic headers/params (functions evaluated per-request) */
|
|
272
|
+
dynamicHeaders?: boolean
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Successful create result.
|
|
278
|
+
*/
|
|
279
|
+
export interface CreateResult {
|
|
280
|
+
type: `create`
|
|
281
|
+
success: true
|
|
282
|
+
/** HTTP status code received */
|
|
283
|
+
status: number
|
|
284
|
+
/** Stream offset after creation */
|
|
285
|
+
offset?: string
|
|
286
|
+
/** Response headers of interest */
|
|
287
|
+
headers?: Record<string, string>
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Successful connect result.
|
|
292
|
+
*/
|
|
293
|
+
export interface ConnectResult {
|
|
294
|
+
type: `connect`
|
|
295
|
+
success: true
|
|
296
|
+
status: number
|
|
297
|
+
offset?: string
|
|
298
|
+
headers?: Record<string, string>
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Successful append result.
|
|
303
|
+
*/
|
|
304
|
+
export interface AppendResult {
|
|
305
|
+
type: `append`
|
|
306
|
+
success: true
|
|
307
|
+
status: number
|
|
308
|
+
/** New offset after append */
|
|
309
|
+
offset?: string
|
|
310
|
+
/** Response headers */
|
|
311
|
+
headers?: Record<string, string>
|
|
312
|
+
/** Headers that were sent in the request (for dynamic header testing) */
|
|
313
|
+
headersSent?: Record<string, string>
|
|
314
|
+
/** Params that were sent in the request (for dynamic param testing) */
|
|
315
|
+
paramsSent?: Record<string, string>
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* A chunk of data read from the stream.
|
|
320
|
+
*/
|
|
321
|
+
export interface ReadChunk {
|
|
322
|
+
/** Data content - string for text, base64 for binary */
|
|
323
|
+
data: string
|
|
324
|
+
/** Whether data is base64 encoded */
|
|
325
|
+
binary?: boolean
|
|
326
|
+
/** Offset of this chunk */
|
|
327
|
+
offset?: string
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Successful read result.
|
|
332
|
+
*/
|
|
333
|
+
export interface ReadResult {
|
|
334
|
+
type: `read`
|
|
335
|
+
success: true
|
|
336
|
+
status: number
|
|
337
|
+
/** Chunks of data read */
|
|
338
|
+
chunks: Array<ReadChunk>
|
|
339
|
+
/** Final offset after reading */
|
|
340
|
+
offset?: string
|
|
341
|
+
/** Whether stream is up-to-date (caught up to head) */
|
|
342
|
+
upToDate?: boolean
|
|
343
|
+
/** Cursor value if provided */
|
|
344
|
+
cursor?: string
|
|
345
|
+
/** Response headers */
|
|
346
|
+
headers?: Record<string, string>
|
|
347
|
+
/** Headers that were sent in the request (for dynamic header testing) */
|
|
348
|
+
headersSent?: Record<string, string>
|
|
349
|
+
/** Params that were sent in the request (for dynamic param testing) */
|
|
350
|
+
paramsSent?: Record<string, string>
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Successful head result.
|
|
355
|
+
*/
|
|
356
|
+
export interface HeadResult {
|
|
357
|
+
type: `head`
|
|
358
|
+
success: true
|
|
359
|
+
status: number
|
|
360
|
+
/** Current tail offset */
|
|
361
|
+
offset?: string
|
|
362
|
+
/** Stream content type */
|
|
363
|
+
contentType?: string
|
|
364
|
+
/** TTL remaining in seconds */
|
|
365
|
+
ttlSeconds?: number
|
|
366
|
+
/** Absolute expiry (ISO 8601) */
|
|
367
|
+
expiresAt?: string
|
|
368
|
+
headers?: Record<string, string>
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Successful delete result.
|
|
373
|
+
*/
|
|
374
|
+
export interface DeleteResult {
|
|
375
|
+
type: `delete`
|
|
376
|
+
success: true
|
|
377
|
+
status: number
|
|
378
|
+
headers?: Record<string, string>
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Successful shutdown result.
|
|
383
|
+
*/
|
|
384
|
+
export interface ShutdownResult {
|
|
385
|
+
type: `shutdown`
|
|
386
|
+
success: true
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Successful set-dynamic-header result.
|
|
391
|
+
*/
|
|
392
|
+
export interface SetDynamicHeaderResult {
|
|
393
|
+
type: `set-dynamic-header`
|
|
394
|
+
success: true
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Successful set-dynamic-param result.
|
|
399
|
+
*/
|
|
400
|
+
export interface SetDynamicParamResult {
|
|
401
|
+
type: `set-dynamic-param`
|
|
402
|
+
success: true
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Successful clear-dynamic result.
|
|
407
|
+
*/
|
|
408
|
+
export interface ClearDynamicResult {
|
|
409
|
+
type: `clear-dynamic`
|
|
410
|
+
success: true
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Successful benchmark result with timing.
|
|
415
|
+
*/
|
|
416
|
+
export interface BenchmarkResult {
|
|
417
|
+
type: `benchmark`
|
|
418
|
+
success: true
|
|
419
|
+
iterationId: string
|
|
420
|
+
/** Timing in nanoseconds (as string since bigint doesn't JSON serialize) */
|
|
421
|
+
durationNs: string
|
|
422
|
+
/** Optional metrics */
|
|
423
|
+
metrics?: {
|
|
424
|
+
/** Bytes transferred */
|
|
425
|
+
bytesTransferred?: number
|
|
426
|
+
/** Messages processed */
|
|
427
|
+
messagesProcessed?: number
|
|
428
|
+
/** Operations per second (for throughput tests) */
|
|
429
|
+
opsPerSecond?: number
|
|
430
|
+
/** Bytes per second (for throughput tests) */
|
|
431
|
+
bytesPerSecond?: number
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Error result for any failed operation.
|
|
437
|
+
*/
|
|
438
|
+
export interface ErrorResult {
|
|
439
|
+
type: `error`
|
|
440
|
+
success: false
|
|
441
|
+
/** Original command type that failed */
|
|
442
|
+
commandType: TestCommand[`type`]
|
|
443
|
+
/** HTTP status code if available */
|
|
444
|
+
status?: number
|
|
445
|
+
/** Error code (e.g., "NETWORK_ERROR", "TIMEOUT", "CONFLICT") */
|
|
446
|
+
errorCode: string
|
|
447
|
+
/** Human-readable error message */
|
|
448
|
+
message: string
|
|
449
|
+
/** Additional error details */
|
|
450
|
+
details?: Record<string, unknown>
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* All possible results from client to test runner.
|
|
455
|
+
*/
|
|
456
|
+
export type TestResult =
|
|
457
|
+
| InitResult
|
|
458
|
+
| CreateResult
|
|
459
|
+
| ConnectResult
|
|
460
|
+
| AppendResult
|
|
461
|
+
| ReadResult
|
|
462
|
+
| HeadResult
|
|
463
|
+
| DeleteResult
|
|
464
|
+
| ShutdownResult
|
|
465
|
+
| SetDynamicHeaderResult
|
|
466
|
+
| SetDynamicParamResult
|
|
467
|
+
| ClearDynamicResult
|
|
468
|
+
| BenchmarkResult
|
|
469
|
+
| ErrorResult
|
|
470
|
+
|
|
471
|
+
// =============================================================================
|
|
472
|
+
// Utilities
|
|
473
|
+
// =============================================================================
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Parse a JSON line into a TestCommand.
|
|
477
|
+
*/
|
|
478
|
+
export function parseCommand(line: string): TestCommand {
|
|
479
|
+
return JSON.parse(line) as TestCommand
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Serialize a TestResult to a JSON line.
|
|
484
|
+
*/
|
|
485
|
+
export function serializeResult(result: TestResult): string {
|
|
486
|
+
return JSON.stringify(result)
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Parse a JSON line into a TestResult.
|
|
491
|
+
*/
|
|
492
|
+
export function parseResult(line: string): TestResult {
|
|
493
|
+
return JSON.parse(line) as TestResult
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Serialize a TestCommand to a JSON line.
|
|
498
|
+
*/
|
|
499
|
+
export function serializeCommand(command: TestCommand): string {
|
|
500
|
+
return JSON.stringify(command)
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Encode binary data to base64 for transmission.
|
|
505
|
+
*/
|
|
506
|
+
export function encodeBase64(data: Uint8Array): string {
|
|
507
|
+
return Buffer.from(data).toString(`base64`)
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Decode base64 string back to binary data.
|
|
512
|
+
*/
|
|
513
|
+
export function decodeBase64(encoded: string): Uint8Array {
|
|
514
|
+
return new Uint8Array(Buffer.from(encoded, `base64`))
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Standard error codes for ErrorResult.
|
|
519
|
+
*/
|
|
520
|
+
export const ErrorCodes = {
|
|
521
|
+
/** Network connection failed */
|
|
522
|
+
NETWORK_ERROR: `NETWORK_ERROR`,
|
|
523
|
+
/** Operation timed out */
|
|
524
|
+
TIMEOUT: `TIMEOUT`,
|
|
525
|
+
/** Stream already exists (409 Conflict) */
|
|
526
|
+
CONFLICT: `CONFLICT`,
|
|
527
|
+
/** Stream not found (404) */
|
|
528
|
+
NOT_FOUND: `NOT_FOUND`,
|
|
529
|
+
/** Sequence number conflict (409) */
|
|
530
|
+
SEQUENCE_CONFLICT: `SEQUENCE_CONFLICT`,
|
|
531
|
+
/** Invalid offset format */
|
|
532
|
+
INVALID_OFFSET: `INVALID_OFFSET`,
|
|
533
|
+
/** Server returned unexpected status */
|
|
534
|
+
UNEXPECTED_STATUS: `UNEXPECTED_STATUS`,
|
|
535
|
+
/** Failed to parse response */
|
|
536
|
+
PARSE_ERROR: `PARSE_ERROR`,
|
|
537
|
+
/** Client internal error */
|
|
538
|
+
INTERNAL_ERROR: `INTERNAL_ERROR`,
|
|
539
|
+
/** Operation not supported by this client */
|
|
540
|
+
NOT_SUPPORTED: `NOT_SUPPORTED`,
|
|
541
|
+
} as const
|
|
542
|
+
|
|
543
|
+
export type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes]
|
|
544
|
+
|
|
545
|
+
// =============================================================================
|
|
546
|
+
// Benchmark Statistics
|
|
547
|
+
// =============================================================================
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Statistical summary of benchmark results.
|
|
551
|
+
*/
|
|
552
|
+
export interface BenchmarkStats {
|
|
553
|
+
/** Minimum value in milliseconds */
|
|
554
|
+
min: number
|
|
555
|
+
/** Maximum value in milliseconds */
|
|
556
|
+
max: number
|
|
557
|
+
/** Arithmetic mean in milliseconds */
|
|
558
|
+
mean: number
|
|
559
|
+
/** Median (p50) in milliseconds */
|
|
560
|
+
median: number
|
|
561
|
+
/** 75th percentile in milliseconds */
|
|
562
|
+
p75: number
|
|
563
|
+
/** 95th percentile in milliseconds */
|
|
564
|
+
p95: number
|
|
565
|
+
/** 99th percentile in milliseconds */
|
|
566
|
+
p99: number
|
|
567
|
+
/** Standard deviation in milliseconds */
|
|
568
|
+
stdDev: number
|
|
569
|
+
/** Margin of error (95% confidence) in milliseconds */
|
|
570
|
+
marginOfError: number
|
|
571
|
+
/** Number of samples */
|
|
572
|
+
sampleCount: number
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Calculate statistics from an array of durations in nanoseconds.
|
|
577
|
+
*/
|
|
578
|
+
export function calculateStats(durationsNs: Array<bigint>): BenchmarkStats {
|
|
579
|
+
if (durationsNs.length === 0) {
|
|
580
|
+
return {
|
|
581
|
+
min: 0,
|
|
582
|
+
max: 0,
|
|
583
|
+
mean: 0,
|
|
584
|
+
median: 0,
|
|
585
|
+
p75: 0,
|
|
586
|
+
p95: 0,
|
|
587
|
+
p99: 0,
|
|
588
|
+
stdDev: 0,
|
|
589
|
+
marginOfError: 0,
|
|
590
|
+
sampleCount: 0,
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Convert to milliseconds for statistics
|
|
595
|
+
const samplesMs = durationsNs.map((ns) => Number(ns) / 1_000_000)
|
|
596
|
+
const sorted = [...samplesMs].sort((a, b) => a - b)
|
|
597
|
+
const n = sorted.length
|
|
598
|
+
|
|
599
|
+
const min = sorted[0]!
|
|
600
|
+
const max = sorted[n - 1]!
|
|
601
|
+
const mean = samplesMs.reduce((a, b) => a + b, 0) / n
|
|
602
|
+
|
|
603
|
+
// Percentiles (nearest rank method, 0-based indexing)
|
|
604
|
+
const percentile = (p: number) => {
|
|
605
|
+
const idx = Math.floor((n - 1) * p)
|
|
606
|
+
return sorted[idx]!
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const median = percentile(0.5)
|
|
610
|
+
const p75 = percentile(0.75)
|
|
611
|
+
const p95 = percentile(0.95)
|
|
612
|
+
const p99 = percentile(0.99)
|
|
613
|
+
|
|
614
|
+
// Standard deviation
|
|
615
|
+
const squaredDiffs = samplesMs.map((v) => Math.pow(v - mean, 2))
|
|
616
|
+
const variance = squaredDiffs.reduce((a, b) => a + b, 0) / n
|
|
617
|
+
const stdDev = Math.sqrt(variance)
|
|
618
|
+
|
|
619
|
+
// Margin of error (95% confidence, z = 1.96)
|
|
620
|
+
const marginOfError = (1.96 * stdDev) / Math.sqrt(n)
|
|
621
|
+
|
|
622
|
+
return {
|
|
623
|
+
min,
|
|
624
|
+
max,
|
|
625
|
+
mean,
|
|
626
|
+
median,
|
|
627
|
+
p75,
|
|
628
|
+
p95,
|
|
629
|
+
p99,
|
|
630
|
+
stdDev,
|
|
631
|
+
marginOfError,
|
|
632
|
+
sampleCount: n,
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Format a BenchmarkStats object for display.
|
|
638
|
+
*/
|
|
639
|
+
export function formatStats(
|
|
640
|
+
stats: BenchmarkStats,
|
|
641
|
+
unit = `ms`
|
|
642
|
+
): Record<string, string> {
|
|
643
|
+
const fmt = (v: number) => `${v.toFixed(2)} ${unit}`
|
|
644
|
+
return {
|
|
645
|
+
Min: fmt(stats.min),
|
|
646
|
+
Max: fmt(stats.max),
|
|
647
|
+
Mean: fmt(stats.mean),
|
|
648
|
+
Median: fmt(stats.median),
|
|
649
|
+
P75: fmt(stats.p75),
|
|
650
|
+
P95: fmt(stats.p95),
|
|
651
|
+
P99: fmt(stats.p99),
|
|
652
|
+
StdDev: fmt(stats.stdDev),
|
|
653
|
+
"Margin of Error": fmt(stats.marginOfError),
|
|
654
|
+
Samples: stats.sampleCount.toString(),
|
|
655
|
+
}
|
|
656
|
+
}
|