@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.
@@ -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.5",
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.3",
18
- "@durable-streams/server": "0.3.1"
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 prevents parsers from using trim() or similar which would
298
- incorrectly strip all leading/trailing whitespace.
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: " two leading spaces"
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
- waitForUpToDate: true
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
- count: 1
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
- count: 1
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: "sse-data"
339
- - action: close
340
- path: ${streamPath}
338
+ data: "initial"
339
+ expect:
340
+ storeOffsetAs: tailOffset
341
341
  operations:
342
- # Read via SSE - may need multiple chunks as SSE delivers data then control event
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