@durable-streams/client-conformance-tests 0.2.5 → 0.2.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 +60 -0
- package/dist/adapters/typescript-adapter.js +60 -0
- package/package.json +3 -3
- package/src/adapters/typescript-adapter.ts +76 -1
- package/test-cases/consumer/read-sse.yaml +17 -4
- package/test-cases/consumer/sse-parsing-errors.yaml +14 -2
- package/test-cases/consumer/streaming-equivalence.yaml +2 -0
- package/test-cases/lifecycle/stream-closure.yaml +17 -4
|
@@ -260,6 +260,66 @@ async function handleCommand(command) {
|
|
|
260
260
|
finalOffset = response.offset;
|
|
261
261
|
upToDate = response.upToDate;
|
|
262
262
|
streamClosed = response.streamClosed;
|
|
263
|
+
} else if (isJson) {
|
|
264
|
+
const startTime = Date.now();
|
|
265
|
+
let chunkCount = 0;
|
|
266
|
+
let done = false;
|
|
267
|
+
await new Promise((resolve, reject) => {
|
|
268
|
+
const subscriptionTimeoutId = setTimeout(() => {
|
|
269
|
+
done = true;
|
|
270
|
+
abortController.abort();
|
|
271
|
+
upToDate = response.upToDate || true;
|
|
272
|
+
finalOffset = response.offset;
|
|
273
|
+
streamClosed = response.streamClosed;
|
|
274
|
+
resolve();
|
|
275
|
+
}, timeoutMs);
|
|
276
|
+
const unsubscribe = response.subscribeJson(async (batch) => {
|
|
277
|
+
if (done || chunkCount >= maxChunks) return;
|
|
278
|
+
if (Date.now() - startTime > timeoutMs) {
|
|
279
|
+
done = true;
|
|
280
|
+
resolve();
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (batch.items.length > 0) {
|
|
284
|
+
chunks.push({
|
|
285
|
+
data: JSON.stringify(batch.items),
|
|
286
|
+
offset: batch.offset
|
|
287
|
+
});
|
|
288
|
+
chunkCount++;
|
|
289
|
+
}
|
|
290
|
+
finalOffset = batch.offset;
|
|
291
|
+
upToDate = batch.upToDate;
|
|
292
|
+
streamClosed = batch.streamClosed;
|
|
293
|
+
if (command.waitForUpToDate && batch.upToDate) {
|
|
294
|
+
done = true;
|
|
295
|
+
clearTimeout(subscriptionTimeoutId);
|
|
296
|
+
resolve();
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (chunkCount >= maxChunks) {
|
|
300
|
+
done = true;
|
|
301
|
+
clearTimeout(subscriptionTimeoutId);
|
|
302
|
+
resolve();
|
|
303
|
+
}
|
|
304
|
+
await Promise.resolve();
|
|
305
|
+
});
|
|
306
|
+
response.closed.then(() => {
|
|
307
|
+
if (!done) {
|
|
308
|
+
done = true;
|
|
309
|
+
clearTimeout(subscriptionTimeoutId);
|
|
310
|
+
upToDate = response.upToDate;
|
|
311
|
+
finalOffset = response.offset;
|
|
312
|
+
streamClosed = response.streamClosed;
|
|
313
|
+
resolve();
|
|
314
|
+
}
|
|
315
|
+
}).catch((err) => {
|
|
316
|
+
if (!done) {
|
|
317
|
+
done = true;
|
|
318
|
+
clearTimeout(subscriptionTimeoutId);
|
|
319
|
+
reject(err);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
});
|
|
263
323
|
} else {
|
|
264
324
|
const decoder = new TextDecoder();
|
|
265
325
|
const startTime = Date.now();
|
|
@@ -258,6 +258,66 @@ async function handleCommand(command) {
|
|
|
258
258
|
finalOffset = response.offset;
|
|
259
259
|
upToDate = response.upToDate;
|
|
260
260
|
streamClosed = response.streamClosed;
|
|
261
|
+
} else if (isJson) {
|
|
262
|
+
const startTime = Date.now();
|
|
263
|
+
let chunkCount = 0;
|
|
264
|
+
let done = false;
|
|
265
|
+
await new Promise((resolve, reject) => {
|
|
266
|
+
const subscriptionTimeoutId = setTimeout(() => {
|
|
267
|
+
done = true;
|
|
268
|
+
abortController.abort();
|
|
269
|
+
upToDate = response.upToDate || true;
|
|
270
|
+
finalOffset = response.offset;
|
|
271
|
+
streamClosed = response.streamClosed;
|
|
272
|
+
resolve();
|
|
273
|
+
}, timeoutMs);
|
|
274
|
+
const unsubscribe = response.subscribeJson(async (batch) => {
|
|
275
|
+
if (done || chunkCount >= maxChunks) return;
|
|
276
|
+
if (Date.now() - startTime > timeoutMs) {
|
|
277
|
+
done = true;
|
|
278
|
+
resolve();
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (batch.items.length > 0) {
|
|
282
|
+
chunks.push({
|
|
283
|
+
data: JSON.stringify(batch.items),
|
|
284
|
+
offset: batch.offset
|
|
285
|
+
});
|
|
286
|
+
chunkCount++;
|
|
287
|
+
}
|
|
288
|
+
finalOffset = batch.offset;
|
|
289
|
+
upToDate = batch.upToDate;
|
|
290
|
+
streamClosed = batch.streamClosed;
|
|
291
|
+
if (command.waitForUpToDate && batch.upToDate) {
|
|
292
|
+
done = true;
|
|
293
|
+
clearTimeout(subscriptionTimeoutId);
|
|
294
|
+
resolve();
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
if (chunkCount >= maxChunks) {
|
|
298
|
+
done = true;
|
|
299
|
+
clearTimeout(subscriptionTimeoutId);
|
|
300
|
+
resolve();
|
|
301
|
+
}
|
|
302
|
+
await Promise.resolve();
|
|
303
|
+
});
|
|
304
|
+
response.closed.then(() => {
|
|
305
|
+
if (!done) {
|
|
306
|
+
done = true;
|
|
307
|
+
clearTimeout(subscriptionTimeoutId);
|
|
308
|
+
upToDate = response.upToDate;
|
|
309
|
+
finalOffset = response.offset;
|
|
310
|
+
streamClosed = response.streamClosed;
|
|
311
|
+
resolve();
|
|
312
|
+
}
|
|
313
|
+
}).catch((err) => {
|
|
314
|
+
if (!done) {
|
|
315
|
+
done = true;
|
|
316
|
+
clearTimeout(subscriptionTimeoutId);
|
|
317
|
+
reject(err);
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
});
|
|
261
321
|
} else {
|
|
262
322
|
const decoder = new TextDecoder();
|
|
263
323
|
const startTime = Date.now();
|
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.7",
|
|
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.3.
|
|
17
|
+
"@durable-streams/client": "0.2.4",
|
|
18
|
+
"@durable-streams/server": "0.3.3"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"tsdown": "^0.9.0",
|
|
@@ -418,8 +418,83 @@ async function handleCommand(command: TestCommand): Promise<TestResult> {
|
|
|
418
418
|
finalOffset = response.offset
|
|
419
419
|
upToDate = response.upToDate
|
|
420
420
|
streamClosed = response.streamClosed
|
|
421
|
+
} else if (isJson) {
|
|
422
|
+
const startTime = Date.now()
|
|
423
|
+
let chunkCount = 0
|
|
424
|
+
let done = false
|
|
425
|
+
|
|
426
|
+
await new Promise<void>((resolve, reject) => {
|
|
427
|
+
const subscriptionTimeoutId = setTimeout(() => {
|
|
428
|
+
done = true
|
|
429
|
+
abortController.abort()
|
|
430
|
+
upToDate = response.upToDate || true
|
|
431
|
+
finalOffset = response.offset
|
|
432
|
+
streamClosed = response.streamClosed
|
|
433
|
+
resolve()
|
|
434
|
+
}, timeoutMs)
|
|
435
|
+
|
|
436
|
+
const unsubscribe = response.subscribeJson(async (batch) => {
|
|
437
|
+
if (done || chunkCount >= maxChunks) {
|
|
438
|
+
return
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (Date.now() - startTime > timeoutMs) {
|
|
442
|
+
done = true
|
|
443
|
+
resolve()
|
|
444
|
+
return
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (batch.items.length > 0) {
|
|
448
|
+
chunks.push({
|
|
449
|
+
data: JSON.stringify(batch.items),
|
|
450
|
+
offset: batch.offset,
|
|
451
|
+
})
|
|
452
|
+
chunkCount++
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
finalOffset = batch.offset
|
|
456
|
+
upToDate = batch.upToDate
|
|
457
|
+
streamClosed = batch.streamClosed
|
|
458
|
+
|
|
459
|
+
if (command.waitForUpToDate && batch.upToDate) {
|
|
460
|
+
done = true
|
|
461
|
+
clearTimeout(subscriptionTimeoutId)
|
|
462
|
+
resolve()
|
|
463
|
+
return
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (chunkCount >= maxChunks) {
|
|
467
|
+
done = true
|
|
468
|
+
clearTimeout(subscriptionTimeoutId)
|
|
469
|
+
resolve()
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
await Promise.resolve()
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
response.closed
|
|
476
|
+
.then(() => {
|
|
477
|
+
if (!done) {
|
|
478
|
+
done = true
|
|
479
|
+
clearTimeout(subscriptionTimeoutId)
|
|
480
|
+
upToDate = response.upToDate
|
|
481
|
+
finalOffset = response.offset
|
|
482
|
+
streamClosed = response.streamClosed
|
|
483
|
+
resolve()
|
|
484
|
+
}
|
|
485
|
+
})
|
|
486
|
+
.catch((err) => {
|
|
487
|
+
if (!done) {
|
|
488
|
+
done = true
|
|
489
|
+
clearTimeout(subscriptionTimeoutId)
|
|
490
|
+
reject(err)
|
|
491
|
+
}
|
|
492
|
+
})
|
|
493
|
+
|
|
494
|
+
void unsubscribe
|
|
495
|
+
})
|
|
421
496
|
} else {
|
|
422
|
-
// For live mode, use subscribeBytes which provides per-chunk metadata
|
|
497
|
+
// For live non-JSON mode, use subscribeBytes which provides per-chunk metadata
|
|
423
498
|
const decoder = new TextDecoder()
|
|
424
499
|
const startTime = Date.now()
|
|
425
500
|
let chunkCount = 0
|
|
@@ -294,20 +294,33 @@ tests:
|
|
|
294
294
|
Per EventSource spec section 9.2.4: "If value starts with a
|
|
295
295
|
U+0020 SPACE character, remove it from value."
|
|
296
296
|
Only ONE space should be removed, preserving any additional spaces.
|
|
297
|
-
This
|
|
298
|
-
|
|
297
|
+
This test appends data after the reader reaches the tail so every client
|
|
298
|
+
receives the payload through SSE, including fetch-then-live clients.
|
|
299
299
|
setup:
|
|
300
300
|
- action: create
|
|
301
301
|
as: streamPath
|
|
302
302
|
contentType: text/plain
|
|
303
303
|
- action: append
|
|
304
304
|
path: ${streamPath}
|
|
305
|
-
data: "
|
|
305
|
+
data: "initial"
|
|
306
|
+
expect:
|
|
307
|
+
storeOffsetAs: tailOffset
|
|
306
308
|
operations:
|
|
307
309
|
- action: read
|
|
308
310
|
path: ${streamPath}
|
|
311
|
+
offset: ${tailOffset}
|
|
309
312
|
live: sse
|
|
310
|
-
|
|
313
|
+
maxChunks: 1
|
|
314
|
+
timeoutMs: 10000
|
|
315
|
+
background: true
|
|
316
|
+
as: readOp
|
|
317
|
+
- action: wait
|
|
318
|
+
ms: 200
|
|
319
|
+
- action: server-append
|
|
320
|
+
path: ${streamPath}
|
|
321
|
+
data: " two leading spaces"
|
|
322
|
+
- action: await
|
|
323
|
+
ref: readOp
|
|
311
324
|
expect:
|
|
312
325
|
data: " two leading spaces"
|
|
313
326
|
minChunks: 1
|
|
@@ -49,6 +49,8 @@ tests:
|
|
|
49
49
|
- action: append
|
|
50
50
|
path: ${streamPath}
|
|
51
51
|
data: "data-before-error"
|
|
52
|
+
expect:
|
|
53
|
+
storeOffsetAs: tailOffset
|
|
52
54
|
operations:
|
|
53
55
|
# Inject a control event with invalid JSON
|
|
54
56
|
- action: inject-error
|
|
@@ -56,10 +58,14 @@ tests:
|
|
|
56
58
|
injectSseEvent:
|
|
57
59
|
eventType: "control"
|
|
58
60
|
data: "{invalid json here"
|
|
59
|
-
|
|
61
|
+
# Fetch-then-live makes an HTTP catch-up request before opening SSE.
|
|
62
|
+
# The first injected fault is consumed by catch-up; the second is
|
|
63
|
+
# delivered on the live SSE request.
|
|
64
|
+
count: 2
|
|
60
65
|
# Client should throw a parse error
|
|
61
66
|
- action: read
|
|
62
67
|
path: ${streamPath}
|
|
68
|
+
offset: ${tailOffset}
|
|
63
69
|
live: sse
|
|
64
70
|
maxChunks: 1
|
|
65
71
|
expect:
|
|
@@ -77,6 +83,8 @@ tests:
|
|
|
77
83
|
- action: append
|
|
78
84
|
path: ${streamPath}
|
|
79
85
|
data: "actual-data"
|
|
86
|
+
expect:
|
|
87
|
+
storeOffsetAs: tailOffset
|
|
80
88
|
operations:
|
|
81
89
|
# Inject a control event with empty data
|
|
82
90
|
- action: inject-error
|
|
@@ -84,10 +92,14 @@ tests:
|
|
|
84
92
|
injectSseEvent:
|
|
85
93
|
eventType: "control"
|
|
86
94
|
data: ""
|
|
87
|
-
|
|
95
|
+
# Fetch-then-live makes an HTTP catch-up request before opening SSE.
|
|
96
|
+
# The first injected fault is consumed by catch-up; the second is
|
|
97
|
+
# delivered on the live SSE request.
|
|
98
|
+
count: 2
|
|
88
99
|
# Client should throw because empty is not valid JSON
|
|
89
100
|
- action: read
|
|
90
101
|
path: ${streamPath}
|
|
102
|
+
offset: ${tailOffset}
|
|
91
103
|
live: sse
|
|
92
104
|
maxChunks: 1
|
|
93
105
|
expect:
|
|
@@ -131,6 +131,7 @@ tests:
|
|
|
131
131
|
path: ${streamPath}
|
|
132
132
|
offset: ${initialOffset}
|
|
133
133
|
live: long-poll
|
|
134
|
+
maxChunks: 1
|
|
134
135
|
timeoutMs: 5000
|
|
135
136
|
background: true
|
|
136
137
|
as: longpollRead
|
|
@@ -164,6 +165,7 @@ tests:
|
|
|
164
165
|
path: ${streamPath}
|
|
165
166
|
offset: ${initialOffset}
|
|
166
167
|
live: sse
|
|
168
|
+
maxChunks: 1
|
|
167
169
|
timeoutMs: 5000
|
|
168
170
|
background: true
|
|
169
171
|
as: sseRead
|
|
@@ -335,15 +335,28 @@ tests:
|
|
|
335
335
|
contentType: text/plain
|
|
336
336
|
- action: append
|
|
337
337
|
path: ${streamPath}
|
|
338
|
-
data: "
|
|
339
|
-
|
|
340
|
-
|
|
338
|
+
data: "initial"
|
|
339
|
+
expect:
|
|
340
|
+
storeOffsetAs: tailOffset
|
|
341
341
|
operations:
|
|
342
|
-
#
|
|
342
|
+
# Start at the tail, then close with a final body so the data is delivered
|
|
343
|
+
# as the SSE final event rather than as HTTP catch-up data.
|
|
343
344
|
- action: read
|
|
344
345
|
path: ${streamPath}
|
|
346
|
+
offset: ${tailOffset}
|
|
345
347
|
live: sse
|
|
348
|
+
maxChunks: 1
|
|
346
349
|
timeoutMs: 5000
|
|
350
|
+
background: true
|
|
351
|
+
as: readOp
|
|
352
|
+
- action: wait
|
|
353
|
+
ms: 200
|
|
354
|
+
- action: server-close
|
|
355
|
+
path: ${streamPath}
|
|
356
|
+
data: "sse-data"
|
|
357
|
+
contentType: text/plain
|
|
358
|
+
- action: await
|
|
359
|
+
ref: readOp
|
|
347
360
|
expect:
|
|
348
361
|
data: "sse-data"
|
|
349
362
|
streamClosed: true
|