@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
|
@@ -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-
|
|
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
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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.
|
|
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.
|
|
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)
|
|
504
|
-
|
|
505
|
-
|
|
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-
|
|
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
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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.
|
|
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.
|
|
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)
|
|
502
|
-
|
|
503
|
-
|
|
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-
|
|
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
|
}
|