@durable-streams/client-conformance-tests 0.1.6 → 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 -3
- package/dist/adapters/typescript-adapter.js +76 -4
- package/dist/{benchmark-runner-D-YSAvRy.js → benchmark-runner-CrE6JkbX.js} +86 -8
- package/dist/{benchmark-runner-BlKqhoXE.cjs → benchmark-runner-Db4he452.cjs} +87 -8
- package/dist/cli.cjs +1 -1
- package/dist/cli.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +106 -6
- package/dist/index.d.ts +106 -6
- 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 -6
- package/src/protocol.ts +85 -1
- package/src/runner.ts +178 -13
- package/src/test-cases.ts +110 -3
- package/test-cases/consumer/error-handling.yaml +42 -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,
|
|
@@ -471,7 +538,7 @@ async function handleBenchmark(command) {
|
|
|
471
538
|
const readPromise = (async () => {
|
|
472
539
|
const res = await ds.stream({ live: operation.live ?? `long-poll` });
|
|
473
540
|
return new Promise((resolve) => {
|
|
474
|
-
const unsubscribe = res.subscribeBytes((chunk) => {
|
|
541
|
+
const unsubscribe = res.subscribeBytes(async (chunk) => {
|
|
475
542
|
if (chunk.data.length > 0) {
|
|
476
543
|
unsubscribe();
|
|
477
544
|
res.cancel();
|
|
@@ -508,7 +575,12 @@ async function handleBenchmark(command) {
|
|
|
508
575
|
contentType
|
|
509
576
|
});
|
|
510
577
|
const payload = new Uint8Array(operation.size).fill(42);
|
|
511
|
-
|
|
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();
|
|
512
584
|
metrics.bytesTransferred = operation.count * operation.size;
|
|
513
585
|
metrics.messagesProcessed = operation.count;
|
|
514
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,
|
|
@@ -469,7 +536,7 @@ async function handleBenchmark(command) {
|
|
|
469
536
|
const readPromise = (async () => {
|
|
470
537
|
const res = await ds.stream({ live: operation.live ?? `long-poll` });
|
|
471
538
|
return new Promise((resolve) => {
|
|
472
|
-
const unsubscribe = res.subscribeBytes((chunk) => {
|
|
539
|
+
const unsubscribe = res.subscribeBytes(async (chunk) => {
|
|
473
540
|
if (chunk.data.length > 0) {
|
|
474
541
|
unsubscribe();
|
|
475
542
|
res.cancel();
|
|
@@ -506,7 +573,12 @@ async function handleBenchmark(command) {
|
|
|
506
573
|
contentType
|
|
507
574
|
});
|
|
508
575
|
const payload = new Uint8Array(operation.size).fill(42);
|
|
509
|
-
|
|
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();
|
|
510
582
|
metrics.bytesTransferred = operation.count * operation.size;
|
|
511
583
|
metrics.messagesProcessed = operation.count;
|
|
512
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
|
}
|
|
@@ -444,6 +498,12 @@ function validateExpectation(result, expect) {
|
|
|
444
498
|
const missing = expect.dataContainsAll.filter((s) => !actualData.includes(s));
|
|
445
499
|
if (missing.length > 0) return `Expected data to contain all of [${expect.dataContainsAll.join(`, `)}], missing: [${missing.join(`, `)}]`;
|
|
446
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
|
+
}
|
|
447
507
|
if (expect.upToDate !== void 0 && isReadResult(result)) {
|
|
448
508
|
if (result.upToDate !== expect.upToDate) return `Expected upToDate=${expect.upToDate}, got ${result.upToDate}`;
|
|
449
509
|
}
|
|
@@ -482,6 +542,21 @@ function validateExpectation(result, expect) {
|
|
|
482
542
|
if (actualValue !== expectedValue) return `Expected paramsSent[${key}]="${expectedValue}", got "${actualValue ?? `undefined`}"`;
|
|
483
543
|
}
|
|
484
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
|
+
}
|
|
485
560
|
return null;
|
|
486
561
|
}
|
|
487
562
|
/**
|
|
@@ -592,7 +667,10 @@ async function runConformanceTests(options) {
|
|
|
592
667
|
})).filter((suite) => suite.tests.length > 0);
|
|
593
668
|
const totalTests = countTests(suites);
|
|
594
669
|
console.log(`\nRunning ${totalTests} client conformance tests...\n`);
|
|
595
|
-
const server = new DurableStreamTestServer({
|
|
670
|
+
const server = new DurableStreamTestServer({
|
|
671
|
+
port: options.serverPort ?? 0,
|
|
672
|
+
longPollTimeout: 500
|
|
673
|
+
});
|
|
596
674
|
await server.start();
|
|
597
675
|
const serverUrl = server.url;
|
|
598
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
|
}
|
|
@@ -446,6 +500,12 @@ function validateExpectation(result, expect) {
|
|
|
446
500
|
const missing = expect.dataContainsAll.filter((s) => !actualData.includes(s));
|
|
447
501
|
if (missing.length > 0) return `Expected data to contain all of [${expect.dataContainsAll.join(`, `)}], missing: [${missing.join(`, `)}]`;
|
|
448
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
|
+
}
|
|
449
509
|
if (expect.upToDate !== void 0 && isReadResult(result)) {
|
|
450
510
|
if (result.upToDate !== expect.upToDate) return `Expected upToDate=${expect.upToDate}, got ${result.upToDate}`;
|
|
451
511
|
}
|
|
@@ -484,6 +544,21 @@ function validateExpectation(result, expect) {
|
|
|
484
544
|
if (actualValue !== expectedValue) return `Expected paramsSent[${key}]="${expectedValue}", got "${actualValue ?? `undefined`}"`;
|
|
485
545
|
}
|
|
486
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
|
+
}
|
|
487
562
|
return null;
|
|
488
563
|
}
|
|
489
564
|
/**
|
|
@@ -594,7 +669,10 @@ async function runConformanceTests(options) {
|
|
|
594
669
|
})).filter((suite) => suite.tests.length > 0);
|
|
595
670
|
const totalTests = countTests(suites);
|
|
596
671
|
console.log(`\nRunning ${totalTests} client conformance tests...\n`);
|
|
597
|
-
const server = new __durable_streams_server.DurableStreamTestServer({
|
|
672
|
+
const server = new __durable_streams_server.DurableStreamTestServer({
|
|
673
|
+
port: options.serverPort ?? 0,
|
|
674
|
+
longPollTimeout: 500
|
|
675
|
+
});
|
|
598
676
|
await server.start();
|
|
599
677
|
const serverUrl = server.url;
|
|
600
678
|
console.log(`Reference server started at ${serverUrl}\n`);
|
|
@@ -607,6 +685,7 @@ async function runConformanceTests(options) {
|
|
|
607
685
|
// Multi-status
|
|
608
686
|
// No result yet - will be retrieved via await
|
|
609
687
|
// Clean up
|
|
688
|
+
// 500ms timeout for testing
|
|
610
689
|
require("url").pathToFileURL(__filename).href
|
|
611
690
|
).pathname];
|
|
612
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
|