@durable-streams/client-conformance-tests 0.1.5 → 0.1.7

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 (30) hide show
  1. package/dist/adapters/typescript-adapter.cjs +75 -2
  2. package/dist/adapters/typescript-adapter.js +76 -3
  3. package/dist/{benchmark-runner-C_Yghc8f.js → benchmark-runner-CrE6JkbX.js} +106 -12
  4. package/dist/{benchmark-runner-CLAR9oLd.cjs → benchmark-runner-Db4he452.cjs} +107 -12
  5. package/dist/cli.cjs +1 -1
  6. package/dist/cli.js +1 -1
  7. package/dist/index.cjs +1 -1
  8. package/dist/index.d.cts +126 -11
  9. package/dist/index.d.ts +126 -11
  10. package/dist/index.js +1 -1
  11. package/dist/{protocol-3cf94Xyb.d.cts → protocol-D37G3c4e.d.cts} +80 -4
  12. package/dist/{protocol-DyEvTHPF.d.ts → protocol-Mcbiq3nQ.d.ts} +80 -4
  13. package/dist/protocol.d.cts +2 -2
  14. package/dist/protocol.d.ts +2 -2
  15. package/package.json +3 -3
  16. package/src/adapters/typescript-adapter.ts +127 -5
  17. package/src/protocol.ts +85 -1
  18. package/src/runner.ts +202 -17
  19. package/src/test-cases.ts +130 -8
  20. package/test-cases/consumer/error-handling.yaml +42 -0
  21. package/test-cases/consumer/fault-injection.yaml +202 -0
  22. package/test-cases/consumer/offset-handling.yaml +209 -0
  23. package/test-cases/producer/idempotent/autoclaim.yaml +214 -0
  24. package/test-cases/producer/idempotent/batching.yaml +98 -0
  25. package/test-cases/producer/idempotent/concurrent-requests.yaml +100 -0
  26. package/test-cases/producer/idempotent/epoch-management.yaml +333 -0
  27. package/test-cases/producer/idempotent/error-handling.yaml +194 -0
  28. package/test-cases/producer/idempotent/multi-producer.yaml +322 -0
  29. package/test-cases/producer/idempotent/sequence-validation.yaml +339 -0
  30. package/test-cases/producer/idempotent-json-batching.yaml +134 -0
@@ -275,7 +275,7 @@ async function handleCommand(command) {
275
275
  return {
276
276
  type: `read`,
277
277
  success: true,
278
- status: 200,
278
+ status: response.status,
279
279
  chunks,
280
280
  offset: finalOffset,
281
281
  upToDate,
@@ -351,6 +351,73 @@ async function handleCommand(command) {
351
351
  success: true
352
352
  };
353
353
  }
354
+ case `idempotent-append`: try {
355
+ const url = `${serverUrl}${command.path}`;
356
+ const contentType = streamContentTypes.get(command.path) ?? `application/octet-stream`;
357
+ const ds = new __durable_streams_client.DurableStream({
358
+ url,
359
+ contentType
360
+ });
361
+ const producer = new __durable_streams_client.IdempotentProducer(ds, command.producerId, {
362
+ epoch: command.epoch,
363
+ autoClaim: command.autoClaim,
364
+ maxInFlight: 1,
365
+ lingerMs: 0
366
+ });
367
+ const normalizedContentType = contentType.split(`;`)[0]?.trim().toLowerCase();
368
+ const isJson = normalizedContentType === `application/json`;
369
+ const data = isJson ? JSON.parse(command.data) : command.data;
370
+ try {
371
+ producer.append(data);
372
+ await producer.flush();
373
+ await producer.close();
374
+ return {
375
+ type: `idempotent-append`,
376
+ success: true,
377
+ status: 200
378
+ };
379
+ } catch (err) {
380
+ await producer.close();
381
+ throw err;
382
+ }
383
+ } catch (err) {
384
+ return errorResult(`idempotent-append`, err);
385
+ }
386
+ case `idempotent-append-batch`: try {
387
+ const url = `${serverUrl}${command.path}`;
388
+ const contentType = streamContentTypes.get(command.path) ?? `application/octet-stream`;
389
+ const ds = new __durable_streams_client.DurableStream({
390
+ url,
391
+ contentType
392
+ });
393
+ const maxInFlight = command.maxInFlight ?? 1;
394
+ const testingConcurrency = maxInFlight > 1;
395
+ const producer = new __durable_streams_client.IdempotentProducer(ds, command.producerId, {
396
+ epoch: command.epoch,
397
+ autoClaim: command.autoClaim,
398
+ maxInFlight,
399
+ lingerMs: testingConcurrency ? 0 : 1e3,
400
+ maxBatchBytes: testingConcurrency ? 1 : 1024 * 1024
401
+ });
402
+ const normalizedContentType = contentType.split(`;`)[0]?.trim().toLowerCase();
403
+ const isJson = normalizedContentType === `application/json`;
404
+ const items = isJson ? command.items.map((item) => JSON.parse(item)) : command.items;
405
+ try {
406
+ for (const item of items) producer.append(item);
407
+ await producer.flush();
408
+ await producer.close();
409
+ return {
410
+ type: `idempotent-append-batch`,
411
+ success: true,
412
+ status: 200
413
+ };
414
+ } catch (err) {
415
+ await producer.close();
416
+ throw err;
417
+ }
418
+ } catch (err) {
419
+ return errorResult(`idempotent-append-batch`, err);
420
+ }
354
421
  default: return {
355
422
  type: `error`,
356
423
  success: false,
@@ -477,6 +544,7 @@ async function handleBenchmark(command) {
477
544
  res.cancel();
478
545
  resolve(chunk.data);
479
546
  }
547
+ return Promise.resolve();
480
548
  });
481
549
  });
482
550
  })();
@@ -507,7 +575,12 @@ async function handleBenchmark(command) {
507
575
  contentType
508
576
  });
509
577
  const payload = new Uint8Array(operation.size).fill(42);
510
- await Promise.all(Array.from({ length: operation.count }, () => ds.append(payload)));
578
+ const producer = new __durable_streams_client.IdempotentProducer(ds, `bench-producer`, {
579
+ lingerMs: 0,
580
+ onError: (err) => console.error(`Batch failed:`, err)
581
+ });
582
+ for (let i = 0; i < operation.count; i++) producer.append(payload);
583
+ await producer.flush();
511
584
  metrics.bytesTransferred = operation.count * operation.size;
512
585
  metrics.messagesProcessed = operation.count;
513
586
  break;
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { ErrorCodes, decodeBase64, parseCommand, serializeResult } from "../protocol-qb83AeUH.js";
3
3
  import { createInterface } from "node:readline";
4
- import { DurableStream, DurableStreamError, FetchError, stream } from "@durable-streams/client";
4
+ import { DurableStream, DurableStreamError, FetchError, IdempotentProducer, stream } from "@durable-streams/client";
5
5
 
6
6
  //#region src/adapters/typescript-adapter.ts
7
7
  const CLIENT_VERSION = `0.0.1`;
@@ -273,7 +273,7 @@ async function handleCommand(command) {
273
273
  return {
274
274
  type: `read`,
275
275
  success: true,
276
- status: 200,
276
+ status: response.status,
277
277
  chunks,
278
278
  offset: finalOffset,
279
279
  upToDate,
@@ -349,6 +349,73 @@ async function handleCommand(command) {
349
349
  success: true
350
350
  };
351
351
  }
352
+ case `idempotent-append`: try {
353
+ const url = `${serverUrl}${command.path}`;
354
+ const contentType = streamContentTypes.get(command.path) ?? `application/octet-stream`;
355
+ const ds = new DurableStream({
356
+ url,
357
+ contentType
358
+ });
359
+ const producer = new IdempotentProducer(ds, command.producerId, {
360
+ epoch: command.epoch,
361
+ autoClaim: command.autoClaim,
362
+ maxInFlight: 1,
363
+ lingerMs: 0
364
+ });
365
+ const normalizedContentType = contentType.split(`;`)[0]?.trim().toLowerCase();
366
+ const isJson = normalizedContentType === `application/json`;
367
+ const data = isJson ? JSON.parse(command.data) : command.data;
368
+ try {
369
+ producer.append(data);
370
+ await producer.flush();
371
+ await producer.close();
372
+ return {
373
+ type: `idempotent-append`,
374
+ success: true,
375
+ status: 200
376
+ };
377
+ } catch (err) {
378
+ await producer.close();
379
+ throw err;
380
+ }
381
+ } catch (err) {
382
+ return errorResult(`idempotent-append`, err);
383
+ }
384
+ case `idempotent-append-batch`: try {
385
+ const url = `${serverUrl}${command.path}`;
386
+ const contentType = streamContentTypes.get(command.path) ?? `application/octet-stream`;
387
+ const ds = new DurableStream({
388
+ url,
389
+ contentType
390
+ });
391
+ const maxInFlight = command.maxInFlight ?? 1;
392
+ const testingConcurrency = maxInFlight > 1;
393
+ const producer = new IdempotentProducer(ds, command.producerId, {
394
+ epoch: command.epoch,
395
+ autoClaim: command.autoClaim,
396
+ maxInFlight,
397
+ lingerMs: testingConcurrency ? 0 : 1e3,
398
+ maxBatchBytes: testingConcurrency ? 1 : 1024 * 1024
399
+ });
400
+ const normalizedContentType = contentType.split(`;`)[0]?.trim().toLowerCase();
401
+ const isJson = normalizedContentType === `application/json`;
402
+ const items = isJson ? command.items.map((item) => JSON.parse(item)) : command.items;
403
+ try {
404
+ for (const item of items) producer.append(item);
405
+ await producer.flush();
406
+ await producer.close();
407
+ return {
408
+ type: `idempotent-append-batch`,
409
+ success: true,
410
+ status: 200
411
+ };
412
+ } catch (err) {
413
+ await producer.close();
414
+ throw err;
415
+ }
416
+ } catch (err) {
417
+ return errorResult(`idempotent-append-batch`, err);
418
+ }
352
419
  default: return {
353
420
  type: `error`,
354
421
  success: false,
@@ -475,6 +542,7 @@ async function handleBenchmark(command) {
475
542
  res.cancel();
476
543
  resolve(chunk.data);
477
544
  }
545
+ return Promise.resolve();
478
546
  });
479
547
  });
480
548
  })();
@@ -505,7 +573,12 @@ async function handleBenchmark(command) {
505
573
  contentType
506
574
  });
507
575
  const payload = new Uint8Array(operation.size).fill(42);
508
- await Promise.all(Array.from({ length: operation.count }, () => ds.append(payload)));
576
+ const producer = new IdempotentProducer(ds, `bench-producer`, {
577
+ lingerMs: 0,
578
+ onError: (err) => console.error(`Batch failed:`, err)
579
+ });
580
+ for (let i = 0; i < operation.count; i++) producer.append(payload);
581
+ await producer.flush();
509
582
  metrics.bytesTransferred = operation.count * operation.size;
510
583
  metrics.messagesProcessed = operation.count;
511
584
  break;
@@ -214,6 +214,38 @@ async function executeOperation(op, ctx) {
214
214
  status: allSucceeded ? 200 : 207
215
215
  } };
216
216
  }
217
+ case `idempotent-append`: {
218
+ const path$1 = resolveVariables(op.path, variables);
219
+ const data = resolveVariables(op.data, variables);
220
+ const result = await client.send({
221
+ type: `idempotent-append`,
222
+ path: path$1,
223
+ data,
224
+ producerId: op.producerId,
225
+ epoch: op.epoch ?? 0,
226
+ autoClaim: op.autoClaim ?? false,
227
+ headers: op.headers
228
+ }, commandTimeout);
229
+ if (verbose) console.log(` idempotent-append ${path$1}: ${result.success ? `ok` : `failed`}`);
230
+ if (result.success && result.type === `idempotent-append` && op.expect?.storeOffsetAs) variables.set(op.expect.storeOffsetAs, result.offset ?? ``);
231
+ return { result };
232
+ }
233
+ case `idempotent-append-batch`: {
234
+ const path$1 = resolveVariables(op.path, variables);
235
+ const items = op.items.map((item) => resolveVariables(item.data, variables));
236
+ const result = await client.send({
237
+ type: `idempotent-append-batch`,
238
+ path: path$1,
239
+ items,
240
+ producerId: op.producerId,
241
+ epoch: op.epoch ?? 0,
242
+ autoClaim: op.autoClaim ?? false,
243
+ maxInFlight: op.maxInFlight,
244
+ headers: op.headers
245
+ }, commandTimeout);
246
+ if (verbose) console.log(` idempotent-append-batch ${path$1}: ${result.success ? `ok` : `failed`}`);
247
+ return { result };
248
+ }
217
249
  case `read`: {
218
250
  const path$1 = resolveVariables(op.path, variables);
219
251
  const offset = op.offset ? resolveVariables(op.offset, variables) : void 0;
@@ -316,17 +348,39 @@ async function executeOperation(op, ctx) {
316
348
  try {
317
349
  const headResponse = await fetch(`${ctx.serverUrl}${path$1}`, { method: `HEAD` });
318
350
  const contentType = headResponse.headers.get(`content-type`) ?? `application/octet-stream`;
351
+ const headers = {
352
+ "content-type": contentType,
353
+ ...op.headers
354
+ };
355
+ if (op.producerId !== void 0) headers[`Producer-Id`] = op.producerId;
356
+ if (op.producerEpoch !== void 0) headers[`Producer-Epoch`] = op.producerEpoch.toString();
357
+ if (op.producerSeq !== void 0) headers[`Producer-Seq`] = op.producerSeq.toString();
319
358
  const response = await fetch(`${ctx.serverUrl}${path$1}`, {
320
359
  method: `POST`,
321
360
  body: data,
322
- headers: {
323
- "content-type": contentType,
324
- ...op.headers
325
- }
361
+ headers
326
362
  });
327
- if (verbose) console.log(` server-append ${path$1}: ${response.ok ? `ok` : `failed (${response.status})`}`);
328
- if (!response.ok) return { error: `Server append failed with status ${response.status}` };
329
- return {};
363
+ const status = response.status;
364
+ const offset = response.headers.get(`Stream-Next-Offset`) ?? void 0;
365
+ const duplicate = status === 204;
366
+ const producerEpoch = response.headers.get(`Producer-Epoch`);
367
+ const producerSeq = response.headers.get(`Producer-Seq`);
368
+ const producerExpectedSeq = response.headers.get(`Producer-Expected-Seq`);
369
+ const producerReceivedSeq = response.headers.get(`Producer-Received-Seq`);
370
+ if (verbose) console.log(` server-append ${path$1}: status=${status}${duplicate ? ` (duplicate)` : ``}`);
371
+ const result = {
372
+ type: `append`,
373
+ success: true,
374
+ status,
375
+ offset,
376
+ duplicate,
377
+ producerEpoch: producerEpoch ? parseInt(producerEpoch, 10) : void 0,
378
+ producerSeq: producerSeq ? parseInt(producerSeq, 10) : void 0,
379
+ producerExpectedSeq: producerExpectedSeq ? parseInt(producerExpectedSeq, 10) : void 0,
380
+ producerReceivedSeq: producerReceivedSeq ? parseInt(producerReceivedSeq, 10) : void 0
381
+ };
382
+ if (op.expect?.storeOffsetAs && offset) variables.set(op.expect.storeOffsetAs, offset);
383
+ return { result };
330
384
  } catch (err) {
331
385
  return { error: `Server append failed: ${err instanceof Error ? err.message : String(err)}` };
332
386
  }
@@ -350,14 +404,30 @@ async function executeOperation(op, ctx) {
350
404
  path: path$1,
351
405
  status: op.status,
352
406
  count: op.count ?? 1,
353
- retryAfter: op.retryAfter
407
+ retryAfter: op.retryAfter,
408
+ delayMs: op.delayMs,
409
+ dropConnection: op.dropConnection,
410
+ truncateBodyBytes: op.truncateBodyBytes,
411
+ probability: op.probability,
412
+ method: op.method,
413
+ corruptBody: op.corruptBody,
414
+ jitterMs: op.jitterMs
354
415
  })
355
416
  });
356
- if (verbose) console.log(` inject-error ${path$1} ${op.status}x${op.count ?? 1}: ${response.ok ? `ok` : `failed`}`);
357
- if (!response.ok) return { error: `Failed to inject error: ${response.status}` };
417
+ const faultTypes = [];
418
+ if (op.status != null) faultTypes.push(`status=${op.status}`);
419
+ if (op.delayMs != null) faultTypes.push(`delay=${op.delayMs}ms`);
420
+ if (op.jitterMs != null) faultTypes.push(`jitter=${op.jitterMs}ms`);
421
+ if (op.dropConnection) faultTypes.push(`dropConnection`);
422
+ if (op.truncateBodyBytes != null) faultTypes.push(`truncate=${op.truncateBodyBytes}b`);
423
+ if (op.corruptBody) faultTypes.push(`corrupt`);
424
+ if (op.probability != null) faultTypes.push(`p=${op.probability}`);
425
+ const faultDesc = faultTypes.join(`,`) || `unknown`;
426
+ if (verbose) console.log(` inject-error ${path$1} [${faultDesc}]x${op.count ?? 1}: ${response.ok ? `ok` : `failed`}`);
427
+ if (!response.ok) return { error: `Failed to inject fault: ${response.status}` };
358
428
  return {};
359
429
  } catch (err) {
360
- return { error: `Failed to inject error: ${err instanceof Error ? err.message : String(err)}` };
430
+ return { error: `Failed to inject fault: ${err instanceof Error ? err.message : String(err)}` };
361
431
  }
362
432
  }
363
433
  case `clear-errors`: try {
@@ -428,6 +498,12 @@ function validateExpectation(result, expect) {
428
498
  const missing = expect.dataContainsAll.filter((s) => !actualData.includes(s));
429
499
  if (missing.length > 0) return `Expected data to contain all of [${expect.dataContainsAll.join(`, `)}], missing: [${missing.join(`, `)}]`;
430
500
  }
501
+ if (expect.dataExact !== void 0 && isReadResult(result)) {
502
+ const expectedMessages = expect.dataExact;
503
+ const actualMessages = result.chunks.map((c) => c.data);
504
+ if (actualMessages.length !== expectedMessages.length) return `Expected ${expectedMessages.length} messages, got ${actualMessages.length}. Expected: [${expectedMessages.join(`, `)}], got: [${actualMessages.join(`, `)}]`;
505
+ for (let i = 0; i < expectedMessages.length; i++) if (actualMessages[i] !== expectedMessages[i]) return `Message ${i} mismatch: expected "${expectedMessages[i]}", got "${actualMessages[i]}"`;
506
+ }
431
507
  if (expect.upToDate !== void 0 && isReadResult(result)) {
432
508
  if (result.upToDate !== expect.upToDate) return `Expected upToDate=${expect.upToDate}, got ${result.upToDate}`;
433
509
  }
@@ -466,6 +542,21 @@ function validateExpectation(result, expect) {
466
542
  if (actualValue !== expectedValue) return `Expected paramsSent[${key}]="${expectedValue}", got "${actualValue ?? `undefined`}"`;
467
543
  }
468
544
  }
545
+ if (expect.duplicate !== void 0 && isAppendResult(result)) {
546
+ if (result.duplicate !== expect.duplicate) return `Expected duplicate=${expect.duplicate}, got ${result.duplicate}`;
547
+ }
548
+ if (expect.producerEpoch !== void 0 && isAppendResult(result)) {
549
+ if (result.producerEpoch !== expect.producerEpoch) return `Expected producerEpoch=${expect.producerEpoch}, got ${result.producerEpoch}`;
550
+ }
551
+ if (expect.producerSeq !== void 0 && isAppendResult(result)) {
552
+ if (result.producerSeq !== expect.producerSeq) return `Expected producerSeq=${expect.producerSeq}, got ${result.producerSeq}`;
553
+ }
554
+ if (expect.producerExpectedSeq !== void 0 && isAppendResult(result)) {
555
+ if (result.producerExpectedSeq !== expect.producerExpectedSeq) return `Expected producerExpectedSeq=${expect.producerExpectedSeq}, got ${result.producerExpectedSeq}`;
556
+ }
557
+ if (expect.producerReceivedSeq !== void 0 && isAppendResult(result)) {
558
+ if (result.producerReceivedSeq !== expect.producerReceivedSeq) return `Expected producerReceivedSeq=${expect.producerReceivedSeq}, got ${result.producerReceivedSeq}`;
559
+ }
469
560
  return null;
470
561
  }
471
562
  /**
@@ -576,7 +667,10 @@ async function runConformanceTests(options) {
576
667
  })).filter((suite) => suite.tests.length > 0);
577
668
  const totalTests = countTests(suites);
578
669
  console.log(`\nRunning ${totalTests} client conformance tests...\n`);
579
- const server = new DurableStreamTestServer({ port: options.serverPort ?? 0 });
670
+ const server = new DurableStreamTestServer({
671
+ port: options.serverPort ?? 0,
672
+ longPollTimeout: 500
673
+ });
580
674
  await server.start();
581
675
  const serverUrl = server.url;
582
676
  console.log(`Reference server started at ${serverUrl}\n`);
@@ -216,6 +216,38 @@ async function executeOperation(op, ctx) {
216
216
  status: allSucceeded ? 200 : 207
217
217
  } };
218
218
  }
219
+ case `idempotent-append`: {
220
+ const path = resolveVariables(op.path, variables);
221
+ const data = resolveVariables(op.data, variables);
222
+ const result = await client.send({
223
+ type: `idempotent-append`,
224
+ path,
225
+ data,
226
+ producerId: op.producerId,
227
+ epoch: op.epoch ?? 0,
228
+ autoClaim: op.autoClaim ?? false,
229
+ headers: op.headers
230
+ }, commandTimeout);
231
+ if (verbose) console.log(` idempotent-append ${path}: ${result.success ? `ok` : `failed`}`);
232
+ if (result.success && result.type === `idempotent-append` && op.expect?.storeOffsetAs) variables.set(op.expect.storeOffsetAs, result.offset ?? ``);
233
+ return { result };
234
+ }
235
+ case `idempotent-append-batch`: {
236
+ const path = resolveVariables(op.path, variables);
237
+ const items = op.items.map((item) => resolveVariables(item.data, variables));
238
+ const result = await client.send({
239
+ type: `idempotent-append-batch`,
240
+ path,
241
+ items,
242
+ producerId: op.producerId,
243
+ epoch: op.epoch ?? 0,
244
+ autoClaim: op.autoClaim ?? false,
245
+ maxInFlight: op.maxInFlight,
246
+ headers: op.headers
247
+ }, commandTimeout);
248
+ if (verbose) console.log(` idempotent-append-batch ${path}: ${result.success ? `ok` : `failed`}`);
249
+ return { result };
250
+ }
219
251
  case `read`: {
220
252
  const path = resolveVariables(op.path, variables);
221
253
  const offset = op.offset ? resolveVariables(op.offset, variables) : void 0;
@@ -318,17 +350,39 @@ async function executeOperation(op, ctx) {
318
350
  try {
319
351
  const headResponse = await fetch(`${ctx.serverUrl}${path}`, { method: `HEAD` });
320
352
  const contentType = headResponse.headers.get(`content-type`) ?? `application/octet-stream`;
353
+ const headers = {
354
+ "content-type": contentType,
355
+ ...op.headers
356
+ };
357
+ if (op.producerId !== void 0) headers[`Producer-Id`] = op.producerId;
358
+ if (op.producerEpoch !== void 0) headers[`Producer-Epoch`] = op.producerEpoch.toString();
359
+ if (op.producerSeq !== void 0) headers[`Producer-Seq`] = op.producerSeq.toString();
321
360
  const response = await fetch(`${ctx.serverUrl}${path}`, {
322
361
  method: `POST`,
323
362
  body: data,
324
- headers: {
325
- "content-type": contentType,
326
- ...op.headers
327
- }
363
+ headers
328
364
  });
329
- if (verbose) console.log(` server-append ${path}: ${response.ok ? `ok` : `failed (${response.status})`}`);
330
- if (!response.ok) return { error: `Server append failed with status ${response.status}` };
331
- return {};
365
+ const status = response.status;
366
+ const offset = response.headers.get(`Stream-Next-Offset`) ?? void 0;
367
+ const duplicate = status === 204;
368
+ const producerEpoch = response.headers.get(`Producer-Epoch`);
369
+ const producerSeq = response.headers.get(`Producer-Seq`);
370
+ const producerExpectedSeq = response.headers.get(`Producer-Expected-Seq`);
371
+ const producerReceivedSeq = response.headers.get(`Producer-Received-Seq`);
372
+ if (verbose) console.log(` server-append ${path}: status=${status}${duplicate ? ` (duplicate)` : ``}`);
373
+ const result = {
374
+ type: `append`,
375
+ success: true,
376
+ status,
377
+ offset,
378
+ duplicate,
379
+ producerEpoch: producerEpoch ? parseInt(producerEpoch, 10) : void 0,
380
+ producerSeq: producerSeq ? parseInt(producerSeq, 10) : void 0,
381
+ producerExpectedSeq: producerExpectedSeq ? parseInt(producerExpectedSeq, 10) : void 0,
382
+ producerReceivedSeq: producerReceivedSeq ? parseInt(producerReceivedSeq, 10) : void 0
383
+ };
384
+ if (op.expect?.storeOffsetAs && offset) variables.set(op.expect.storeOffsetAs, offset);
385
+ return { result };
332
386
  } catch (err) {
333
387
  return { error: `Server append failed: ${err instanceof Error ? err.message : String(err)}` };
334
388
  }
@@ -352,14 +406,30 @@ async function executeOperation(op, ctx) {
352
406
  path,
353
407
  status: op.status,
354
408
  count: op.count ?? 1,
355
- retryAfter: op.retryAfter
409
+ retryAfter: op.retryAfter,
410
+ delayMs: op.delayMs,
411
+ dropConnection: op.dropConnection,
412
+ truncateBodyBytes: op.truncateBodyBytes,
413
+ probability: op.probability,
414
+ method: op.method,
415
+ corruptBody: op.corruptBody,
416
+ jitterMs: op.jitterMs
356
417
  })
357
418
  });
358
- if (verbose) console.log(` inject-error ${path} ${op.status}x${op.count ?? 1}: ${response.ok ? `ok` : `failed`}`);
359
- if (!response.ok) return { error: `Failed to inject error: ${response.status}` };
419
+ const faultTypes = [];
420
+ if (op.status != null) faultTypes.push(`status=${op.status}`);
421
+ if (op.delayMs != null) faultTypes.push(`delay=${op.delayMs}ms`);
422
+ if (op.jitterMs != null) faultTypes.push(`jitter=${op.jitterMs}ms`);
423
+ if (op.dropConnection) faultTypes.push(`dropConnection`);
424
+ if (op.truncateBodyBytes != null) faultTypes.push(`truncate=${op.truncateBodyBytes}b`);
425
+ if (op.corruptBody) faultTypes.push(`corrupt`);
426
+ if (op.probability != null) faultTypes.push(`p=${op.probability}`);
427
+ const faultDesc = faultTypes.join(`,`) || `unknown`;
428
+ if (verbose) console.log(` inject-error ${path} [${faultDesc}]x${op.count ?? 1}: ${response.ok ? `ok` : `failed`}`);
429
+ if (!response.ok) return { error: `Failed to inject fault: ${response.status}` };
360
430
  return {};
361
431
  } catch (err) {
362
- return { error: `Failed to inject error: ${err instanceof Error ? err.message : String(err)}` };
432
+ return { error: `Failed to inject fault: ${err instanceof Error ? err.message : String(err)}` };
363
433
  }
364
434
  }
365
435
  case `clear-errors`: try {
@@ -430,6 +500,12 @@ function validateExpectation(result, expect) {
430
500
  const missing = expect.dataContainsAll.filter((s) => !actualData.includes(s));
431
501
  if (missing.length > 0) return `Expected data to contain all of [${expect.dataContainsAll.join(`, `)}], missing: [${missing.join(`, `)}]`;
432
502
  }
503
+ if (expect.dataExact !== void 0 && isReadResult(result)) {
504
+ const expectedMessages = expect.dataExact;
505
+ const actualMessages = result.chunks.map((c) => c.data);
506
+ if (actualMessages.length !== expectedMessages.length) return `Expected ${expectedMessages.length} messages, got ${actualMessages.length}. Expected: [${expectedMessages.join(`, `)}], got: [${actualMessages.join(`, `)}]`;
507
+ for (let i = 0; i < expectedMessages.length; i++) if (actualMessages[i] !== expectedMessages[i]) return `Message ${i} mismatch: expected "${expectedMessages[i]}", got "${actualMessages[i]}"`;
508
+ }
433
509
  if (expect.upToDate !== void 0 && isReadResult(result)) {
434
510
  if (result.upToDate !== expect.upToDate) return `Expected upToDate=${expect.upToDate}, got ${result.upToDate}`;
435
511
  }
@@ -468,6 +544,21 @@ function validateExpectation(result, expect) {
468
544
  if (actualValue !== expectedValue) return `Expected paramsSent[${key}]="${expectedValue}", got "${actualValue ?? `undefined`}"`;
469
545
  }
470
546
  }
547
+ if (expect.duplicate !== void 0 && isAppendResult(result)) {
548
+ if (result.duplicate !== expect.duplicate) return `Expected duplicate=${expect.duplicate}, got ${result.duplicate}`;
549
+ }
550
+ if (expect.producerEpoch !== void 0 && isAppendResult(result)) {
551
+ if (result.producerEpoch !== expect.producerEpoch) return `Expected producerEpoch=${expect.producerEpoch}, got ${result.producerEpoch}`;
552
+ }
553
+ if (expect.producerSeq !== void 0 && isAppendResult(result)) {
554
+ if (result.producerSeq !== expect.producerSeq) return `Expected producerSeq=${expect.producerSeq}, got ${result.producerSeq}`;
555
+ }
556
+ if (expect.producerExpectedSeq !== void 0 && isAppendResult(result)) {
557
+ if (result.producerExpectedSeq !== expect.producerExpectedSeq) return `Expected producerExpectedSeq=${expect.producerExpectedSeq}, got ${result.producerExpectedSeq}`;
558
+ }
559
+ if (expect.producerReceivedSeq !== void 0 && isAppendResult(result)) {
560
+ if (result.producerReceivedSeq !== expect.producerReceivedSeq) return `Expected producerReceivedSeq=${expect.producerReceivedSeq}, got ${result.producerReceivedSeq}`;
561
+ }
471
562
  return null;
472
563
  }
473
564
  /**
@@ -578,7 +669,10 @@ async function runConformanceTests(options) {
578
669
  })).filter((suite) => suite.tests.length > 0);
579
670
  const totalTests = countTests(suites);
580
671
  console.log(`\nRunning ${totalTests} client conformance tests...\n`);
581
- const server = new __durable_streams_server.DurableStreamTestServer({ port: options.serverPort ?? 0 });
672
+ const server = new __durable_streams_server.DurableStreamTestServer({
673
+ port: options.serverPort ?? 0,
674
+ longPollTimeout: 500
675
+ });
582
676
  await server.start();
583
677
  const serverUrl = server.url;
584
678
  console.log(`Reference server started at ${serverUrl}\n`);
@@ -591,6 +685,7 @@ async function runConformanceTests(options) {
591
685
  // Multi-status
592
686
  // No result yet - will be retrieved via await
593
687
  // Clean up
688
+ // 500ms timeout for testing
594
689
  require("url").pathToFileURL(__filename).href
595
690
  ).pathname];
596
691
  }
package/dist/cli.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
  require('./protocol-XeAOKBD-.cjs');
4
- const require_benchmark_runner = require('./benchmark-runner-CLAR9oLd.cjs');
4
+ const require_benchmark_runner = require('./benchmark-runner-Db4he452.cjs');
5
5
 
6
6
  //#region src/cli.ts
7
7
  const HELP = `
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import "./protocol-qb83AeUH.js";
3
- import { runBenchmarks, runConformanceTests } from "./benchmark-runner-C_Yghc8f.js";
3
+ import { runBenchmarks, runConformanceTests } from "./benchmark-runner-CrE6JkbX.js";
4
4
 
5
5
  //#region src/cli.ts
6
6
  const HELP = `
package/dist/index.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  const require_protocol = require('./protocol-XeAOKBD-.cjs');
2
- const require_benchmark_runner = require('./benchmark-runner-CLAR9oLd.cjs');
2
+ const require_benchmark_runner = require('./benchmark-runner-Db4he452.cjs');
3
3
 
4
4
  exports.ErrorCodes = require_protocol.ErrorCodes
5
5
  exports.allScenarios = require_benchmark_runner.allScenarios