@durable-streams/client-conformance-tests 0.1.8 → 0.2.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.
Files changed (35) hide show
  1. package/dist/adapters/typescript-adapter.cjs +72 -22
  2. package/dist/adapters/typescript-adapter.js +72 -22
  3. package/dist/{benchmark-runner-CrE6JkbX.js → benchmark-runner-81waaCzs.js} +89 -9
  4. package/dist/{benchmark-runner-Db4he452.cjs → benchmark-runner-DliEfq9k.cjs} +93 -8
  5. package/dist/cli.cjs +41 -5
  6. package/dist/cli.js +41 -5
  7. package/dist/index.cjs +2 -2
  8. package/dist/index.d.cts +50 -3
  9. package/dist/index.d.ts +50 -3
  10. package/dist/index.js +2 -2
  11. package/dist/{protocol-qb83AeUH.js → protocol-1p0soayz.js} +2 -1
  12. package/dist/{protocol-D37G3c4e.d.cts → protocol-BxZTqJmO.d.cts} +67 -5
  13. package/dist/{protocol-XeAOKBD-.cjs → protocol-IioVPNaP.cjs} +2 -1
  14. package/dist/{protocol-Mcbiq3nQ.d.ts → protocol-JuFzdV5x.d.ts} +67 -5
  15. package/dist/protocol.cjs +1 -1
  16. package/dist/protocol.d.cts +2 -2
  17. package/dist/protocol.d.ts +2 -2
  18. package/dist/protocol.js +1 -1
  19. package/package.json +8 -3
  20. package/src/adapters/typescript-adapter.ts +110 -32
  21. package/src/benchmark-runner.ts +75 -1
  22. package/src/benchmark-scenarios.ts +4 -4
  23. package/src/cli.ts +46 -5
  24. package/src/protocol.ts +75 -2
  25. package/src/runner.ts +72 -1
  26. package/src/test-cases.ts +55 -0
  27. package/test-cases/consumer/error-context.yaml +67 -0
  28. package/test-cases/consumer/json-parsing-errors.yaml +115 -0
  29. package/test-cases/consumer/read-auto.yaml +155 -0
  30. package/test-cases/consumer/read-sse.yaml +24 -0
  31. package/test-cases/consumer/retry-resilience.yaml +28 -0
  32. package/test-cases/consumer/sse-parsing-errors.yaml +121 -0
  33. package/test-cases/producer/error-context.yaml +72 -0
  34. package/test-cases/producer/idempotent-json-batching.yaml +40 -0
  35. package/test-cases/validation/input-validation.yaml +192 -0
@@ -110,8 +110,8 @@ interface ReadCommand {
110
110
  path: string;
111
111
  /** Starting offset (opaque string from previous reads) */
112
112
  offset?: string;
113
- /** Live mode: false for catch-up only, "long-poll" or "sse" for live */
114
- live?: false | `long-poll` | `sse`;
113
+ /** Live mode: false for catch-up only, true for auto-select, "long-poll" or "sse" for explicit */
114
+ live?: false | true | `long-poll` | `sse`;
115
115
  /** Timeout for long-poll in milliseconds */
116
116
  timeoutMs?: number;
117
117
  /** Maximum number of chunks to read (for testing) */
@@ -176,6 +176,51 @@ interface ClearDynamicCommand {
176
176
  type: `clear-dynamic`;
177
177
  }
178
178
  /**
179
+ * Test client-side input validation.
180
+ *
181
+ * This command tests that the client properly validates input parameters
182
+ * before making any network requests. The adapter should attempt to create
183
+ * the specified object with the given parameters and report whether
184
+ * validation passed or failed.
185
+ */
186
+ interface ValidateCommand {
187
+ type: `validate`;
188
+ /** What to validate */
189
+ target: ValidateTarget;
190
+ }
191
+ /**
192
+ * Validation targets - what client-side validation to test.
193
+ */
194
+ type ValidateTarget = ValidateRetryOptions | ValidateIdempotentProducer;
195
+ /**
196
+ * Validate RetryOptions construction.
197
+ */
198
+ interface ValidateRetryOptions {
199
+ target: `retry-options`;
200
+ /** Max retries (should reject < 0) */
201
+ maxRetries?: number;
202
+ /** Initial delay in ms (should reject <= 0) */
203
+ initialDelayMs?: number;
204
+ /** Max delay in ms (should reject < initialDelayMs) */
205
+ maxDelayMs?: number;
206
+ /** Backoff multiplier (should reject < 1.0) */
207
+ multiplier?: number;
208
+ }
209
+ /**
210
+ * Validate IdempotentProducer construction.
211
+ */
212
+ interface ValidateIdempotentProducer {
213
+ target: `idempotent-producer`;
214
+ /** Producer ID (required, non-empty) */
215
+ producerId?: string;
216
+ /** Starting epoch (should reject < 0) */
217
+ epoch?: number;
218
+ /** Max batch bytes (should reject <= 0) */
219
+ maxBatchBytes?: number;
220
+ /** Max batch items (should reject <= 0) */
221
+ maxBatchItems?: number;
222
+ }
223
+ /**
179
224
  * Execute a timed benchmark operation.
180
225
  * The adapter times the operation internally using high-resolution timing.
181
226
  */
@@ -235,7 +280,7 @@ interface BenchmarkThroughputReadOp {
235
280
  /**
236
281
  * All possible commands from test runner to client.
237
282
  */
238
- type TestCommand = InitCommand | CreateCommand | ConnectCommand | AppendCommand | IdempotentAppendCommand | IdempotentAppendBatchCommand | ReadCommand | HeadCommand | DeleteCommand | ShutdownCommand | SetDynamicHeaderCommand | SetDynamicParamCommand | ClearDynamicCommand | BenchmarkCommand;
283
+ type TestCommand = InitCommand | CreateCommand | ConnectCommand | AppendCommand | IdempotentAppendCommand | IdempotentAppendBatchCommand | ReadCommand | HeadCommand | DeleteCommand | ShutdownCommand | SetDynamicHeaderCommand | SetDynamicParamCommand | ClearDynamicCommand | BenchmarkCommand | ValidateCommand;
239
284
  /**
240
285
  * Successful initialization result.
241
286
  */
@@ -254,10 +299,18 @@ interface InitResult {
254
299
  sse?: boolean;
255
300
  /** Supports long-poll mode */
256
301
  longPoll?: boolean;
302
+ /** Supports auto mode (catch-up then auto-select SSE or long-poll) */
303
+ auto?: boolean;
257
304
  /** Supports streaming reads */
258
305
  streaming?: boolean;
259
306
  /** Supports dynamic headers/params (functions evaluated per-request) */
260
307
  dynamicHeaders?: boolean;
308
+ /** Supports RetryOptions validation (PHP-specific) */
309
+ retryOptions?: boolean;
310
+ /** Supports maxBatchItems option (PHP-specific) */
311
+ batchItems?: boolean;
312
+ /** Rejects zero values as invalid (vs treating 0 as "use default" like Go) */
313
+ strictZeroValidation?: boolean;
261
314
  };
262
315
  }
263
316
  /**
@@ -421,6 +474,13 @@ interface ClearDynamicResult {
421
474
  success: true;
422
475
  }
423
476
  /**
477
+ * Successful validate result (validation passed).
478
+ */
479
+ interface ValidateResult {
480
+ type: `validate`;
481
+ success: true;
482
+ }
483
+ /**
424
484
  * Successful benchmark result with timing.
425
485
  */
426
486
  interface BenchmarkResult {
@@ -461,7 +521,7 @@ interface ErrorResult {
461
521
  /**
462
522
  * All possible results from client to test runner.
463
523
  */
464
- type TestResult = InitResult | CreateResult | ConnectResult | AppendResult | IdempotentAppendResult | IdempotentAppendBatchResult | ReadResult | HeadResult | DeleteResult | ShutdownResult | SetDynamicHeaderResult | SetDynamicParamResult | ClearDynamicResult | BenchmarkResult | ErrorResult;
524
+ type TestResult = InitResult | CreateResult | ConnectResult | AppendResult | IdempotentAppendResult | IdempotentAppendBatchResult | ReadResult | HeadResult | DeleteResult | ShutdownResult | SetDynamicHeaderResult | SetDynamicParamResult | ClearDynamicResult | ValidateResult | BenchmarkResult | ErrorResult;
465
525
  /**
466
526
  * Parse a JSON line into a TestCommand.
467
527
  */
@@ -510,6 +570,8 @@ declare const ErrorCodes: {
510
570
  readonly INTERNAL_ERROR: "INTERNAL_ERROR";
511
571
  /** Operation not supported by this client */
512
572
  readonly NOT_SUPPORTED: "NOT_SUPPORTED";
573
+ /** Invalid argument passed to client API */
574
+ readonly INVALID_ARGUMENT: "INVALID_ARGUMENT";
513
575
  };
514
576
  type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];
515
577
  /**
@@ -545,4 +607,4 @@ declare function calculateStats(durationsNs: Array<bigint>): BenchmarkStats;
545
607
  * Format a BenchmarkStats object for display.
546
608
  */
547
609
  declare function formatStats(stats: BenchmarkStats, unit?: string): Record<string, string>; //#endregion
548
- export { AppendCommand, AppendResult, BenchmarkAppendOp, BenchmarkCommand, BenchmarkCreateOp, BenchmarkOperation, BenchmarkReadOp, BenchmarkResult, BenchmarkRoundtripOp, BenchmarkStats, BenchmarkThroughputAppendOp, BenchmarkThroughputReadOp, ClearDynamicCommand, ClearDynamicResult, ConnectCommand, ConnectResult, CreateCommand, CreateResult, DeleteCommand, DeleteResult, ErrorCode, ErrorCodes, ErrorResult, HeadCommand, HeadResult, IdempotentAppendBatchCommand, IdempotentAppendBatchResult, IdempotentAppendCommand, IdempotentAppendResult, InitCommand, InitResult, ReadChunk, ReadCommand, ReadResult, SetDynamicHeaderCommand, SetDynamicHeaderResult, SetDynamicParamCommand, SetDynamicParamResult, ShutdownCommand, ShutdownResult, TestCommand, TestResult, calculateStats, decodeBase64, encodeBase64, formatStats, parseCommand, parseResult, serializeCommand, serializeResult };
610
+ export { AppendCommand, AppendResult, BenchmarkAppendOp, BenchmarkCommand, BenchmarkCreateOp, BenchmarkOperation, BenchmarkReadOp, BenchmarkResult, BenchmarkRoundtripOp, BenchmarkStats, BenchmarkThroughputAppendOp, BenchmarkThroughputReadOp, ClearDynamicCommand, ClearDynamicResult, ConnectCommand, ConnectResult, CreateCommand, CreateResult, DeleteCommand, DeleteResult, ErrorCode, ErrorCodes, ErrorResult, HeadCommand, HeadResult, IdempotentAppendBatchCommand, IdempotentAppendBatchResult, IdempotentAppendCommand, IdempotentAppendResult, InitCommand, InitResult, ReadChunk, ReadCommand, ReadResult, SetDynamicHeaderCommand, SetDynamicHeaderResult, SetDynamicParamCommand, SetDynamicParamResult, ShutdownCommand, ShutdownResult, TestCommand, TestResult, ValidateCommand, ValidateIdempotentProducer, ValidateResult, ValidateRetryOptions, ValidateTarget, calculateStats, decodeBase64, encodeBase64, formatStats, parseCommand, parseResult, serializeCommand, serializeResult };
@@ -50,7 +50,8 @@ const ErrorCodes = {
50
50
  UNEXPECTED_STATUS: `UNEXPECTED_STATUS`,
51
51
  PARSE_ERROR: `PARSE_ERROR`,
52
52
  INTERNAL_ERROR: `INTERNAL_ERROR`,
53
- NOT_SUPPORTED: `NOT_SUPPORTED`
53
+ NOT_SUPPORTED: `NOT_SUPPORTED`,
54
+ INVALID_ARGUMENT: `INVALID_ARGUMENT`
54
55
  };
55
56
  /**
56
57
  * Calculate statistics from an array of durations in nanoseconds.
@@ -110,8 +110,8 @@ interface ReadCommand {
110
110
  path: string;
111
111
  /** Starting offset (opaque string from previous reads) */
112
112
  offset?: string;
113
- /** Live mode: false for catch-up only, "long-poll" or "sse" for live */
114
- live?: false | `long-poll` | `sse`;
113
+ /** Live mode: false for catch-up only, true for auto-select, "long-poll" or "sse" for explicit */
114
+ live?: false | true | `long-poll` | `sse`;
115
115
  /** Timeout for long-poll in milliseconds */
116
116
  timeoutMs?: number;
117
117
  /** Maximum number of chunks to read (for testing) */
@@ -176,6 +176,51 @@ interface ClearDynamicCommand {
176
176
  type: `clear-dynamic`;
177
177
  }
178
178
  /**
179
+ * Test client-side input validation.
180
+ *
181
+ * This command tests that the client properly validates input parameters
182
+ * before making any network requests. The adapter should attempt to create
183
+ * the specified object with the given parameters and report whether
184
+ * validation passed or failed.
185
+ */
186
+ interface ValidateCommand {
187
+ type: `validate`;
188
+ /** What to validate */
189
+ target: ValidateTarget;
190
+ }
191
+ /**
192
+ * Validation targets - what client-side validation to test.
193
+ */
194
+ type ValidateTarget = ValidateRetryOptions | ValidateIdempotentProducer;
195
+ /**
196
+ * Validate RetryOptions construction.
197
+ */
198
+ interface ValidateRetryOptions {
199
+ target: `retry-options`;
200
+ /** Max retries (should reject < 0) */
201
+ maxRetries?: number;
202
+ /** Initial delay in ms (should reject <= 0) */
203
+ initialDelayMs?: number;
204
+ /** Max delay in ms (should reject < initialDelayMs) */
205
+ maxDelayMs?: number;
206
+ /** Backoff multiplier (should reject < 1.0) */
207
+ multiplier?: number;
208
+ }
209
+ /**
210
+ * Validate IdempotentProducer construction.
211
+ */
212
+ interface ValidateIdempotentProducer {
213
+ target: `idempotent-producer`;
214
+ /** Producer ID (required, non-empty) */
215
+ producerId?: string;
216
+ /** Starting epoch (should reject < 0) */
217
+ epoch?: number;
218
+ /** Max batch bytes (should reject <= 0) */
219
+ maxBatchBytes?: number;
220
+ /** Max batch items (should reject <= 0) */
221
+ maxBatchItems?: number;
222
+ }
223
+ /**
179
224
  * Execute a timed benchmark operation.
180
225
  * The adapter times the operation internally using high-resolution timing.
181
226
  */
@@ -235,7 +280,7 @@ interface BenchmarkThroughputReadOp {
235
280
  /**
236
281
  * All possible commands from test runner to client.
237
282
  */
238
- type TestCommand = InitCommand | CreateCommand | ConnectCommand | AppendCommand | IdempotentAppendCommand | IdempotentAppendBatchCommand | ReadCommand | HeadCommand | DeleteCommand | ShutdownCommand | SetDynamicHeaderCommand | SetDynamicParamCommand | ClearDynamicCommand | BenchmarkCommand;
283
+ type TestCommand = InitCommand | CreateCommand | ConnectCommand | AppendCommand | IdempotentAppendCommand | IdempotentAppendBatchCommand | ReadCommand | HeadCommand | DeleteCommand | ShutdownCommand | SetDynamicHeaderCommand | SetDynamicParamCommand | ClearDynamicCommand | BenchmarkCommand | ValidateCommand;
239
284
  /**
240
285
  * Successful initialization result.
241
286
  */
@@ -254,10 +299,18 @@ interface InitResult {
254
299
  sse?: boolean;
255
300
  /** Supports long-poll mode */
256
301
  longPoll?: boolean;
302
+ /** Supports auto mode (catch-up then auto-select SSE or long-poll) */
303
+ auto?: boolean;
257
304
  /** Supports streaming reads */
258
305
  streaming?: boolean;
259
306
  /** Supports dynamic headers/params (functions evaluated per-request) */
260
307
  dynamicHeaders?: boolean;
308
+ /** Supports RetryOptions validation (PHP-specific) */
309
+ retryOptions?: boolean;
310
+ /** Supports maxBatchItems option (PHP-specific) */
311
+ batchItems?: boolean;
312
+ /** Rejects zero values as invalid (vs treating 0 as "use default" like Go) */
313
+ strictZeroValidation?: boolean;
261
314
  };
262
315
  }
263
316
  /**
@@ -421,6 +474,13 @@ interface ClearDynamicResult {
421
474
  success: true;
422
475
  }
423
476
  /**
477
+ * Successful validate result (validation passed).
478
+ */
479
+ interface ValidateResult {
480
+ type: `validate`;
481
+ success: true;
482
+ }
483
+ /**
424
484
  * Successful benchmark result with timing.
425
485
  */
426
486
  interface BenchmarkResult {
@@ -461,7 +521,7 @@ interface ErrorResult {
461
521
  /**
462
522
  * All possible results from client to test runner.
463
523
  */
464
- type TestResult = InitResult | CreateResult | ConnectResult | AppendResult | IdempotentAppendResult | IdempotentAppendBatchResult | ReadResult | HeadResult | DeleteResult | ShutdownResult | SetDynamicHeaderResult | SetDynamicParamResult | ClearDynamicResult | BenchmarkResult | ErrorResult;
524
+ type TestResult = InitResult | CreateResult | ConnectResult | AppendResult | IdempotentAppendResult | IdempotentAppendBatchResult | ReadResult | HeadResult | DeleteResult | ShutdownResult | SetDynamicHeaderResult | SetDynamicParamResult | ClearDynamicResult | ValidateResult | BenchmarkResult | ErrorResult;
465
525
  /**
466
526
  * Parse a JSON line into a TestCommand.
467
527
  */
@@ -510,6 +570,8 @@ declare const ErrorCodes: {
510
570
  readonly INTERNAL_ERROR: "INTERNAL_ERROR";
511
571
  /** Operation not supported by this client */
512
572
  readonly NOT_SUPPORTED: "NOT_SUPPORTED";
573
+ /** Invalid argument passed to client API */
574
+ readonly INVALID_ARGUMENT: "INVALID_ARGUMENT";
513
575
  };
514
576
  type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];
515
577
  /**
@@ -545,4 +607,4 @@ declare function calculateStats(durationsNs: Array<bigint>): BenchmarkStats;
545
607
  * Format a BenchmarkStats object for display.
546
608
  */
547
609
  declare function formatStats(stats: BenchmarkStats, unit?: string): Record<string, string>; //#endregion
548
- export { AppendCommand, AppendResult, BenchmarkAppendOp, BenchmarkCommand, BenchmarkCreateOp, BenchmarkOperation, BenchmarkReadOp, BenchmarkResult, BenchmarkRoundtripOp, BenchmarkStats, BenchmarkThroughputAppendOp, BenchmarkThroughputReadOp, ClearDynamicCommand, ClearDynamicResult, ConnectCommand, ConnectResult, CreateCommand, CreateResult, DeleteCommand, DeleteResult, ErrorCode, ErrorCodes as ErrorCodes$1, ErrorResult, HeadCommand, HeadResult, IdempotentAppendBatchCommand, IdempotentAppendBatchResult, IdempotentAppendCommand, IdempotentAppendResult, InitCommand, InitResult, ReadChunk, ReadCommand, ReadResult, SetDynamicHeaderCommand, SetDynamicHeaderResult, SetDynamicParamCommand, SetDynamicParamResult, ShutdownCommand, ShutdownResult, TestCommand, TestResult, calculateStats as calculateStats$1, decodeBase64 as decodeBase64$1, encodeBase64 as encodeBase64$1, formatStats as formatStats$1, parseCommand as parseCommand$1, parseResult as parseResult$1, serializeCommand as serializeCommand$1, serializeResult as serializeResult$1 };
610
+ export { AppendCommand, AppendResult, BenchmarkAppendOp, BenchmarkCommand, BenchmarkCreateOp, BenchmarkOperation, BenchmarkReadOp, BenchmarkResult, BenchmarkRoundtripOp, BenchmarkStats, BenchmarkThroughputAppendOp, BenchmarkThroughputReadOp, ClearDynamicCommand, ClearDynamicResult, ConnectCommand, ConnectResult, CreateCommand, CreateResult, DeleteCommand, DeleteResult, ErrorCode, ErrorCodes as ErrorCodes$1, ErrorResult, HeadCommand, HeadResult, IdempotentAppendBatchCommand, IdempotentAppendBatchResult, IdempotentAppendCommand, IdempotentAppendResult, InitCommand, InitResult, ReadChunk, ReadCommand, ReadResult, SetDynamicHeaderCommand, SetDynamicHeaderResult, SetDynamicParamCommand, SetDynamicParamResult, ShutdownCommand, ShutdownResult, TestCommand, TestResult, ValidateCommand, ValidateIdempotentProducer, ValidateResult, ValidateRetryOptions, ValidateTarget, calculateStats as calculateStats$1, decodeBase64 as decodeBase64$1, encodeBase64 as encodeBase64$1, formatStats as formatStats$1, parseCommand as parseCommand$1, parseResult as parseResult$1, serializeCommand as serializeCommand$1, serializeResult as serializeResult$1 };
package/dist/protocol.cjs CHANGED
@@ -1,4 +1,4 @@
1
- const require_protocol = require('./protocol-XeAOKBD-.cjs');
1
+ const require_protocol = require('./protocol-IioVPNaP.cjs');
2
2
 
3
3
  exports.ErrorCodes = require_protocol.ErrorCodes
4
4
  exports.calculateStats = require_protocol.calculateStats
@@ -1,2 +1,2 @@
1
- import { AppendCommand, AppendResult, BenchmarkAppendOp, BenchmarkCommand, BenchmarkCreateOp, BenchmarkOperation, BenchmarkReadOp, BenchmarkResult, BenchmarkRoundtripOp, BenchmarkStats, BenchmarkThroughputAppendOp, BenchmarkThroughputReadOp, ClearDynamicCommand, ClearDynamicResult, ConnectCommand, ConnectResult, CreateCommand, CreateResult, DeleteCommand, DeleteResult, ErrorCode, ErrorCodes, ErrorResult, HeadCommand, HeadResult, IdempotentAppendBatchCommand, IdempotentAppendBatchResult, IdempotentAppendCommand, IdempotentAppendResult, InitCommand, InitResult, ReadChunk, ReadCommand, ReadResult, SetDynamicHeaderCommand, SetDynamicHeaderResult, SetDynamicParamCommand, SetDynamicParamResult, ShutdownCommand, ShutdownResult, TestCommand, TestResult, calculateStats, decodeBase64, encodeBase64, formatStats, parseCommand, parseResult, serializeCommand, serializeResult } from "./protocol-D37G3c4e.cjs";
2
- export { AppendCommand, AppendResult, BenchmarkAppendOp, BenchmarkCommand, BenchmarkCreateOp, BenchmarkOperation, BenchmarkReadOp, BenchmarkResult, BenchmarkRoundtripOp, BenchmarkStats, BenchmarkThroughputAppendOp, BenchmarkThroughputReadOp, ClearDynamicCommand, ClearDynamicResult, ConnectCommand, ConnectResult, CreateCommand, CreateResult, DeleteCommand, DeleteResult, ErrorCode, ErrorCodes, ErrorResult, HeadCommand, HeadResult, IdempotentAppendBatchCommand, IdempotentAppendBatchResult, IdempotentAppendCommand, IdempotentAppendResult, InitCommand, InitResult, ReadChunk, ReadCommand, ReadResult, SetDynamicHeaderCommand, SetDynamicHeaderResult, SetDynamicParamCommand, SetDynamicParamResult, ShutdownCommand, ShutdownResult, TestCommand, TestResult, calculateStats, decodeBase64, encodeBase64, formatStats, parseCommand, parseResult, serializeCommand, serializeResult };
1
+ import { AppendCommand, AppendResult, BenchmarkAppendOp, BenchmarkCommand, BenchmarkCreateOp, BenchmarkOperation, BenchmarkReadOp, BenchmarkResult, BenchmarkRoundtripOp, BenchmarkStats, BenchmarkThroughputAppendOp, BenchmarkThroughputReadOp, ClearDynamicCommand, ClearDynamicResult, ConnectCommand, ConnectResult, CreateCommand, CreateResult, DeleteCommand, DeleteResult, ErrorCode, ErrorCodes, ErrorResult, HeadCommand, HeadResult, IdempotentAppendBatchCommand, IdempotentAppendBatchResult, IdempotentAppendCommand, IdempotentAppendResult, InitCommand, InitResult, ReadChunk, ReadCommand, ReadResult, SetDynamicHeaderCommand, SetDynamicHeaderResult, SetDynamicParamCommand, SetDynamicParamResult, ShutdownCommand, ShutdownResult, TestCommand, TestResult, ValidateCommand, ValidateIdempotentProducer, ValidateResult, ValidateRetryOptions, ValidateTarget, calculateStats, decodeBase64, encodeBase64, formatStats, parseCommand, parseResult, serializeCommand, serializeResult } from "./protocol-BxZTqJmO.cjs";
2
+ export { AppendCommand, AppendResult, BenchmarkAppendOp, BenchmarkCommand, BenchmarkCreateOp, BenchmarkOperation, BenchmarkReadOp, BenchmarkResult, BenchmarkRoundtripOp, BenchmarkStats, BenchmarkThroughputAppendOp, BenchmarkThroughputReadOp, ClearDynamicCommand, ClearDynamicResult, ConnectCommand, ConnectResult, CreateCommand, CreateResult, DeleteCommand, DeleteResult, ErrorCode, ErrorCodes, ErrorResult, HeadCommand, HeadResult, IdempotentAppendBatchCommand, IdempotentAppendBatchResult, IdempotentAppendCommand, IdempotentAppendResult, InitCommand, InitResult, ReadChunk, ReadCommand, ReadResult, SetDynamicHeaderCommand, SetDynamicHeaderResult, SetDynamicParamCommand, SetDynamicParamResult, ShutdownCommand, ShutdownResult, TestCommand, TestResult, ValidateCommand, ValidateIdempotentProducer, ValidateResult, ValidateRetryOptions, ValidateTarget, calculateStats, decodeBase64, encodeBase64, formatStats, parseCommand, parseResult, serializeCommand, serializeResult };
@@ -1,2 +1,2 @@
1
- import { AppendCommand, AppendResult, BenchmarkAppendOp, BenchmarkCommand, BenchmarkCreateOp, BenchmarkOperation, BenchmarkReadOp, BenchmarkResult, BenchmarkRoundtripOp, BenchmarkStats, BenchmarkThroughputAppendOp, BenchmarkThroughputReadOp, ClearDynamicCommand, ClearDynamicResult, ConnectCommand, ConnectResult, CreateCommand, CreateResult, DeleteCommand, DeleteResult, ErrorCode, ErrorCodes$1 as ErrorCodes, ErrorResult, HeadCommand, HeadResult, IdempotentAppendBatchCommand, IdempotentAppendBatchResult, IdempotentAppendCommand, IdempotentAppendResult, InitCommand, InitResult, ReadChunk, ReadCommand, ReadResult, SetDynamicHeaderCommand, SetDynamicHeaderResult, SetDynamicParamCommand, SetDynamicParamResult, ShutdownCommand, ShutdownResult, TestCommand, TestResult, calculateStats$1 as calculateStats, decodeBase64$1 as decodeBase64, encodeBase64$1 as encodeBase64, formatStats$1 as formatStats, parseCommand$1 as parseCommand, parseResult$1 as parseResult, serializeCommand$1 as serializeCommand, serializeResult$1 as serializeResult } from "./protocol-Mcbiq3nQ.js";
2
- export { AppendCommand, AppendResult, BenchmarkAppendOp, BenchmarkCommand, BenchmarkCreateOp, BenchmarkOperation, BenchmarkReadOp, BenchmarkResult, BenchmarkRoundtripOp, BenchmarkStats, BenchmarkThroughputAppendOp, BenchmarkThroughputReadOp, ClearDynamicCommand, ClearDynamicResult, ConnectCommand, ConnectResult, CreateCommand, CreateResult, DeleteCommand, DeleteResult, ErrorCode, ErrorCodes, ErrorResult, HeadCommand, HeadResult, IdempotentAppendBatchCommand, IdempotentAppendBatchResult, IdempotentAppendCommand, IdempotentAppendResult, InitCommand, InitResult, ReadChunk, ReadCommand, ReadResult, SetDynamicHeaderCommand, SetDynamicHeaderResult, SetDynamicParamCommand, SetDynamicParamResult, ShutdownCommand, ShutdownResult, TestCommand, TestResult, calculateStats, decodeBase64, encodeBase64, formatStats, parseCommand, parseResult, serializeCommand, serializeResult };
1
+ import { AppendCommand, AppendResult, BenchmarkAppendOp, BenchmarkCommand, BenchmarkCreateOp, BenchmarkOperation, BenchmarkReadOp, BenchmarkResult, BenchmarkRoundtripOp, BenchmarkStats, BenchmarkThroughputAppendOp, BenchmarkThroughputReadOp, ClearDynamicCommand, ClearDynamicResult, ConnectCommand, ConnectResult, CreateCommand, CreateResult, DeleteCommand, DeleteResult, ErrorCode, ErrorCodes$1 as ErrorCodes, ErrorResult, HeadCommand, HeadResult, IdempotentAppendBatchCommand, IdempotentAppendBatchResult, IdempotentAppendCommand, IdempotentAppendResult, InitCommand, InitResult, ReadChunk, ReadCommand, ReadResult, SetDynamicHeaderCommand, SetDynamicHeaderResult, SetDynamicParamCommand, SetDynamicParamResult, ShutdownCommand, ShutdownResult, TestCommand, TestResult, ValidateCommand, ValidateIdempotentProducer, ValidateResult, ValidateRetryOptions, ValidateTarget, calculateStats$1 as calculateStats, decodeBase64$1 as decodeBase64, encodeBase64$1 as encodeBase64, formatStats$1 as formatStats, parseCommand$1 as parseCommand, parseResult$1 as parseResult, serializeCommand$1 as serializeCommand, serializeResult$1 as serializeResult } from "./protocol-JuFzdV5x.js";
2
+ export { AppendCommand, AppendResult, BenchmarkAppendOp, BenchmarkCommand, BenchmarkCreateOp, BenchmarkOperation, BenchmarkReadOp, BenchmarkResult, BenchmarkRoundtripOp, BenchmarkStats, BenchmarkThroughputAppendOp, BenchmarkThroughputReadOp, ClearDynamicCommand, ClearDynamicResult, ConnectCommand, ConnectResult, CreateCommand, CreateResult, DeleteCommand, DeleteResult, ErrorCode, ErrorCodes, ErrorResult, HeadCommand, HeadResult, IdempotentAppendBatchCommand, IdempotentAppendBatchResult, IdempotentAppendCommand, IdempotentAppendResult, InitCommand, InitResult, ReadChunk, ReadCommand, ReadResult, SetDynamicHeaderCommand, SetDynamicHeaderResult, SetDynamicParamCommand, SetDynamicParamResult, ShutdownCommand, ShutdownResult, TestCommand, TestResult, ValidateCommand, ValidateIdempotentProducer, ValidateResult, ValidateRetryOptions, ValidateTarget, calculateStats, decodeBase64, encodeBase64, formatStats, parseCommand, parseResult, serializeCommand, serializeResult };
package/dist/protocol.js CHANGED
@@ -1,3 +1,3 @@
1
- import { ErrorCodes, calculateStats, decodeBase64, encodeBase64, formatStats, parseCommand, parseResult, serializeCommand, serializeResult } from "./protocol-qb83AeUH.js";
1
+ import { ErrorCodes, calculateStats, decodeBase64, encodeBase64, formatStats, parseCommand, parseResult, serializeCommand, serializeResult } from "./protocol-1p0soayz.js";
2
2
 
3
3
  export { ErrorCodes, calculateStats, decodeBase64, encodeBase64, formatStats, parseCommand, parseResult, serializeCommand, serializeResult };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@durable-streams/client-conformance-tests",
3
3
  "description": "Conformance test suite for Durable Streams client implementations (producer and consumer)",
4
- "version": "0.1.8",
4
+ "version": "0.2.0",
5
5
  "author": "Durable Stream contributors",
6
6
  "bin": {
7
7
  "client-conformance-tests": "./dist/cli.js",
@@ -14,8 +14,8 @@
14
14
  "fast-check": "^4.4.0",
15
15
  "tsx": "^4.19.2",
16
16
  "yaml": "^2.7.1",
17
- "@durable-streams/client": "0.1.5",
18
- "@durable-streams/server": "0.1.6"
17
+ "@durable-streams/client": "0.2.0",
18
+ "@durable-streams/server": "0.2.0"
19
19
  },
20
20
  "devDependencies": {
21
21
  "tsdown": "^0.9.0",
@@ -73,11 +73,16 @@
73
73
  "build": "tsdown",
74
74
  "dev": "tsdown --watch",
75
75
  "test:run": "tsx src/cli.ts --run ts",
76
+ "test:run:elixir": "tsx src/cli.ts --run ../client-elixir/run-conformance-adapter.sh",
77
+ "test:run:elixir:verbose": "tsx src/cli.ts --run ../client-elixir/run-conformance-adapter.sh --verbose",
76
78
  "test:run:python": "tsx src/cli.ts --run ../client-py/run-conformance-adapter.sh",
77
79
  "test:run:python-async": "tsx src/cli.ts --run ../client-py/run-conformance-adapter-async.sh",
78
80
  "test:run:python-async:verbose": "tsx src/cli.ts --run ../client-py/run-conformance-adapter-async.sh --verbose",
79
81
  "test:run:python:verbose": "tsx src/cli.ts --run ../client-py/run-conformance-adapter.sh --verbose",
80
82
  "test:run:verbose": "tsx src/cli.ts --run ts --verbose",
83
+ "bench:ts": "tsx src/cli.ts --bench ts",
84
+ "bench:elixir": "tsx src/cli.ts --bench ../client-elixir/run-conformance-adapter.sh",
85
+ "bench:python": "tsx src/cli.ts --bench ../client-py/run-conformance-adapter.sh",
81
86
  "typecheck": "tsc --noEmit"
82
87
  }
83
88
  }
@@ -132,8 +132,10 @@ async function handleCommand(command: TestCommand): Promise<TestResult> {
132
132
  batching: true,
133
133
  sse: true,
134
134
  longPoll: true,
135
+ auto: true,
135
136
  streaming: true,
136
137
  dynamicHeaders: true,
138
+ strictZeroValidation: true,
137
139
  },
138
140
  }
139
141
  }
@@ -268,11 +270,13 @@ async function handleCommand(command: TestCommand): Promise<TestResult> {
268
270
  }
269
271
 
270
272
  // Determine live mode
271
- let live: `long-poll` | `sse` | false
273
+ let live: true | `long-poll` | `sse` | false
272
274
  if (command.live === `long-poll`) {
273
275
  live = `long-poll`
274
276
  } else if (command.live === `sse`) {
275
277
  live = `sse`
278
+ } else if (command.live === true) {
279
+ live = true
276
280
  } else {
277
281
  live = false
278
282
  }
@@ -326,9 +330,25 @@ async function handleCommand(command: TestCommand): Promise<TestResult> {
326
330
  // Collect chunks using body() for non-live mode or bodyStream() for live
327
331
  const maxChunks = command.maxChunks ?? 100
328
332
 
333
+ // Determine if we should use JSON parsing based on content type
334
+ const contentType = streamContentTypes.get(command.path)
335
+ const isJson = contentType?.includes(`application/json`) ?? false
336
+
329
337
  if (!live) {
330
- // For non-live mode, use body() to get all data
331
- try {
338
+ // For non-live mode, use json() or body() based on content type
339
+ if (isJson) {
340
+ // Use JSON parsing to trigger PARSE_ERROR on malformed JSON
341
+ const items = await response.json()
342
+ if (items.length > 0) {
343
+ // Serialize the items array back to string for the test framework
344
+ // Keep as array format to match what the server returns
345
+ chunks.push({
346
+ data: JSON.stringify(items),
347
+ offset: response.offset,
348
+ })
349
+ }
350
+ } else {
351
+ // Use byte reading for non-JSON content
332
352
  const data = await response.body()
333
353
  if (data.length > 0) {
334
354
  chunks.push({
@@ -336,11 +356,9 @@ async function handleCommand(command: TestCommand): Promise<TestResult> {
336
356
  offset: response.offset,
337
357
  })
338
358
  }
339
- finalOffset = response.offset
340
- upToDate = response.upToDate
341
- } catch {
342
- // If body fails, stream might be empty
343
359
  }
360
+ finalOffset = response.offset
361
+ upToDate = response.upToDate
344
362
  } else {
345
363
  // For live mode, use subscribeBytes which provides per-chunk metadata
346
364
  const decoder = new TextDecoder()
@@ -349,7 +367,7 @@ async function handleCommand(command: TestCommand): Promise<TestResult> {
349
367
  let done = false
350
368
 
351
369
  // Create a promise that resolves when we're done collecting chunks
352
- await new Promise<void>((resolve) => {
370
+ await new Promise<void>((resolve, reject) => {
353
371
  // Set up subscription timeout
354
372
  const subscriptionTimeoutId = setTimeout(() => {
355
373
  done = true
@@ -420,11 +438,12 @@ async function handleCommand(command: TestCommand): Promise<TestResult> {
420
438
  resolve()
421
439
  }
422
440
  })
423
- .catch(() => {
441
+ .catch((err) => {
424
442
  if (!done) {
425
443
  done = true
426
444
  clearTimeout(subscriptionTimeoutId)
427
- resolve()
445
+ // Propagate errors (like SSE parse errors) to the outer handler
446
+ reject(err)
428
447
  }
429
448
  })
430
449
 
@@ -564,18 +583,10 @@ async function handleCommand(command: TestCommand): Promise<TestResult> {
564
583
  lingerMs: 0, // Send immediately for testing
565
584
  })
566
585
 
567
- // For JSON streams, parse the string data into a native object
568
- // (IdempotentProducer expects native objects for JSON streams)
569
- const normalizedContentType = contentType
570
- .split(`;`)[0]
571
- ?.trim()
572
- .toLowerCase()
573
- const isJson = normalizedContentType === `application/json`
574
- const data = isJson ? JSON.parse(command.data) : command.data
575
-
576
586
  try {
577
587
  // append() is fire-and-forget (synchronous), then flush() sends the batch
578
- producer.append(data)
588
+ // Data is already pre-serialized, pass directly to append()
589
+ producer.append(command.data)
579
590
  await producer.flush()
580
591
  await producer.close()
581
592
 
@@ -620,20 +631,10 @@ async function handleCommand(command: TestCommand): Promise<TestResult> {
620
631
  maxBatchBytes: testingConcurrency ? 1 : 1024 * 1024,
621
632
  })
622
633
 
623
- // For JSON streams, parse string items into native objects
624
- // (IdempotentProducer expects native objects for JSON streams)
625
- const normalizedContentType = contentType
626
- .split(`;`)[0]
627
- ?.trim()
628
- .toLowerCase()
629
- const isJson = normalizedContentType === `application/json`
630
- const items = isJson
631
- ? command.items.map((item: string) => JSON.parse(item))
632
- : command.items
633
-
634
634
  try {
635
635
  // append() is fire-and-forget (synchronous), adds to pending batch
636
- for (const item of items) {
636
+ // Data is already pre-serialized, pass directly to append()
637
+ for (const item of command.items) {
637
638
  producer.append(item)
638
639
  }
639
640
 
@@ -655,6 +656,63 @@ async function handleCommand(command: TestCommand): Promise<TestResult> {
655
656
  }
656
657
  }
657
658
 
659
+ case `validate`: {
660
+ // Test client-side input validation
661
+ const { target } = command
662
+
663
+ try {
664
+ switch (target.target) {
665
+ case `retry-options`: {
666
+ // TypeScript client doesn't have a separate RetryOptions class
667
+ // The retry options are validated when passed to stream() or IdempotentProducer
668
+ // For now, just return success since TS uses the fetch defaults
669
+ return {
670
+ type: `validate`,
671
+ success: true,
672
+ }
673
+ }
674
+
675
+ case `idempotent-producer`: {
676
+ // Try to create an IdempotentProducer with the given options
677
+ const ds = new DurableStream({
678
+ url: `${serverUrl}/test-validate`,
679
+ })
680
+
681
+ // IdempotentProducer doesn't currently validate constructor params in TS
682
+
683
+ // Creating the producer tests validation - we don't need the instance
684
+ new IdempotentProducer(ds, target.producerId ?? `test-producer`, {
685
+ epoch: target.epoch,
686
+ maxBatchBytes: target.maxBatchBytes,
687
+ })
688
+
689
+ return {
690
+ type: `validate`,
691
+ success: true,
692
+ }
693
+ }
694
+
695
+ default:
696
+ return {
697
+ type: `error`,
698
+ success: false,
699
+ commandType: `validate`,
700
+ errorCode: ErrorCodes.NOT_SUPPORTED,
701
+ message: `Unknown validation target: ${(target as { target: string }).target}`,
702
+ }
703
+ }
704
+ } catch (err) {
705
+ // Validation failed - return error with details
706
+ return {
707
+ type: `error`,
708
+ success: false,
709
+ commandType: `validate`,
710
+ errorCode: ErrorCodes.INVALID_ARGUMENT,
711
+ message: err instanceof Error ? err.message : String(err),
712
+ }
713
+ }
714
+ }
715
+
658
716
  default:
659
717
  return {
660
718
  type: `error`,
@@ -687,6 +745,8 @@ function errorResult(
687
745
  } else if (err.code === `BAD_REQUEST`) {
688
746
  errorCode = ErrorCodes.INVALID_OFFSET
689
747
  status = 400
748
+ } else if (err.code === `PARSE_ERROR`) {
749
+ errorCode = ErrorCodes.PARSE_ERROR
690
750
  }
691
751
 
692
752
  return {
@@ -744,6 +804,24 @@ function errorResult(
744
804
  }
745
805
  }
746
806
 
807
+ // JSON parsing errors (SyntaxError) or SSE parsing errors
808
+ if (
809
+ err instanceof SyntaxError ||
810
+ err.name === `SyntaxError` ||
811
+ err.message.includes(`JSON`) ||
812
+ err.message.includes(`parse`) ||
813
+ err.message.includes(`SSE`) ||
814
+ err.message.includes(`control event`)
815
+ ) {
816
+ return {
817
+ type: `error`,
818
+ success: false,
819
+ commandType,
820
+ errorCode: ErrorCodes.PARSE_ERROR,
821
+ message: err.message,
822
+ }
823
+ }
824
+
747
825
  return {
748
826
  type: `error`,
749
827
  success: false,
@@ -11,6 +11,8 @@
11
11
  import { spawn } from "node:child_process"
12
12
  import { createInterface } from "node:readline"
13
13
  import { randomUUID } from "node:crypto"
14
+ import { readFile, readdir } from "node:fs/promises"
15
+ import { join } from "node:path"
14
16
  import { DurableStreamTestServer } from "@durable-streams/server"
15
17
  import { DurableStream } from "@durable-streams/client"
16
18
  import {
@@ -316,7 +318,7 @@ async function runScenario(
316
318
  messages.push({ n: i, data: `message-${i}-padding-for-size` })
317
319
  }
318
320
  // Batch append for speed
319
- await Promise.all(messages.map((msg) => ds.append(msg)))
321
+ await Promise.all(messages.map((msg) => ds.append(JSON.stringify(msg))))
320
322
  }
321
323
  }
322
324
 
@@ -858,3 +860,75 @@ export async function runBenchmarks(
858
860
  }
859
861
 
860
862
  export { allScenarios, getScenarioById }
863
+
864
+ // =============================================================================
865
+ // Aggregate Benchmark Results from JSON Files
866
+ // =============================================================================
867
+
868
+ /**
869
+ * Reads benchmark JSON files from a directory and generates a combined markdown report.
870
+ * Each subdirectory should contain a benchmark-results.json file.
871
+ */
872
+ export async function aggregateBenchmarkResults(
873
+ resultsDir: string
874
+ ): Promise<string> {
875
+ const entries = await readdir(resultsDir, { withFileTypes: true })
876
+ const summaries: Array<BenchmarkSummary> = []
877
+
878
+ for (const entry of entries) {
879
+ if (!entry.isDirectory()) continue
880
+
881
+ const jsonPath = join(resultsDir, entry.name, `benchmark-results.json`)
882
+ try {
883
+ const content = await readFile(jsonPath, `utf-8`)
884
+ const summary = JSON.parse(content) as BenchmarkSummary
885
+ // Reconstruct scenario objects from serialized data
886
+ summary.results = summary.results.map((r) => ({
887
+ ...r,
888
+ scenario: r.scenario,
889
+ }))
890
+ summaries.push(summary)
891
+ } catch {
892
+ // Skip directories without valid benchmark results
893
+ console.error(`Skipping ${entry.name}: no valid benchmark-results.json`)
894
+ }
895
+ }
896
+
897
+ if (summaries.length === 0) {
898
+ return `# Client Benchmark Results\n\nNo benchmark results found.\n`
899
+ }
900
+
901
+ // Sort summaries by adapter name for consistent output
902
+ summaries.sort((a, b) => a.adapter.localeCompare(b.adapter))
903
+
904
+ // Generate combined report
905
+ const lines: Array<string> = []
906
+ lines.push(`# Client Benchmark Results`)
907
+ lines.push(``)
908
+
909
+ // Summary table
910
+ const totalPassed = summaries.reduce((sum, s) => sum + s.passed, 0)
911
+ const totalFailed = summaries.reduce((sum, s) => sum + s.failed, 0)
912
+ const totalSkipped = summaries.reduce((sum, s) => sum + s.skipped, 0)
913
+
914
+ lines.push(`| Client | Passed | Failed | Skipped | Status |`)
915
+ lines.push(`|--------|--------|--------|---------|--------|`)
916
+ for (const summary of summaries) {
917
+ const status = summary.failed === 0 ? `✓` : `✗`
918
+ lines.push(
919
+ `| ${summary.adapter} | ${summary.passed} | ${summary.failed} | ${summary.skipped} | ${status} |`
920
+ )
921
+ }
922
+ lines.push(
923
+ `| **Total** | **${totalPassed}** | **${totalFailed}** | **${totalSkipped}** | ${totalFailed === 0 ? `✓` : `✗`} |`
924
+ )
925
+ lines.push(``)
926
+
927
+ // Individual client details
928
+ for (const summary of summaries) {
929
+ lines.push(generateMarkdownReport(summary))
930
+ lines.push(``)
931
+ }
932
+
933
+ return lines.join(`\n`)
934
+ }