@durable-streams/client-conformance-tests 0.2.0 → 0.2.2
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 +109 -33
- package/dist/adapters/typescript-adapter.js +110 -34
- package/dist/{benchmark-runner-DliEfq9k.cjs → benchmark-runner-BQiarXdy.cjs} +80 -2
- package/dist/{benchmark-runner-81waaCzs.js → benchmark-runner-IGT51RTF.js} +80 -2
- package/dist/cli.cjs +2 -2
- package/dist/cli.js +2 -2
- package/dist/index.cjs +2 -2
- package/dist/index.d.cts +91 -3
- package/dist/index.d.ts +91 -3
- package/dist/index.js +2 -2
- package/dist/{protocol-BxZTqJmO.d.cts → protocol-9WN0gRRQ.d.ts} +97 -3
- package/dist/{protocol-1p0soayz.js → protocol-BnqUAMKe.js} +1 -0
- package/dist/{protocol-JuFzdV5x.d.ts → protocol-COHkkGmU.d.cts} +97 -3
- package/dist/{protocol-IioVPNaP.cjs → protocol-sDk3deGa.cjs} +1 -0
- package/dist/protocol.cjs +1 -1
- package/dist/protocol.d.cts +2 -2
- package/dist/protocol.d.ts +2 -2
- package/dist/protocol.js +1 -1
- package/package.json +3 -3
- package/src/adapters/typescript-adapter.ts +175 -36
- package/src/protocol.ts +108 -0
- package/src/runner.ts +143 -0
- package/src/test-cases.ts +102 -0
- package/test-cases/consumer/message-ordering.yaml +1 -0
- package/test-cases/consumer/offset-handling.yaml +3 -0
- package/test-cases/consumer/read-sse-base64.yaml +663 -0
- package/test-cases/consumer/read-sse.yaml +58 -1
- package/test-cases/consumer/sse-parsing-errors.yaml +4 -0
- package/test-cases/consumer/streaming-equivalence.yaml +6 -0
- package/test-cases/lifecycle/stream-closure.yaml +759 -0
|
@@ -36,6 +36,10 @@ interface CreateCommand {
|
|
|
36
36
|
expiresAt?: string;
|
|
37
37
|
/** Custom headers to include */
|
|
38
38
|
headers?: Record<string, string>;
|
|
39
|
+
/** Create the stream in closed state */
|
|
40
|
+
closed?: boolean;
|
|
41
|
+
/** Initial body data to include on creation */
|
|
42
|
+
data?: string;
|
|
39
43
|
}
|
|
40
44
|
/**
|
|
41
45
|
* Connect to an existing stream without creating it.
|
|
@@ -103,6 +107,36 @@ interface IdempotentAppendBatchCommand {
|
|
|
103
107
|
headers?: Record<string, string>;
|
|
104
108
|
}
|
|
105
109
|
/**
|
|
110
|
+
* Close a stream via IdempotentProducer (uses producer headers for idempotency).
|
|
111
|
+
*/
|
|
112
|
+
interface IdempotentCloseCommand {
|
|
113
|
+
type: `idempotent-close`;
|
|
114
|
+
path: string;
|
|
115
|
+
/** Producer ID */
|
|
116
|
+
producerId: string;
|
|
117
|
+
/** Producer epoch */
|
|
118
|
+
epoch: number;
|
|
119
|
+
/** Optional final message to append atomically with close */
|
|
120
|
+
data?: string;
|
|
121
|
+
/** Auto-claim epoch on 403 */
|
|
122
|
+
autoClaim: boolean;
|
|
123
|
+
/** Custom headers to include */
|
|
124
|
+
headers?: Record<string, string>;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Detach an IdempotentProducer (stop without closing stream).
|
|
128
|
+
*/
|
|
129
|
+
interface IdempotentDetachCommand {
|
|
130
|
+
type: `idempotent-detach`;
|
|
131
|
+
path: string;
|
|
132
|
+
/** Producer ID */
|
|
133
|
+
producerId: string;
|
|
134
|
+
/** Producer epoch */
|
|
135
|
+
epoch: number;
|
|
136
|
+
/** Custom headers to include */
|
|
137
|
+
headers?: Record<string, string>;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
106
140
|
* Read from a stream (GET request).
|
|
107
141
|
*/
|
|
108
142
|
interface ReadCommand {
|
|
@@ -138,6 +172,33 @@ interface DeleteCommand {
|
|
|
138
172
|
headers?: Record<string, string>;
|
|
139
173
|
}
|
|
140
174
|
/**
|
|
175
|
+
* Close a stream (no more appends allowed).
|
|
176
|
+
*/
|
|
177
|
+
interface CloseCommand {
|
|
178
|
+
type: `close`;
|
|
179
|
+
/** Stream path */
|
|
180
|
+
path: string;
|
|
181
|
+
/** Optional final message to append */
|
|
182
|
+
data?: string;
|
|
183
|
+
/** Content type for the final message */
|
|
184
|
+
contentType?: string;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Close a stream via direct HTTP (bypasses client adapter).
|
|
188
|
+
* Used for testing server-side stream closure behavior.
|
|
189
|
+
*/
|
|
190
|
+
interface ServerCloseCommand {
|
|
191
|
+
type: `server-close`;
|
|
192
|
+
/** Stream path */
|
|
193
|
+
path: string;
|
|
194
|
+
/** Whether stream should be closed (always true for this command) */
|
|
195
|
+
streamClosed: true;
|
|
196
|
+
/** Optional body data */
|
|
197
|
+
data?: string;
|
|
198
|
+
/** Content type for the body */
|
|
199
|
+
contentType?: string;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
141
202
|
* Shutdown the client adapter gracefully.
|
|
142
203
|
*/
|
|
143
204
|
interface ShutdownCommand {
|
|
@@ -280,7 +341,7 @@ interface BenchmarkThroughputReadOp {
|
|
|
280
341
|
/**
|
|
281
342
|
* All possible commands from test runner to client.
|
|
282
343
|
*/
|
|
283
|
-
type TestCommand = InitCommand | CreateCommand | ConnectCommand | AppendCommand | IdempotentAppendCommand | IdempotentAppendBatchCommand | ReadCommand | HeadCommand | DeleteCommand | ShutdownCommand | SetDynamicHeaderCommand | SetDynamicParamCommand | ClearDynamicCommand | BenchmarkCommand | ValidateCommand;
|
|
344
|
+
type TestCommand = InitCommand | CreateCommand | ConnectCommand | AppendCommand | IdempotentAppendCommand | IdempotentAppendBatchCommand | IdempotentCloseCommand | IdempotentDetachCommand | ReadCommand | HeadCommand | DeleteCommand | CloseCommand | ServerCloseCommand | ShutdownCommand | SetDynamicHeaderCommand | SetDynamicParamCommand | ClearDynamicCommand | BenchmarkCommand | ValidateCommand;
|
|
284
345
|
/**
|
|
285
346
|
* Successful initialization result.
|
|
286
347
|
*/
|
|
@@ -387,6 +448,24 @@ interface IdempotentAppendBatchResult {
|
|
|
387
448
|
producerSeq?: number;
|
|
388
449
|
}
|
|
389
450
|
/**
|
|
451
|
+
* Successful idempotent-close result.
|
|
452
|
+
*/
|
|
453
|
+
interface IdempotentCloseResult {
|
|
454
|
+
type: `idempotent-close`;
|
|
455
|
+
success: true;
|
|
456
|
+
status: number;
|
|
457
|
+
/** Final stream offset after close */
|
|
458
|
+
finalOffset?: string;
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Successful idempotent-detach result.
|
|
462
|
+
*/
|
|
463
|
+
interface IdempotentDetachResult {
|
|
464
|
+
type: `idempotent-detach`;
|
|
465
|
+
success: true;
|
|
466
|
+
status: number;
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
390
469
|
* A chunk of data read from the stream.
|
|
391
470
|
*/
|
|
392
471
|
interface ReadChunk {
|
|
@@ -410,6 +489,8 @@ interface ReadResult {
|
|
|
410
489
|
offset?: string;
|
|
411
490
|
/** Whether stream is up-to-date (caught up to head) */
|
|
412
491
|
upToDate?: boolean;
|
|
492
|
+
/** Whether the stream has been permanently closed (no more appends) */
|
|
493
|
+
streamClosed?: boolean;
|
|
413
494
|
/** Cursor value if provided */
|
|
414
495
|
cursor?: string;
|
|
415
496
|
/** Response headers */
|
|
@@ -434,6 +515,8 @@ interface HeadResult {
|
|
|
434
515
|
ttlSeconds?: number;
|
|
435
516
|
/** Absolute expiry (ISO 8601) */
|
|
436
517
|
expiresAt?: string;
|
|
518
|
+
/** Whether the stream has been permanently closed (no more appends) */
|
|
519
|
+
streamClosed?: boolean;
|
|
437
520
|
headers?: Record<string, string>;
|
|
438
521
|
}
|
|
439
522
|
/**
|
|
@@ -446,6 +529,15 @@ interface DeleteResult {
|
|
|
446
529
|
headers?: Record<string, string>;
|
|
447
530
|
}
|
|
448
531
|
/**
|
|
532
|
+
* Successful close result.
|
|
533
|
+
*/
|
|
534
|
+
interface CloseResult {
|
|
535
|
+
type: `close`;
|
|
536
|
+
success: true;
|
|
537
|
+
/** Final offset after closing (may include final message) */
|
|
538
|
+
finalOffset: string;
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
449
541
|
* Successful shutdown result.
|
|
450
542
|
*/
|
|
451
543
|
interface ShutdownResult {
|
|
@@ -521,7 +613,7 @@ interface ErrorResult {
|
|
|
521
613
|
/**
|
|
522
614
|
* All possible results from client to test runner.
|
|
523
615
|
*/
|
|
524
|
-
type TestResult = InitResult | CreateResult | ConnectResult | AppendResult | IdempotentAppendResult | IdempotentAppendBatchResult | ReadResult | HeadResult | DeleteResult | ShutdownResult | SetDynamicHeaderResult | SetDynamicParamResult | ClearDynamicResult | ValidateResult | BenchmarkResult | ErrorResult;
|
|
616
|
+
type TestResult = InitResult | CreateResult | ConnectResult | AppendResult | IdempotentAppendResult | IdempotentAppendBatchResult | IdempotentCloseResult | IdempotentDetachResult | ReadResult | HeadResult | DeleteResult | CloseResult | ShutdownResult | SetDynamicHeaderResult | SetDynamicParamResult | ClearDynamicResult | ValidateResult | BenchmarkResult | ErrorResult;
|
|
525
617
|
/**
|
|
526
618
|
* Parse a JSON line into a TestCommand.
|
|
527
619
|
*/
|
|
@@ -560,6 +652,8 @@ declare const ErrorCodes: {
|
|
|
560
652
|
readonly NOT_FOUND: "NOT_FOUND";
|
|
561
653
|
/** Sequence number conflict (409) */
|
|
562
654
|
readonly SEQUENCE_CONFLICT: "SEQUENCE_CONFLICT";
|
|
655
|
+
/** Stream is closed (409 with Stream-Closed header) */
|
|
656
|
+
readonly STREAM_CLOSED: "STREAM_CLOSED";
|
|
563
657
|
/** Invalid offset format */
|
|
564
658
|
readonly INVALID_OFFSET: "INVALID_OFFSET";
|
|
565
659
|
/** Server returned unexpected status */
|
|
@@ -607,4 +701,4 @@ declare function calculateStats(durationsNs: Array<bigint>): BenchmarkStats;
|
|
|
607
701
|
* Format a BenchmarkStats object for display.
|
|
608
702
|
*/
|
|
609
703
|
declare function formatStats(stats: BenchmarkStats, unit?: string): Record<string, string>; //#endregion
|
|
610
|
-
export { AppendCommand, AppendResult, BenchmarkAppendOp, BenchmarkCommand, BenchmarkCreateOp, BenchmarkOperation, BenchmarkReadOp, BenchmarkResult, BenchmarkRoundtripOp, BenchmarkStats, BenchmarkThroughputAppendOp, BenchmarkThroughputReadOp, ClearDynamicCommand, ClearDynamicResult, ConnectCommand, ConnectResult, CreateCommand, CreateResult, DeleteCommand, DeleteResult, ErrorCode, ErrorCodes
|
|
704
|
+
export { AppendCommand, AppendResult, BenchmarkAppendOp, BenchmarkCommand, BenchmarkCreateOp, BenchmarkOperation, BenchmarkReadOp, BenchmarkResult, BenchmarkRoundtripOp, BenchmarkStats, BenchmarkThroughputAppendOp, BenchmarkThroughputReadOp, ClearDynamicCommand, ClearDynamicResult, CloseCommand, CloseResult, ConnectCommand, ConnectResult, CreateCommand, CreateResult, DeleteCommand, DeleteResult, ErrorCode, ErrorCodes, ErrorResult, HeadCommand, HeadResult, IdempotentAppendBatchCommand, IdempotentAppendBatchResult, IdempotentAppendCommand, IdempotentAppendResult, IdempotentCloseCommand, IdempotentCloseResult, IdempotentDetachCommand, IdempotentDetachResult, InitCommand, InitResult, ReadChunk, ReadCommand, ReadResult, ServerCloseCommand, SetDynamicHeaderCommand, SetDynamicHeaderResult, SetDynamicParamCommand, SetDynamicParamResult, ShutdownCommand, ShutdownResult, TestCommand, TestResult, ValidateCommand, ValidateIdempotentProducer, ValidateResult, ValidateRetryOptions, ValidateTarget, calculateStats, decodeBase64, encodeBase64, formatStats, parseCommand, parseResult, serializeCommand, serializeResult };
|
package/dist/protocol.cjs
CHANGED
package/dist/protocol.d.cts
CHANGED
|
@@ -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, ValidateCommand, ValidateIdempotentProducer, ValidateResult, ValidateRetryOptions, ValidateTarget, calculateStats, decodeBase64, encodeBase64, formatStats, parseCommand, parseResult, serializeCommand, serializeResult } from "./protocol-
|
|
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
|
+
import { AppendCommand, AppendResult, BenchmarkAppendOp, BenchmarkCommand, BenchmarkCreateOp, BenchmarkOperation, BenchmarkReadOp, BenchmarkResult, BenchmarkRoundtripOp, BenchmarkStats, BenchmarkThroughputAppendOp, BenchmarkThroughputReadOp, ClearDynamicCommand, ClearDynamicResult, CloseCommand, CloseResult, ConnectCommand, ConnectResult, CreateCommand, CreateResult, DeleteCommand, DeleteResult, ErrorCode, ErrorCodes, ErrorResult, HeadCommand, HeadResult, IdempotentAppendBatchCommand, IdempotentAppendBatchResult, IdempotentAppendCommand, IdempotentAppendResult, IdempotentCloseCommand, IdempotentCloseResult, IdempotentDetachCommand, IdempotentDetachResult, InitCommand, InitResult, ReadChunk, ReadCommand, ReadResult, ServerCloseCommand, SetDynamicHeaderCommand, SetDynamicHeaderResult, SetDynamicParamCommand, SetDynamicParamResult, ShutdownCommand, ShutdownResult, TestCommand, TestResult, ValidateCommand, ValidateIdempotentProducer, ValidateResult, ValidateRetryOptions, ValidateTarget, calculateStats, decodeBase64, encodeBase64, formatStats, parseCommand, parseResult, serializeCommand, serializeResult } from "./protocol-COHkkGmU.cjs";
|
|
2
|
+
export { AppendCommand, AppendResult, BenchmarkAppendOp, BenchmarkCommand, BenchmarkCreateOp, BenchmarkOperation, BenchmarkReadOp, BenchmarkResult, BenchmarkRoundtripOp, BenchmarkStats, BenchmarkThroughputAppendOp, BenchmarkThroughputReadOp, ClearDynamicCommand, ClearDynamicResult, CloseCommand, CloseResult, ConnectCommand, ConnectResult, CreateCommand, CreateResult, DeleteCommand, DeleteResult, ErrorCode, ErrorCodes, ErrorResult, HeadCommand, HeadResult, IdempotentAppendBatchCommand, IdempotentAppendBatchResult, IdempotentAppendCommand, IdempotentAppendResult, IdempotentCloseCommand, IdempotentCloseResult, IdempotentDetachCommand, IdempotentDetachResult, InitCommand, InitResult, ReadChunk, ReadCommand, ReadResult, ServerCloseCommand, SetDynamicHeaderCommand, SetDynamicHeaderResult, SetDynamicParamCommand, SetDynamicParamResult, ShutdownCommand, ShutdownResult, TestCommand, TestResult, ValidateCommand, ValidateIdempotentProducer, ValidateResult, ValidateRetryOptions, ValidateTarget, calculateStats, decodeBase64, encodeBase64, formatStats, parseCommand, parseResult, serializeCommand, serializeResult };
|
package/dist/protocol.d.ts
CHANGED
|
@@ -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, 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-
|
|
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
|
+
import { AppendCommand, AppendResult, BenchmarkAppendOp, BenchmarkCommand, BenchmarkCreateOp, BenchmarkOperation, BenchmarkReadOp, BenchmarkResult, BenchmarkRoundtripOp, BenchmarkStats, BenchmarkThroughputAppendOp, BenchmarkThroughputReadOp, ClearDynamicCommand, ClearDynamicResult, CloseCommand, CloseResult, ConnectCommand, ConnectResult, CreateCommand, CreateResult, DeleteCommand, DeleteResult, ErrorCode, ErrorCodes$1 as ErrorCodes, ErrorResult, HeadCommand, HeadResult, IdempotentAppendBatchCommand, IdempotentAppendBatchResult, IdempotentAppendCommand, IdempotentAppendResult, IdempotentCloseCommand, IdempotentCloseResult, IdempotentDetachCommand, IdempotentDetachResult, InitCommand, InitResult, ReadChunk, ReadCommand, ReadResult, ServerCloseCommand, 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-9WN0gRRQ.js";
|
|
2
|
+
export { AppendCommand, AppendResult, BenchmarkAppendOp, BenchmarkCommand, BenchmarkCreateOp, BenchmarkOperation, BenchmarkReadOp, BenchmarkResult, BenchmarkRoundtripOp, BenchmarkStats, BenchmarkThroughputAppendOp, BenchmarkThroughputReadOp, ClearDynamicCommand, ClearDynamicResult, CloseCommand, CloseResult, ConnectCommand, ConnectResult, CreateCommand, CreateResult, DeleteCommand, DeleteResult, ErrorCode, ErrorCodes, ErrorResult, HeadCommand, HeadResult, IdempotentAppendBatchCommand, IdempotentAppendBatchResult, IdempotentAppendCommand, IdempotentAppendResult, IdempotentCloseCommand, IdempotentCloseResult, IdempotentDetachCommand, IdempotentDetachResult, InitCommand, InitResult, ReadChunk, ReadCommand, ReadResult, ServerCloseCommand, 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-
|
|
1
|
+
import { ErrorCodes, calculateStats, decodeBase64, encodeBase64, formatStats, parseCommand, parseResult, serializeCommand, serializeResult } from "./protocol-BnqUAMKe.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.2.
|
|
4
|
+
"version": "0.2.2",
|
|
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.2.
|
|
18
|
-
"@durable-streams/server": "0.2.
|
|
17
|
+
"@durable-streams/client": "0.2.2",
|
|
18
|
+
"@durable-streams/server": "0.2.2"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"tsdown": "^0.9.0",
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
DurableStreamError,
|
|
16
16
|
FetchError,
|
|
17
17
|
IdempotentProducer,
|
|
18
|
+
StreamClosedError,
|
|
18
19
|
stream,
|
|
19
20
|
} from "@durable-streams/client"
|
|
20
21
|
import {
|
|
@@ -40,6 +41,53 @@ let serverUrl = ``
|
|
|
40
41
|
// Track content-type per stream path for append operations
|
|
41
42
|
const streamContentTypes = new Map<string, string>()
|
|
42
43
|
|
|
44
|
+
// Track IdempotentProducer instances to maintain state across operations
|
|
45
|
+
// Key: "path|producerId|epoch"
|
|
46
|
+
const producerCache = new Map<string, IdempotentProducer>()
|
|
47
|
+
|
|
48
|
+
function getProducerCacheKey(
|
|
49
|
+
path: string,
|
|
50
|
+
producerId: string,
|
|
51
|
+
epoch: number
|
|
52
|
+
): string {
|
|
53
|
+
return `${path}|${producerId}|${epoch}`
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getOrCreateProducer(
|
|
57
|
+
path: string,
|
|
58
|
+
producerId: string,
|
|
59
|
+
epoch: number,
|
|
60
|
+
autoClaim: boolean = false
|
|
61
|
+
): IdempotentProducer {
|
|
62
|
+
const key = getProducerCacheKey(path, producerId, epoch)
|
|
63
|
+
let producer = producerCache.get(key)
|
|
64
|
+
if (!producer) {
|
|
65
|
+
const contentType =
|
|
66
|
+
streamContentTypes.get(path) ?? `application/octet-stream`
|
|
67
|
+
const ds = new DurableStream({
|
|
68
|
+
url: `${serverUrl}${path}`,
|
|
69
|
+
contentType,
|
|
70
|
+
})
|
|
71
|
+
producer = new IdempotentProducer(ds, producerId, {
|
|
72
|
+
epoch,
|
|
73
|
+
autoClaim,
|
|
74
|
+
maxInFlight: 1,
|
|
75
|
+
lingerMs: 0, // Send immediately for testing
|
|
76
|
+
})
|
|
77
|
+
producerCache.set(key, producer)
|
|
78
|
+
}
|
|
79
|
+
return producer
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function removeProducerFromCache(
|
|
83
|
+
path: string,
|
|
84
|
+
producerId: string,
|
|
85
|
+
epoch: number
|
|
86
|
+
): void {
|
|
87
|
+
const key = getProducerCacheKey(path, producerId, epoch)
|
|
88
|
+
producerCache.delete(key)
|
|
89
|
+
}
|
|
90
|
+
|
|
43
91
|
// Dynamic headers/params state
|
|
44
92
|
interface DynamicValue {
|
|
45
93
|
type: `counter` | `timestamp` | `token`
|
|
@@ -123,6 +171,7 @@ async function handleCommand(command: TestCommand): Promise<TestResult> {
|
|
|
123
171
|
streamContentTypes.clear()
|
|
124
172
|
dynamicHeaders.clear()
|
|
125
173
|
dynamicParams.clear()
|
|
174
|
+
producerCache.clear()
|
|
126
175
|
return {
|
|
127
176
|
type: `init`,
|
|
128
177
|
success: true,
|
|
@@ -160,6 +209,8 @@ async function handleCommand(command: TestCommand): Promise<TestResult> {
|
|
|
160
209
|
ttlSeconds: command.ttlSeconds,
|
|
161
210
|
expiresAt: command.expiresAt,
|
|
162
211
|
headers: command.headers,
|
|
212
|
+
closed: command.closed,
|
|
213
|
+
body: command.data,
|
|
163
214
|
})
|
|
164
215
|
|
|
165
216
|
// Cache the content-type
|
|
@@ -326,6 +377,7 @@ async function handleCommand(command: TestCommand): Promise<TestResult> {
|
|
|
326
377
|
const chunks: Array<ReadChunk> = []
|
|
327
378
|
let finalOffset = command.offset ?? response.offset
|
|
328
379
|
let upToDate = response.upToDate
|
|
380
|
+
let streamClosed = response.streamClosed
|
|
329
381
|
|
|
330
382
|
// Collect chunks using body() for non-live mode or bodyStream() for live
|
|
331
383
|
const maxChunks = command.maxChunks ?? 100
|
|
@@ -359,6 +411,7 @@ async function handleCommand(command: TestCommand): Promise<TestResult> {
|
|
|
359
411
|
}
|
|
360
412
|
finalOffset = response.offset
|
|
361
413
|
upToDate = response.upToDate
|
|
414
|
+
streamClosed = response.streamClosed
|
|
362
415
|
} else {
|
|
363
416
|
// For live mode, use subscribeBytes which provides per-chunk metadata
|
|
364
417
|
const decoder = new TextDecoder()
|
|
@@ -405,6 +458,7 @@ async function handleCommand(command: TestCommand): Promise<TestResult> {
|
|
|
405
458
|
|
|
406
459
|
finalOffset = chunk.offset
|
|
407
460
|
upToDate = chunk.upToDate
|
|
461
|
+
streamClosed = chunk.streamClosed
|
|
408
462
|
|
|
409
463
|
// For waitForUpToDate, stop when we've reached up-to-date
|
|
410
464
|
if (command.waitForUpToDate && chunk.upToDate) {
|
|
@@ -435,6 +489,7 @@ async function handleCommand(command: TestCommand): Promise<TestResult> {
|
|
|
435
489
|
// For empty streams, capture the final upToDate from response
|
|
436
490
|
upToDate = response.upToDate
|
|
437
491
|
finalOffset = response.offset
|
|
492
|
+
streamClosed = response.streamClosed
|
|
438
493
|
resolve()
|
|
439
494
|
}
|
|
440
495
|
})
|
|
@@ -463,6 +518,7 @@ async function handleCommand(command: TestCommand): Promise<TestResult> {
|
|
|
463
518
|
chunks,
|
|
464
519
|
offset: finalOffset,
|
|
465
520
|
upToDate,
|
|
521
|
+
streamClosed,
|
|
466
522
|
headersSent:
|
|
467
523
|
Object.keys(headersSent).length > 0 ? headersSent : undefined,
|
|
468
524
|
paramsSent:
|
|
@@ -492,6 +548,7 @@ async function handleCommand(command: TestCommand): Promise<TestResult> {
|
|
|
492
548
|
status: 200,
|
|
493
549
|
offset: result.offset,
|
|
494
550
|
contentType: result.contentType,
|
|
551
|
+
streamClosed: result.streamClosed,
|
|
495
552
|
// Note: HeadResult from client doesn't expose TTL info currently
|
|
496
553
|
}
|
|
497
554
|
} catch (err) {
|
|
@@ -520,6 +577,34 @@ async function handleCommand(command: TestCommand): Promise<TestResult> {
|
|
|
520
577
|
}
|
|
521
578
|
}
|
|
522
579
|
|
|
580
|
+
case `close`: {
|
|
581
|
+
try {
|
|
582
|
+
const url = `${serverUrl}${command.path}`
|
|
583
|
+
|
|
584
|
+
// Get content-type from cache or use default
|
|
585
|
+
const contentType =
|
|
586
|
+
streamContentTypes.get(command.path) ?? `application/octet-stream`
|
|
587
|
+
|
|
588
|
+
const ds = new DurableStream({
|
|
589
|
+
url,
|
|
590
|
+
contentType: command.contentType ?? contentType,
|
|
591
|
+
})
|
|
592
|
+
|
|
593
|
+
const closeResult = await ds.close({
|
|
594
|
+
body: command.data,
|
|
595
|
+
contentType: command.contentType,
|
|
596
|
+
})
|
|
597
|
+
|
|
598
|
+
return {
|
|
599
|
+
type: `close`,
|
|
600
|
+
success: true,
|
|
601
|
+
finalOffset: closeResult.finalOffset,
|
|
602
|
+
}
|
|
603
|
+
} catch (err) {
|
|
604
|
+
return errorResult(`close`, err)
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
523
608
|
case `shutdown`: {
|
|
524
609
|
return {
|
|
525
610
|
type: `shutdown`,
|
|
@@ -565,39 +650,23 @@ async function handleCommand(command: TestCommand): Promise<TestResult> {
|
|
|
565
650
|
|
|
566
651
|
case `idempotent-append`: {
|
|
567
652
|
try {
|
|
568
|
-
const
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
epoch: command.epoch,
|
|
581
|
-
autoClaim: command.autoClaim,
|
|
582
|
-
maxInFlight: 1,
|
|
583
|
-
lingerMs: 0, // Send immediately for testing
|
|
584
|
-
})
|
|
585
|
-
|
|
586
|
-
try {
|
|
587
|
-
// append() is fire-and-forget (synchronous), then flush() sends the batch
|
|
588
|
-
// Data is already pre-serialized, pass directly to append()
|
|
589
|
-
producer.append(command.data)
|
|
590
|
-
await producer.flush()
|
|
591
|
-
await producer.close()
|
|
653
|
+
const producer = getOrCreateProducer(
|
|
654
|
+
command.path,
|
|
655
|
+
command.producerId,
|
|
656
|
+
command.epoch,
|
|
657
|
+
command.autoClaim
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
// append() is fire-and-forget (synchronous), then flush() sends the batch
|
|
661
|
+
// Data is already pre-serialized, pass directly to append()
|
|
662
|
+
producer.append(command.data)
|
|
663
|
+
await producer.flush()
|
|
664
|
+
// Don't detach - keep producer for subsequent operations
|
|
592
665
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
}
|
|
598
|
-
} catch (err) {
|
|
599
|
-
await producer.close()
|
|
600
|
-
throw err
|
|
666
|
+
return {
|
|
667
|
+
type: `idempotent-append`,
|
|
668
|
+
success: true,
|
|
669
|
+
status: 200,
|
|
601
670
|
}
|
|
602
671
|
} catch (err) {
|
|
603
672
|
return errorResult(`idempotent-append`, err)
|
|
@@ -640,7 +709,8 @@ async function handleCommand(command: TestCommand): Promise<TestResult> {
|
|
|
640
709
|
|
|
641
710
|
// flush() sends the batch and waits for completion
|
|
642
711
|
await producer.flush()
|
|
643
|
-
|
|
712
|
+
// Use detach() to stop producer without closing the stream
|
|
713
|
+
await producer.detach()
|
|
644
714
|
|
|
645
715
|
return {
|
|
646
716
|
type: `idempotent-append-batch`,
|
|
@@ -648,7 +718,7 @@ async function handleCommand(command: TestCommand): Promise<TestResult> {
|
|
|
648
718
|
status: 200,
|
|
649
719
|
}
|
|
650
720
|
} catch (err) {
|
|
651
|
-
await producer.
|
|
721
|
+
await producer.detach()
|
|
652
722
|
throw err
|
|
653
723
|
}
|
|
654
724
|
} catch (err) {
|
|
@@ -656,6 +726,56 @@ async function handleCommand(command: TestCommand): Promise<TestResult> {
|
|
|
656
726
|
}
|
|
657
727
|
}
|
|
658
728
|
|
|
729
|
+
case `idempotent-close`: {
|
|
730
|
+
try {
|
|
731
|
+
const producer = getOrCreateProducer(
|
|
732
|
+
command.path,
|
|
733
|
+
command.producerId,
|
|
734
|
+
command.epoch,
|
|
735
|
+
command.autoClaim
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
// Close the stream with optional final message
|
|
739
|
+
const result = await producer.close(command.data)
|
|
740
|
+
|
|
741
|
+
// Keep producer in cache - subsequent close() calls should be idempotent
|
|
742
|
+
// The producer's internal #closed flag will handle idempotency
|
|
743
|
+
|
|
744
|
+
return {
|
|
745
|
+
type: `idempotent-close`,
|
|
746
|
+
success: true,
|
|
747
|
+
status: 200,
|
|
748
|
+
finalOffset: result.finalOffset,
|
|
749
|
+
}
|
|
750
|
+
} catch (err) {
|
|
751
|
+
return errorResult(`idempotent-close`, err)
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
case `idempotent-detach`: {
|
|
756
|
+
try {
|
|
757
|
+
const producer = getOrCreateProducer(
|
|
758
|
+
command.path,
|
|
759
|
+
command.producerId,
|
|
760
|
+
command.epoch
|
|
761
|
+
)
|
|
762
|
+
|
|
763
|
+
// Detach the producer without closing the stream
|
|
764
|
+
await producer.detach()
|
|
765
|
+
|
|
766
|
+
// Remove from cache since producer is detached
|
|
767
|
+
removeProducerFromCache(command.path, command.producerId, command.epoch)
|
|
768
|
+
|
|
769
|
+
return {
|
|
770
|
+
type: `idempotent-detach`,
|
|
771
|
+
success: true,
|
|
772
|
+
status: 200,
|
|
773
|
+
}
|
|
774
|
+
} catch (err) {
|
|
775
|
+
return errorResult(`idempotent-detach`, err)
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
659
779
|
case `validate`: {
|
|
660
780
|
// Test client-side input validation
|
|
661
781
|
const { target } = command
|
|
@@ -728,6 +848,18 @@ function errorResult(
|
|
|
728
848
|
commandType: TestCommand[`type`],
|
|
729
849
|
err: unknown
|
|
730
850
|
): TestResult {
|
|
851
|
+
// Handle StreamClosedError specifically
|
|
852
|
+
if (err instanceof StreamClosedError) {
|
|
853
|
+
return {
|
|
854
|
+
type: `error`,
|
|
855
|
+
success: false,
|
|
856
|
+
commandType,
|
|
857
|
+
status: 409,
|
|
858
|
+
errorCode: ErrorCodes.STREAM_CLOSED,
|
|
859
|
+
message: err.message,
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
731
863
|
if (err instanceof DurableStreamError) {
|
|
732
864
|
let errorCode: ErrorCode = ErrorCodes.INTERNAL_ERROR
|
|
733
865
|
let status: number | undefined
|
|
@@ -742,6 +874,9 @@ function errorResult(
|
|
|
742
874
|
} else if (err.code === `CONFLICT_SEQ`) {
|
|
743
875
|
errorCode = ErrorCodes.SEQUENCE_CONFLICT
|
|
744
876
|
status = 409
|
|
877
|
+
} else if (err.code === `STREAM_CLOSED`) {
|
|
878
|
+
errorCode = ErrorCodes.STREAM_CLOSED
|
|
879
|
+
status = 409
|
|
745
880
|
} else if (err.code === `BAD_REQUEST`) {
|
|
746
881
|
errorCode = ErrorCodes.INVALID_OFFSET
|
|
747
882
|
status = 400
|
|
@@ -766,8 +901,12 @@ function errorResult(
|
|
|
766
901
|
if (err.status === 404) {
|
|
767
902
|
errorCode = ErrorCodes.NOT_FOUND
|
|
768
903
|
} else if (err.status === 409) {
|
|
769
|
-
// Check for
|
|
770
|
-
|
|
904
|
+
// Check for stream closed header first
|
|
905
|
+
const streamClosedHeader =
|
|
906
|
+
err.headers[`stream-closed`] ?? err.headers[`Stream-Closed`]
|
|
907
|
+
if (streamClosedHeader?.toLowerCase() === `true`) {
|
|
908
|
+
errorCode = ErrorCodes.STREAM_CLOSED
|
|
909
|
+
} else if (msg.includes(`sequence`)) {
|
|
771
910
|
errorCode = ErrorCodes.SEQUENCE_CONFLICT
|
|
772
911
|
} else {
|
|
773
912
|
errorCode = ErrorCodes.CONFLICT
|