@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.
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
  const require_chunk = require('../chunk-BCwAaXi7.cjs');
4
- const require_protocol = require('../protocol-IioVPNaP.cjs');
4
+ const require_protocol = require('../protocol-sDk3deGa.cjs');
5
5
  const node_readline = require_chunk.__toESM(require("node:readline"));
6
6
  const __durable_streams_client = require_chunk.__toESM(require("@durable-streams/client"));
7
7
 
@@ -9,6 +9,33 @@ const __durable_streams_client = require_chunk.__toESM(require("@durable-streams
9
9
  const CLIENT_VERSION = `0.0.1`;
10
10
  let serverUrl = ``;
11
11
  const streamContentTypes = new Map();
12
+ const producerCache = new Map();
13
+ function getProducerCacheKey(path, producerId, epoch) {
14
+ return `${path}|${producerId}|${epoch}`;
15
+ }
16
+ function getOrCreateProducer(path, producerId, epoch, autoClaim = false) {
17
+ const key = getProducerCacheKey(path, producerId, epoch);
18
+ let producer = producerCache.get(key);
19
+ if (!producer) {
20
+ const contentType = streamContentTypes.get(path) ?? `application/octet-stream`;
21
+ const ds = new __durable_streams_client.DurableStream({
22
+ url: `${serverUrl}${path}`,
23
+ contentType
24
+ });
25
+ producer = new __durable_streams_client.IdempotentProducer(ds, producerId, {
26
+ epoch,
27
+ autoClaim,
28
+ maxInFlight: 1,
29
+ lingerMs: 0
30
+ });
31
+ producerCache.set(key, producer);
32
+ }
33
+ return producer;
34
+ }
35
+ function removeProducerFromCache(path, producerId, epoch) {
36
+ const key = getProducerCacheKey(path, producerId, epoch);
37
+ producerCache.delete(key);
38
+ }
12
39
  const dynamicHeaders = new Map();
13
40
  const dynamicParams = new Map();
14
41
  /** Resolve dynamic headers, returning both the header function map and tracked values */
@@ -70,6 +97,7 @@ async function handleCommand(command) {
70
97
  streamContentTypes.clear();
71
98
  dynamicHeaders.clear();
72
99
  dynamicParams.clear();
100
+ producerCache.clear();
73
101
  return {
74
102
  type: `init`,
75
103
  success: true,
@@ -99,7 +127,9 @@ async function handleCommand(command) {
99
127
  contentType,
100
128
  ttlSeconds: command.ttlSeconds,
101
129
  expiresAt: command.expiresAt,
102
- headers: command.headers
130
+ headers: command.headers,
131
+ closed: command.closed,
132
+ body: command.data
103
133
  });
104
134
  streamContentTypes.set(command.path, contentType);
105
135
  const head = await ds.head();
@@ -204,6 +234,7 @@ async function handleCommand(command) {
204
234
  const chunks = [];
205
235
  let finalOffset = command.offset ?? response.offset;
206
236
  let upToDate = response.upToDate;
237
+ let streamClosed = response.streamClosed;
207
238
  const maxChunks = command.maxChunks ?? 100;
208
239
  const contentType = streamContentTypes.get(command.path);
209
240
  const isJson = contentType?.includes(`application/json`) ?? false;
@@ -223,6 +254,7 @@ async function handleCommand(command) {
223
254
  }
224
255
  finalOffset = response.offset;
225
256
  upToDate = response.upToDate;
257
+ streamClosed = response.streamClosed;
226
258
  } else {
227
259
  const decoder = new TextDecoder();
228
260
  const startTime = Date.now();
@@ -253,6 +285,7 @@ async function handleCommand(command) {
253
285
  }
254
286
  finalOffset = chunk.offset;
255
287
  upToDate = chunk.upToDate;
288
+ streamClosed = chunk.streamClosed;
256
289
  if (command.waitForUpToDate && chunk.upToDate) {
257
290
  done = true;
258
291
  clearTimeout(subscriptionTimeoutId);
@@ -272,6 +305,7 @@ async function handleCommand(command) {
272
305
  clearTimeout(subscriptionTimeoutId);
273
306
  upToDate = response.upToDate;
274
307
  finalOffset = response.offset;
308
+ streamClosed = response.streamClosed;
275
309
  resolve();
276
310
  }
277
311
  }).catch((err) => {
@@ -291,6 +325,7 @@ async function handleCommand(command) {
291
325
  chunks,
292
326
  offset: finalOffset,
293
327
  upToDate,
328
+ streamClosed,
294
329
  headersSent: Object.keys(headersSent).length > 0 ? headersSent : void 0,
295
330
  paramsSent: Object.keys(paramsSent).length > 0 ? paramsSent : void 0
296
331
  };
@@ -309,7 +344,8 @@ async function handleCommand(command) {
309
344
  success: true,
310
345
  status: 200,
311
346
  offset: result.offset,
312
- contentType: result.contentType
347
+ contentType: result.contentType,
348
+ streamClosed: result.streamClosed
313
349
  };
314
350
  } catch (err) {
315
351
  return errorResult(`head`, err);
@@ -329,6 +365,25 @@ async function handleCommand(command) {
329
365
  } catch (err) {
330
366
  return errorResult(`delete`, err);
331
367
  }
368
+ case `close`: try {
369
+ const url = `${serverUrl}${command.path}`;
370
+ const contentType = streamContentTypes.get(command.path) ?? `application/octet-stream`;
371
+ const ds = new __durable_streams_client.DurableStream({
372
+ url,
373
+ contentType: command.contentType ?? contentType
374
+ });
375
+ const closeResult = await ds.close({
376
+ body: command.data,
377
+ contentType: command.contentType
378
+ });
379
+ return {
380
+ type: `close`,
381
+ success: true,
382
+ finalOffset: closeResult.finalOffset
383
+ };
384
+ } catch (err) {
385
+ return errorResult(`close`, err);
386
+ }
332
387
  case `shutdown`: return {
333
388
  type: `shutdown`,
334
389
  success: true
@@ -364,31 +419,14 @@ async function handleCommand(command) {
364
419
  };
365
420
  }
366
421
  case `idempotent-append`: try {
367
- const url = `${serverUrl}${command.path}`;
368
- const contentType = streamContentTypes.get(command.path) ?? `application/octet-stream`;
369
- const ds = new __durable_streams_client.DurableStream({
370
- url,
371
- contentType
372
- });
373
- const producer = new __durable_streams_client.IdempotentProducer(ds, command.producerId, {
374
- epoch: command.epoch,
375
- autoClaim: command.autoClaim,
376
- maxInFlight: 1,
377
- lingerMs: 0
378
- });
379
- try {
380
- producer.append(command.data);
381
- await producer.flush();
382
- await producer.close();
383
- return {
384
- type: `idempotent-append`,
385
- success: true,
386
- status: 200
387
- };
388
- } catch (err) {
389
- await producer.close();
390
- throw err;
391
- }
422
+ const producer = getOrCreateProducer(command.path, command.producerId, command.epoch, command.autoClaim);
423
+ producer.append(command.data);
424
+ await producer.flush();
425
+ return {
426
+ type: `idempotent-append`,
427
+ success: true,
428
+ status: 200
429
+ };
392
430
  } catch (err) {
393
431
  return errorResult(`idempotent-append`, err);
394
432
  }
@@ -411,19 +449,43 @@ async function handleCommand(command) {
411
449
  try {
412
450
  for (const item of command.items) producer.append(item);
413
451
  await producer.flush();
414
- await producer.close();
452
+ await producer.detach();
415
453
  return {
416
454
  type: `idempotent-append-batch`,
417
455
  success: true,
418
456
  status: 200
419
457
  };
420
458
  } catch (err) {
421
- await producer.close();
459
+ await producer.detach();
422
460
  throw err;
423
461
  }
424
462
  } catch (err) {
425
463
  return errorResult(`idempotent-append-batch`, err);
426
464
  }
465
+ case `idempotent-close`: try {
466
+ const producer = getOrCreateProducer(command.path, command.producerId, command.epoch, command.autoClaim);
467
+ const result = await producer.close(command.data);
468
+ return {
469
+ type: `idempotent-close`,
470
+ success: true,
471
+ status: 200,
472
+ finalOffset: result.finalOffset
473
+ };
474
+ } catch (err) {
475
+ return errorResult(`idempotent-close`, err);
476
+ }
477
+ case `idempotent-detach`: try {
478
+ const producer = getOrCreateProducer(command.path, command.producerId, command.epoch);
479
+ await producer.detach();
480
+ removeProducerFromCache(command.path, command.producerId, command.epoch);
481
+ return {
482
+ type: `idempotent-detach`,
483
+ success: true,
484
+ status: 200
485
+ };
486
+ } catch (err) {
487
+ return errorResult(`idempotent-detach`, err);
488
+ }
427
489
  case `validate`: {
428
490
  const { target } = command;
429
491
  try {
@@ -471,6 +533,14 @@ async function handleCommand(command) {
471
533
  }
472
534
  }
473
535
  function errorResult(commandType, err) {
536
+ if (err instanceof __durable_streams_client.StreamClosedError) return {
537
+ type: `error`,
538
+ success: false,
539
+ commandType,
540
+ status: 409,
541
+ errorCode: require_protocol.ErrorCodes.STREAM_CLOSED,
542
+ message: err.message
543
+ };
474
544
  if (err instanceof __durable_streams_client.DurableStreamError) {
475
545
  let errorCode = require_protocol.ErrorCodes.INTERNAL_ERROR;
476
546
  let status;
@@ -483,6 +553,9 @@ function errorResult(commandType, err) {
483
553
  } else if (err.code === `CONFLICT_SEQ`) {
484
554
  errorCode = require_protocol.ErrorCodes.SEQUENCE_CONFLICT;
485
555
  status = 409;
556
+ } else if (err.code === `STREAM_CLOSED`) {
557
+ errorCode = require_protocol.ErrorCodes.STREAM_CLOSED;
558
+ status = 409;
486
559
  } else if (err.code === `BAD_REQUEST`) {
487
560
  errorCode = require_protocol.ErrorCodes.INVALID_OFFSET;
488
561
  status = 400;
@@ -500,9 +573,12 @@ function errorResult(commandType, err) {
500
573
  let errorCode;
501
574
  const msg = err.message.toLowerCase();
502
575
  if (err.status === 404) errorCode = require_protocol.ErrorCodes.NOT_FOUND;
503
- else if (err.status === 409) if (msg.includes(`sequence`)) errorCode = require_protocol.ErrorCodes.SEQUENCE_CONFLICT;
504
- else errorCode = require_protocol.ErrorCodes.CONFLICT;
505
- else if (err.status === 400) if (msg.includes(`offset`) || msg.includes(`invalid`)) errorCode = require_protocol.ErrorCodes.INVALID_OFFSET;
576
+ else if (err.status === 409) {
577
+ const streamClosedHeader = err.headers[`stream-closed`] ?? err.headers[`Stream-Closed`];
578
+ if (streamClosedHeader?.toLowerCase() === `true`) errorCode = require_protocol.ErrorCodes.STREAM_CLOSED;
579
+ else if (msg.includes(`sequence`)) errorCode = require_protocol.ErrorCodes.SEQUENCE_CONFLICT;
580
+ else errorCode = require_protocol.ErrorCodes.CONFLICT;
581
+ } else if (err.status === 400) if (msg.includes(`offset`) || msg.includes(`invalid`)) errorCode = require_protocol.ErrorCodes.INVALID_OFFSET;
506
582
  else errorCode = require_protocol.ErrorCodes.UNEXPECTED_STATUS;
507
583
  else errorCode = require_protocol.ErrorCodes.UNEXPECTED_STATUS;
508
584
  return {
@@ -1,12 +1,39 @@
1
1
  #!/usr/bin/env node
2
- import { ErrorCodes, decodeBase64, parseCommand, serializeResult } from "../protocol-1p0soayz.js";
2
+ import { ErrorCodes, decodeBase64, parseCommand, serializeResult } from "../protocol-BnqUAMKe.js";
3
3
  import { createInterface } from "node:readline";
4
- import { DurableStream, DurableStreamError, FetchError, IdempotentProducer, stream } from "@durable-streams/client";
4
+ import { DurableStream, DurableStreamError, FetchError, IdempotentProducer, StreamClosedError, stream } from "@durable-streams/client";
5
5
 
6
6
  //#region src/adapters/typescript-adapter.ts
7
7
  const CLIENT_VERSION = `0.0.1`;
8
8
  let serverUrl = ``;
9
9
  const streamContentTypes = new Map();
10
+ const producerCache = new Map();
11
+ function getProducerCacheKey(path, producerId, epoch) {
12
+ return `${path}|${producerId}|${epoch}`;
13
+ }
14
+ function getOrCreateProducer(path, producerId, epoch, autoClaim = false) {
15
+ const key = getProducerCacheKey(path, producerId, epoch);
16
+ let producer = producerCache.get(key);
17
+ if (!producer) {
18
+ const contentType = streamContentTypes.get(path) ?? `application/octet-stream`;
19
+ const ds = new DurableStream({
20
+ url: `${serverUrl}${path}`,
21
+ contentType
22
+ });
23
+ producer = new IdempotentProducer(ds, producerId, {
24
+ epoch,
25
+ autoClaim,
26
+ maxInFlight: 1,
27
+ lingerMs: 0
28
+ });
29
+ producerCache.set(key, producer);
30
+ }
31
+ return producer;
32
+ }
33
+ function removeProducerFromCache(path, producerId, epoch) {
34
+ const key = getProducerCacheKey(path, producerId, epoch);
35
+ producerCache.delete(key);
36
+ }
10
37
  const dynamicHeaders = new Map();
11
38
  const dynamicParams = new Map();
12
39
  /** Resolve dynamic headers, returning both the header function map and tracked values */
@@ -68,6 +95,7 @@ async function handleCommand(command) {
68
95
  streamContentTypes.clear();
69
96
  dynamicHeaders.clear();
70
97
  dynamicParams.clear();
98
+ producerCache.clear();
71
99
  return {
72
100
  type: `init`,
73
101
  success: true,
@@ -97,7 +125,9 @@ async function handleCommand(command) {
97
125
  contentType,
98
126
  ttlSeconds: command.ttlSeconds,
99
127
  expiresAt: command.expiresAt,
100
- headers: command.headers
128
+ headers: command.headers,
129
+ closed: command.closed,
130
+ body: command.data
101
131
  });
102
132
  streamContentTypes.set(command.path, contentType);
103
133
  const head = await ds.head();
@@ -202,6 +232,7 @@ async function handleCommand(command) {
202
232
  const chunks = [];
203
233
  let finalOffset = command.offset ?? response.offset;
204
234
  let upToDate = response.upToDate;
235
+ let streamClosed = response.streamClosed;
205
236
  const maxChunks = command.maxChunks ?? 100;
206
237
  const contentType = streamContentTypes.get(command.path);
207
238
  const isJson = contentType?.includes(`application/json`) ?? false;
@@ -221,6 +252,7 @@ async function handleCommand(command) {
221
252
  }
222
253
  finalOffset = response.offset;
223
254
  upToDate = response.upToDate;
255
+ streamClosed = response.streamClosed;
224
256
  } else {
225
257
  const decoder = new TextDecoder();
226
258
  const startTime = Date.now();
@@ -251,6 +283,7 @@ async function handleCommand(command) {
251
283
  }
252
284
  finalOffset = chunk.offset;
253
285
  upToDate = chunk.upToDate;
286
+ streamClosed = chunk.streamClosed;
254
287
  if (command.waitForUpToDate && chunk.upToDate) {
255
288
  done = true;
256
289
  clearTimeout(subscriptionTimeoutId);
@@ -270,6 +303,7 @@ async function handleCommand(command) {
270
303
  clearTimeout(subscriptionTimeoutId);
271
304
  upToDate = response.upToDate;
272
305
  finalOffset = response.offset;
306
+ streamClosed = response.streamClosed;
273
307
  resolve();
274
308
  }
275
309
  }).catch((err) => {
@@ -289,6 +323,7 @@ async function handleCommand(command) {
289
323
  chunks,
290
324
  offset: finalOffset,
291
325
  upToDate,
326
+ streamClosed,
292
327
  headersSent: Object.keys(headersSent).length > 0 ? headersSent : void 0,
293
328
  paramsSent: Object.keys(paramsSent).length > 0 ? paramsSent : void 0
294
329
  };
@@ -307,7 +342,8 @@ async function handleCommand(command) {
307
342
  success: true,
308
343
  status: 200,
309
344
  offset: result.offset,
310
- contentType: result.contentType
345
+ contentType: result.contentType,
346
+ streamClosed: result.streamClosed
311
347
  };
312
348
  } catch (err) {
313
349
  return errorResult(`head`, err);
@@ -327,6 +363,25 @@ async function handleCommand(command) {
327
363
  } catch (err) {
328
364
  return errorResult(`delete`, err);
329
365
  }
366
+ case `close`: try {
367
+ const url = `${serverUrl}${command.path}`;
368
+ const contentType = streamContentTypes.get(command.path) ?? `application/octet-stream`;
369
+ const ds = new DurableStream({
370
+ url,
371
+ contentType: command.contentType ?? contentType
372
+ });
373
+ const closeResult = await ds.close({
374
+ body: command.data,
375
+ contentType: command.contentType
376
+ });
377
+ return {
378
+ type: `close`,
379
+ success: true,
380
+ finalOffset: closeResult.finalOffset
381
+ };
382
+ } catch (err) {
383
+ return errorResult(`close`, err);
384
+ }
330
385
  case `shutdown`: return {
331
386
  type: `shutdown`,
332
387
  success: true
@@ -362,31 +417,14 @@ async function handleCommand(command) {
362
417
  };
363
418
  }
364
419
  case `idempotent-append`: try {
365
- const url = `${serverUrl}${command.path}`;
366
- const contentType = streamContentTypes.get(command.path) ?? `application/octet-stream`;
367
- const ds = new DurableStream({
368
- url,
369
- contentType
370
- });
371
- const producer = new IdempotentProducer(ds, command.producerId, {
372
- epoch: command.epoch,
373
- autoClaim: command.autoClaim,
374
- maxInFlight: 1,
375
- lingerMs: 0
376
- });
377
- try {
378
- producer.append(command.data);
379
- await producer.flush();
380
- await producer.close();
381
- return {
382
- type: `idempotent-append`,
383
- success: true,
384
- status: 200
385
- };
386
- } catch (err) {
387
- await producer.close();
388
- throw err;
389
- }
420
+ const producer = getOrCreateProducer(command.path, command.producerId, command.epoch, command.autoClaim);
421
+ producer.append(command.data);
422
+ await producer.flush();
423
+ return {
424
+ type: `idempotent-append`,
425
+ success: true,
426
+ status: 200
427
+ };
390
428
  } catch (err) {
391
429
  return errorResult(`idempotent-append`, err);
392
430
  }
@@ -409,19 +447,43 @@ async function handleCommand(command) {
409
447
  try {
410
448
  for (const item of command.items) producer.append(item);
411
449
  await producer.flush();
412
- await producer.close();
450
+ await producer.detach();
413
451
  return {
414
452
  type: `idempotent-append-batch`,
415
453
  success: true,
416
454
  status: 200
417
455
  };
418
456
  } catch (err) {
419
- await producer.close();
457
+ await producer.detach();
420
458
  throw err;
421
459
  }
422
460
  } catch (err) {
423
461
  return errorResult(`idempotent-append-batch`, err);
424
462
  }
463
+ case `idempotent-close`: try {
464
+ const producer = getOrCreateProducer(command.path, command.producerId, command.epoch, command.autoClaim);
465
+ const result = await producer.close(command.data);
466
+ return {
467
+ type: `idempotent-close`,
468
+ success: true,
469
+ status: 200,
470
+ finalOffset: result.finalOffset
471
+ };
472
+ } catch (err) {
473
+ return errorResult(`idempotent-close`, err);
474
+ }
475
+ case `idempotent-detach`: try {
476
+ const producer = getOrCreateProducer(command.path, command.producerId, command.epoch);
477
+ await producer.detach();
478
+ removeProducerFromCache(command.path, command.producerId, command.epoch);
479
+ return {
480
+ type: `idempotent-detach`,
481
+ success: true,
482
+ status: 200
483
+ };
484
+ } catch (err) {
485
+ return errorResult(`idempotent-detach`, err);
486
+ }
425
487
  case `validate`: {
426
488
  const { target } = command;
427
489
  try {
@@ -469,6 +531,14 @@ async function handleCommand(command) {
469
531
  }
470
532
  }
471
533
  function errorResult(commandType, err) {
534
+ if (err instanceof StreamClosedError) return {
535
+ type: `error`,
536
+ success: false,
537
+ commandType,
538
+ status: 409,
539
+ errorCode: ErrorCodes.STREAM_CLOSED,
540
+ message: err.message
541
+ };
472
542
  if (err instanceof DurableStreamError) {
473
543
  let errorCode = ErrorCodes.INTERNAL_ERROR;
474
544
  let status;
@@ -481,6 +551,9 @@ function errorResult(commandType, err) {
481
551
  } else if (err.code === `CONFLICT_SEQ`) {
482
552
  errorCode = ErrorCodes.SEQUENCE_CONFLICT;
483
553
  status = 409;
554
+ } else if (err.code === `STREAM_CLOSED`) {
555
+ errorCode = ErrorCodes.STREAM_CLOSED;
556
+ status = 409;
484
557
  } else if (err.code === `BAD_REQUEST`) {
485
558
  errorCode = ErrorCodes.INVALID_OFFSET;
486
559
  status = 400;
@@ -498,9 +571,12 @@ function errorResult(commandType, err) {
498
571
  let errorCode;
499
572
  const msg = err.message.toLowerCase();
500
573
  if (err.status === 404) errorCode = ErrorCodes.NOT_FOUND;
501
- else if (err.status === 409) if (msg.includes(`sequence`)) errorCode = ErrorCodes.SEQUENCE_CONFLICT;
502
- else errorCode = ErrorCodes.CONFLICT;
503
- else if (err.status === 400) if (msg.includes(`offset`) || msg.includes(`invalid`)) errorCode = ErrorCodes.INVALID_OFFSET;
574
+ else if (err.status === 409) {
575
+ const streamClosedHeader = err.headers[`stream-closed`] ?? err.headers[`Stream-Closed`];
576
+ if (streamClosedHeader?.toLowerCase() === `true`) errorCode = ErrorCodes.STREAM_CLOSED;
577
+ else if (msg.includes(`sequence`)) errorCode = ErrorCodes.SEQUENCE_CONFLICT;
578
+ else errorCode = ErrorCodes.CONFLICT;
579
+ } else if (err.status === 400) if (msg.includes(`offset`) || msg.includes(`invalid`)) errorCode = ErrorCodes.INVALID_OFFSET;
504
580
  else errorCode = ErrorCodes.UNEXPECTED_STATUS;
505
581
  else errorCode = ErrorCodes.UNEXPECTED_STATUS;
506
582
  return {
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  const require_chunk = require('./chunk-BCwAaXi7.cjs');
3
- const require_protocol = require('./protocol-IioVPNaP.cjs');
3
+ const require_protocol = require('./protocol-sDk3deGa.cjs');
4
4
  const node_child_process = require_chunk.__toESM(require("node:child_process"));
5
5
  const node_readline = require_chunk.__toESM(require("node:readline"));
6
6
  const node_crypto = require_chunk.__toESM(require("node:crypto"));
@@ -162,7 +162,9 @@ async function executeOperation(op, ctx) {
162
162
  contentType: op.contentType,
163
163
  ttlSeconds: op.ttlSeconds,
164
164
  expiresAt: op.expiresAt,
165
- headers: op.headers
165
+ headers: op.headers,
166
+ closed: op.closed,
167
+ data: op.data
166
168
  }, commandTimeout);
167
169
  if (verbose) console.log(` create ${path}: ${result.success ? `ok` : `failed`}`);
168
170
  return { result };
@@ -249,6 +251,33 @@ async function executeOperation(op, ctx) {
249
251
  if (verbose) console.log(` idempotent-append-batch ${path}: ${result.success ? `ok` : `failed`}`);
250
252
  return { result };
251
253
  }
254
+ case `idempotent-close`: {
255
+ const path = resolveVariables(op.path, variables);
256
+ const data = op.data ? resolveVariables(op.data, variables) : void 0;
257
+ const result = await client.send({
258
+ type: `idempotent-close`,
259
+ path,
260
+ producerId: op.producerId,
261
+ epoch: op.epoch ?? 0,
262
+ data,
263
+ autoClaim: op.autoClaim ?? false,
264
+ headers: op.headers
265
+ }, commandTimeout);
266
+ if (verbose) console.log(` idempotent-close ${path}: ${result.success ? `ok` : `failed`}`);
267
+ return { result };
268
+ }
269
+ case `idempotent-detach`: {
270
+ const path = resolveVariables(op.path, variables);
271
+ const result = await client.send({
272
+ type: `idempotent-detach`,
273
+ path,
274
+ producerId: op.producerId,
275
+ epoch: op.epoch ?? 0,
276
+ headers: op.headers
277
+ }, commandTimeout);
278
+ if (verbose) console.log(` idempotent-detach ${path}: ${result.success ? `ok` : `failed`}`);
279
+ return { result };
280
+ }
252
281
  case `read`: {
253
282
  const path = resolveVariables(op.path, variables);
254
283
  const offset = op.offset ? resolveVariables(op.offset, variables) : void 0;
@@ -308,6 +337,43 @@ async function executeOperation(op, ctx) {
308
337
  if (verbose) console.log(` delete ${path}: ${result.success ? `ok` : `failed`}`);
309
338
  return { result };
310
339
  }
340
+ case `close`: {
341
+ const path = resolveVariables(op.path, variables);
342
+ const result = await client.send({
343
+ type: `close`,
344
+ path,
345
+ data: op.data,
346
+ contentType: op.contentType
347
+ }, commandTimeout);
348
+ if (verbose) console.log(` close ${path}: ${result.success ? `ok` : `failed`}`);
349
+ return { result };
350
+ }
351
+ case `server-close`: {
352
+ const path = resolveVariables(op.path, variables);
353
+ try {
354
+ const headers = {
355
+ "Stream-Closed": `true`,
356
+ ...op.headers
357
+ };
358
+ if (op.data && op.contentType) headers[`content-type`] = op.contentType;
359
+ const response = await fetch(`${ctx.serverUrl}${path}`, {
360
+ method: `POST`,
361
+ body: op.data,
362
+ headers
363
+ });
364
+ const status = response.status;
365
+ const finalOffset = response.headers.get(`Stream-Next-Offset`) ?? void 0;
366
+ if (verbose) console.log(` server-close ${path}: status=${status}`);
367
+ const result = {
368
+ type: `close`,
369
+ success: true,
370
+ finalOffset: finalOffset ?? ``
371
+ };
372
+ return { result };
373
+ } catch (err) {
374
+ return { error: `Server close failed: ${err instanceof Error ? err.message : String(err)}` };
375
+ }
376
+ }
311
377
  case `wait`: {
312
378
  await new Promise((resolve) => setTimeout(resolve, op.ms));
313
379
  return {};
@@ -489,6 +555,9 @@ function isAppendResult(result) {
489
555
  function isHeadResult(result) {
490
556
  return result.type === `head` && result.success;
491
557
  }
558
+ function isCloseResult(result) {
559
+ return result.type === `close` && result.success;
560
+ }
492
561
  function isErrorResult(result) {
493
562
  return result.type === `error` && !result.success;
494
563
  }
@@ -530,6 +599,15 @@ function validateExpectation(result, expect) {
530
599
  if (expect.upToDate !== void 0 && isReadResult(result)) {
531
600
  if (result.upToDate !== expect.upToDate) return `Expected upToDate=${expect.upToDate}, got ${result.upToDate}`;
532
601
  }
602
+ if (expect.streamClosed !== void 0 && isReadResult(result)) {
603
+ if (result.streamClosed !== expect.streamClosed) return `Expected streamClosed=${expect.streamClosed}, got ${result.streamClosed}`;
604
+ }
605
+ if (expect.streamClosed !== void 0 && isHeadResult(result)) {
606
+ if (result.streamClosed !== expect.streamClosed) return `Expected streamClosed=${expect.streamClosed}, got ${result.streamClosed}`;
607
+ }
608
+ if (expect.finalOffset !== void 0 && isCloseResult(result)) {
609
+ if (result.finalOffset !== expect.finalOffset) return `Expected finalOffset=${expect.finalOffset}, got ${result.finalOffset}`;
610
+ }
533
611
  if (expect.chunkCount !== void 0 && isReadResult(result)) {
534
612
  if (result.chunks.length !== expect.chunkCount) return `Expected ${expect.chunkCount} chunks, got ${result.chunks.length}`;
535
613
  }