@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.
- package/dist/adapters/typescript-adapter.cjs +75 -2
- package/dist/adapters/typescript-adapter.js +76 -3
- package/dist/{benchmark-runner-C_Yghc8f.js → benchmark-runner-CrE6JkbX.js} +106 -12
- package/dist/{benchmark-runner-CLAR9oLd.cjs → benchmark-runner-Db4he452.cjs} +107 -12
- package/dist/cli.cjs +1 -1
- package/dist/cli.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +126 -11
- package/dist/index.d.ts +126 -11
- package/dist/index.js +1 -1
- package/dist/{protocol-3cf94Xyb.d.cts → protocol-D37G3c4e.d.cts} +80 -4
- package/dist/{protocol-DyEvTHPF.d.ts → protocol-Mcbiq3nQ.d.ts} +80 -4
- package/dist/protocol.d.cts +2 -2
- package/dist/protocol.d.ts +2 -2
- package/package.json +3 -3
- package/src/adapters/typescript-adapter.ts +127 -5
- package/src/protocol.ts +85 -1
- package/src/runner.ts +202 -17
- package/src/test-cases.ts +130 -8
- package/test-cases/consumer/error-handling.yaml +42 -0
- package/test-cases/consumer/fault-injection.yaml +202 -0
- package/test-cases/consumer/offset-handling.yaml +209 -0
- package/test-cases/producer/idempotent/autoclaim.yaml +214 -0
- package/test-cases/producer/idempotent/batching.yaml +98 -0
- package/test-cases/producer/idempotent/concurrent-requests.yaml +100 -0
- package/test-cases/producer/idempotent/epoch-management.yaml +333 -0
- package/test-cases/producer/idempotent/error-handling.yaml +194 -0
- package/test-cases/producer/idempotent/multi-producer.yaml +322 -0
- package/test-cases/producer/idempotent/sequence-validation.yaml +339 -0
- 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:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
-
|
|
357
|
-
if (
|
|
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
|
|
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({
|
|
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
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
359
|
-
if (
|
|
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
|
|
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({
|
|
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-
|
|
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-
|
|
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-
|
|
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
|