@durable-streams/client-conformance-tests 0.1.0 → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 durable-stream
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,588 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ const require_chunk = require('../chunk-BCwAaXi7.cjs');
4
+ const require_protocol = require('../protocol-XeAOKBD-.cjs');
5
+ const node_readline = require_chunk.__toESM(require("node:readline"));
6
+ const __durable_streams_client = require_chunk.__toESM(require("@durable-streams/client"));
7
+
8
+ //#region src/adapters/typescript-adapter.ts
9
+ const CLIENT_VERSION = `0.0.1`;
10
+ let serverUrl = ``;
11
+ const streamContentTypes = new Map();
12
+ const dynamicHeaders = new Map();
13
+ const dynamicParams = new Map();
14
+ /** Resolve dynamic headers, returning both the header function map and tracked values */
15
+ function resolveDynamicHeaders() {
16
+ const headers = {};
17
+ const values = {};
18
+ for (const [name, config] of dynamicHeaders.entries()) {
19
+ let value;
20
+ switch (config.type) {
21
+ case `counter`:
22
+ config.counter++;
23
+ value = config.counter.toString();
24
+ break;
25
+ case `timestamp`:
26
+ value = Date.now().toString();
27
+ break;
28
+ case `token`:
29
+ value = config.tokenValue ?? ``;
30
+ break;
31
+ }
32
+ values[name] = value;
33
+ const capturedValue = value;
34
+ headers[name] = () => capturedValue;
35
+ }
36
+ return {
37
+ headers,
38
+ values
39
+ };
40
+ }
41
+ /** Resolve dynamic params */
42
+ function resolveDynamicParams() {
43
+ const params = {};
44
+ const values = {};
45
+ for (const [name, config] of dynamicParams.entries()) {
46
+ let value;
47
+ switch (config.type) {
48
+ case `counter`:
49
+ config.counter++;
50
+ value = config.counter.toString();
51
+ break;
52
+ case `timestamp`:
53
+ value = Date.now().toString();
54
+ break;
55
+ default: value = ``;
56
+ }
57
+ values[name] = value;
58
+ const capturedValue = value;
59
+ params[name] = () => capturedValue;
60
+ }
61
+ return {
62
+ params,
63
+ values
64
+ };
65
+ }
66
+ async function handleCommand(command) {
67
+ switch (command.type) {
68
+ case `init`: {
69
+ serverUrl = command.serverUrl;
70
+ streamContentTypes.clear();
71
+ dynamicHeaders.clear();
72
+ dynamicParams.clear();
73
+ return {
74
+ type: `init`,
75
+ success: true,
76
+ clientName: `@durable-streams/client`,
77
+ clientVersion: CLIENT_VERSION,
78
+ features: {
79
+ batching: true,
80
+ sse: true,
81
+ longPoll: true,
82
+ streaming: true,
83
+ dynamicHeaders: true
84
+ }
85
+ };
86
+ }
87
+ case `create`: try {
88
+ const url = `${serverUrl}${command.path}`;
89
+ const contentType = command.contentType ?? `application/octet-stream`;
90
+ let alreadyExists = false;
91
+ try {
92
+ await __durable_streams_client.DurableStream.head({ url });
93
+ alreadyExists = true;
94
+ } catch {}
95
+ const ds = await __durable_streams_client.DurableStream.create({
96
+ url,
97
+ contentType,
98
+ ttlSeconds: command.ttlSeconds,
99
+ expiresAt: command.expiresAt,
100
+ headers: command.headers
101
+ });
102
+ streamContentTypes.set(command.path, contentType);
103
+ const head = await ds.head();
104
+ return {
105
+ type: `create`,
106
+ success: true,
107
+ status: alreadyExists ? 200 : 201,
108
+ offset: head.offset
109
+ };
110
+ } catch (err) {
111
+ return errorResult(`create`, err);
112
+ }
113
+ case `connect`: try {
114
+ const url = `${serverUrl}${command.path}`;
115
+ const ds = await __durable_streams_client.DurableStream.connect({
116
+ url,
117
+ headers: command.headers
118
+ });
119
+ const head = await ds.head();
120
+ if (head.contentType) streamContentTypes.set(command.path, head.contentType);
121
+ return {
122
+ type: `connect`,
123
+ success: true,
124
+ status: 200,
125
+ offset: head.offset
126
+ };
127
+ } catch (err) {
128
+ return errorResult(`connect`, err);
129
+ }
130
+ case `append`: try {
131
+ const url = `${serverUrl}${command.path}`;
132
+ const contentType = streamContentTypes.get(command.path) ?? `application/octet-stream`;
133
+ const { headers: dynamicHdrs, values: headersSent } = resolveDynamicHeaders();
134
+ const { values: paramsSent } = resolveDynamicParams();
135
+ const mergedHeaders = {
136
+ ...dynamicHdrs,
137
+ ...command.headers
138
+ };
139
+ const ds = new __durable_streams_client.DurableStream({
140
+ url,
141
+ headers: mergedHeaders,
142
+ contentType
143
+ });
144
+ let body;
145
+ if (command.binary) body = require_protocol.decodeBase64(command.data);
146
+ else body = command.data;
147
+ await ds.append(body, { seq: command.seq?.toString() });
148
+ const head = await ds.head();
149
+ return {
150
+ type: `append`,
151
+ success: true,
152
+ status: 200,
153
+ offset: head.offset,
154
+ headersSent: Object.keys(headersSent).length > 0 ? headersSent : void 0,
155
+ paramsSent: Object.keys(paramsSent).length > 0 ? paramsSent : void 0
156
+ };
157
+ } catch (err) {
158
+ return errorResult(`append`, err);
159
+ }
160
+ case `read`: try {
161
+ const url = `${serverUrl}${command.path}`;
162
+ const { headers: dynamicHdrs, values: headersSent } = resolveDynamicHeaders();
163
+ const { values: paramsSent } = resolveDynamicParams();
164
+ const mergedHeaders = {
165
+ ...dynamicHdrs,
166
+ ...command.headers
167
+ };
168
+ let live;
169
+ if (command.live === `long-poll`) live = `long-poll`;
170
+ else if (command.live === `sse`) live = `sse`;
171
+ else live = false;
172
+ const abortController = new AbortController();
173
+ const timeoutMs = command.timeoutMs ?? 5e3;
174
+ const timeoutId = setTimeout(() => {
175
+ abortController.abort();
176
+ }, timeoutMs);
177
+ let response;
178
+ try {
179
+ response = await (0, __durable_streams_client.stream)({
180
+ url,
181
+ offset: command.offset,
182
+ live,
183
+ headers: mergedHeaders,
184
+ signal: abortController.signal
185
+ });
186
+ } catch (err) {
187
+ clearTimeout(timeoutId);
188
+ if (abortController.signal.aborted) return {
189
+ type: `read`,
190
+ success: true,
191
+ status: 200,
192
+ chunks: [],
193
+ offset: command.offset ?? `-1`,
194
+ upToDate: true,
195
+ headersSent: Object.keys(headersSent).length > 0 ? headersSent : void 0,
196
+ paramsSent: Object.keys(paramsSent).length > 0 ? paramsSent : void 0
197
+ };
198
+ throw err;
199
+ }
200
+ clearTimeout(timeoutId);
201
+ const chunks = [];
202
+ let finalOffset = command.offset ?? response.offset;
203
+ let upToDate = response.upToDate;
204
+ const maxChunks = command.maxChunks ?? 100;
205
+ if (!live) try {
206
+ const data = await response.body();
207
+ if (data.length > 0) chunks.push({
208
+ data: new TextDecoder().decode(data),
209
+ offset: response.offset
210
+ });
211
+ finalOffset = response.offset;
212
+ upToDate = response.upToDate;
213
+ } catch {}
214
+ else {
215
+ const decoder = new TextDecoder();
216
+ const startTime = Date.now();
217
+ let chunkCount = 0;
218
+ let done = false;
219
+ await new Promise((resolve) => {
220
+ const subscriptionTimeoutId = setTimeout(() => {
221
+ done = true;
222
+ abortController.abort();
223
+ upToDate = response.upToDate || true;
224
+ finalOffset = response.offset;
225
+ resolve();
226
+ }, timeoutMs);
227
+ const unsubscribe = response.subscribeBytes(async (chunk) => {
228
+ if (done || chunkCount >= maxChunks) return;
229
+ if (Date.now() - startTime > timeoutMs) {
230
+ done = true;
231
+ resolve();
232
+ return;
233
+ }
234
+ const hasData = chunk.data.length > 0;
235
+ if (hasData) {
236
+ chunks.push({
237
+ data: decoder.decode(chunk.data),
238
+ offset: chunk.offset
239
+ });
240
+ chunkCount++;
241
+ }
242
+ finalOffset = chunk.offset;
243
+ upToDate = chunk.upToDate;
244
+ if (command.waitForUpToDate && chunk.upToDate) {
245
+ done = true;
246
+ clearTimeout(subscriptionTimeoutId);
247
+ resolve();
248
+ return;
249
+ }
250
+ if (chunkCount >= maxChunks) {
251
+ done = true;
252
+ clearTimeout(subscriptionTimeoutId);
253
+ resolve();
254
+ }
255
+ await Promise.resolve();
256
+ });
257
+ response.closed.then(() => {
258
+ if (!done) {
259
+ done = true;
260
+ clearTimeout(subscriptionTimeoutId);
261
+ upToDate = response.upToDate;
262
+ finalOffset = response.offset;
263
+ resolve();
264
+ }
265
+ }).catch(() => {
266
+ if (!done) {
267
+ done = true;
268
+ clearTimeout(subscriptionTimeoutId);
269
+ resolve();
270
+ }
271
+ });
272
+ });
273
+ }
274
+ response.cancel();
275
+ return {
276
+ type: `read`,
277
+ success: true,
278
+ status: 200,
279
+ chunks,
280
+ offset: finalOffset,
281
+ upToDate,
282
+ headersSent: Object.keys(headersSent).length > 0 ? headersSent : void 0,
283
+ paramsSent: Object.keys(paramsSent).length > 0 ? paramsSent : void 0
284
+ };
285
+ } catch (err) {
286
+ return errorResult(`read`, err);
287
+ }
288
+ case `head`: try {
289
+ const url = `${serverUrl}${command.path}`;
290
+ const result = await __durable_streams_client.DurableStream.head({
291
+ url,
292
+ headers: command.headers
293
+ });
294
+ if (result.contentType) streamContentTypes.set(command.path, result.contentType);
295
+ return {
296
+ type: `head`,
297
+ success: true,
298
+ status: 200,
299
+ offset: result.offset,
300
+ contentType: result.contentType
301
+ };
302
+ } catch (err) {
303
+ return errorResult(`head`, err);
304
+ }
305
+ case `delete`: try {
306
+ const url = `${serverUrl}${command.path}`;
307
+ await __durable_streams_client.DurableStream.delete({
308
+ url,
309
+ headers: command.headers
310
+ });
311
+ streamContentTypes.delete(command.path);
312
+ return {
313
+ type: `delete`,
314
+ success: true,
315
+ status: 200
316
+ };
317
+ } catch (err) {
318
+ return errorResult(`delete`, err);
319
+ }
320
+ case `shutdown`: return {
321
+ type: `shutdown`,
322
+ success: true
323
+ };
324
+ case `benchmark`: return handleBenchmark(command);
325
+ case `set-dynamic-header`: {
326
+ dynamicHeaders.set(command.name, {
327
+ type: command.valueType,
328
+ counter: 0,
329
+ tokenValue: command.initialValue
330
+ });
331
+ return {
332
+ type: `set-dynamic-header`,
333
+ success: true
334
+ };
335
+ }
336
+ case `set-dynamic-param`: {
337
+ dynamicParams.set(command.name, {
338
+ type: command.valueType,
339
+ counter: 0
340
+ });
341
+ return {
342
+ type: `set-dynamic-param`,
343
+ success: true
344
+ };
345
+ }
346
+ case `clear-dynamic`: {
347
+ dynamicHeaders.clear();
348
+ dynamicParams.clear();
349
+ return {
350
+ type: `clear-dynamic`,
351
+ success: true
352
+ };
353
+ }
354
+ default: return {
355
+ type: `error`,
356
+ success: false,
357
+ commandType: command.type,
358
+ errorCode: require_protocol.ErrorCodes.NOT_SUPPORTED,
359
+ message: `Unknown command type: ${command.type}`
360
+ };
361
+ }
362
+ }
363
+ function errorResult(commandType, err) {
364
+ if (err instanceof __durable_streams_client.DurableStreamError) {
365
+ let errorCode = require_protocol.ErrorCodes.INTERNAL_ERROR;
366
+ let status;
367
+ if (err.code === `NOT_FOUND`) {
368
+ errorCode = require_protocol.ErrorCodes.NOT_FOUND;
369
+ status = 404;
370
+ } else if (err.code === `CONFLICT_EXISTS`) {
371
+ errorCode = require_protocol.ErrorCodes.CONFLICT;
372
+ status = 409;
373
+ } else if (err.code === `CONFLICT_SEQ`) {
374
+ errorCode = require_protocol.ErrorCodes.SEQUENCE_CONFLICT;
375
+ status = 409;
376
+ } else if (err.code === `BAD_REQUEST`) {
377
+ errorCode = require_protocol.ErrorCodes.INVALID_OFFSET;
378
+ status = 400;
379
+ }
380
+ return {
381
+ type: `error`,
382
+ success: false,
383
+ commandType,
384
+ status,
385
+ errorCode,
386
+ message: err.message
387
+ };
388
+ }
389
+ if (err instanceof __durable_streams_client.FetchError) {
390
+ let errorCode;
391
+ const msg = err.message.toLowerCase();
392
+ if (err.status === 404) errorCode = require_protocol.ErrorCodes.NOT_FOUND;
393
+ else if (err.status === 409) if (msg.includes(`sequence`)) errorCode = require_protocol.ErrorCodes.SEQUENCE_CONFLICT;
394
+ else errorCode = require_protocol.ErrorCodes.CONFLICT;
395
+ else if (err.status === 400) if (msg.includes(`offset`) || msg.includes(`invalid`)) errorCode = require_protocol.ErrorCodes.INVALID_OFFSET;
396
+ else errorCode = require_protocol.ErrorCodes.UNEXPECTED_STATUS;
397
+ else errorCode = require_protocol.ErrorCodes.UNEXPECTED_STATUS;
398
+ return {
399
+ type: `error`,
400
+ success: false,
401
+ commandType,
402
+ status: err.status,
403
+ errorCode,
404
+ message: err.message
405
+ };
406
+ }
407
+ if (err instanceof Error) {
408
+ if (err.message.includes(`ECONNREFUSED`) || err.message.includes(`fetch`)) return {
409
+ type: `error`,
410
+ success: false,
411
+ commandType,
412
+ errorCode: require_protocol.ErrorCodes.NETWORK_ERROR,
413
+ message: err.message
414
+ };
415
+ return {
416
+ type: `error`,
417
+ success: false,
418
+ commandType,
419
+ errorCode: require_protocol.ErrorCodes.INTERNAL_ERROR,
420
+ message: err.message
421
+ };
422
+ }
423
+ return {
424
+ type: `error`,
425
+ success: false,
426
+ commandType,
427
+ errorCode: require_protocol.ErrorCodes.INTERNAL_ERROR,
428
+ message: String(err)
429
+ };
430
+ }
431
+ /**
432
+ * Handle benchmark commands with high-resolution timing.
433
+ */
434
+ async function handleBenchmark(command) {
435
+ const { iterationId, operation } = command;
436
+ try {
437
+ const startTime = process.hrtime.bigint();
438
+ const metrics = {};
439
+ switch (operation.op) {
440
+ case `append`: {
441
+ const url = `${serverUrl}${operation.path}`;
442
+ const contentType = streamContentTypes.get(operation.path) ?? `application/octet-stream`;
443
+ const ds = new __durable_streams_client.DurableStream({
444
+ url,
445
+ contentType
446
+ });
447
+ const payload = new Uint8Array(operation.size).fill(42);
448
+ await ds.append(payload);
449
+ metrics.bytesTransferred = operation.size;
450
+ break;
451
+ }
452
+ case `read`: {
453
+ const url = `${serverUrl}${operation.path}`;
454
+ const res = await (0, __durable_streams_client.stream)({
455
+ url,
456
+ offset: operation.offset,
457
+ live: false
458
+ });
459
+ const data = await res.body();
460
+ metrics.bytesTransferred = data.length;
461
+ break;
462
+ }
463
+ case `roundtrip`: {
464
+ const url = `${serverUrl}${operation.path}`;
465
+ const contentType = operation.contentType ?? `application/octet-stream`;
466
+ const ds = await __durable_streams_client.DurableStream.create({
467
+ url,
468
+ contentType
469
+ });
470
+ const payload = new Uint8Array(operation.size).fill(42);
471
+ const readPromise = (async () => {
472
+ const res = await ds.stream({ live: operation.live ?? `long-poll` });
473
+ return new Promise((resolve) => {
474
+ const unsubscribe = res.subscribeBytes(async (chunk) => {
475
+ if (chunk.data.length > 0) {
476
+ unsubscribe();
477
+ res.cancel();
478
+ resolve(chunk.data);
479
+ }
480
+ });
481
+ });
482
+ })();
483
+ await ds.append(payload);
484
+ const readData = await readPromise;
485
+ metrics.bytesTransferred = operation.size + readData.length;
486
+ break;
487
+ }
488
+ case `create`: {
489
+ const url = `${serverUrl}${operation.path}`;
490
+ await __durable_streams_client.DurableStream.create({
491
+ url,
492
+ contentType: operation.contentType ?? `application/octet-stream`
493
+ });
494
+ break;
495
+ }
496
+ case `throughput_append`: {
497
+ const url = `${serverUrl}${operation.path}`;
498
+ const contentType = streamContentTypes.get(operation.path) ?? `application/octet-stream`;
499
+ try {
500
+ await __durable_streams_client.DurableStream.create({
501
+ url,
502
+ contentType
503
+ });
504
+ } catch {}
505
+ const ds = new __durable_streams_client.DurableStream({
506
+ url,
507
+ contentType
508
+ });
509
+ const payload = new Uint8Array(operation.size).fill(42);
510
+ await Promise.all(Array.from({ length: operation.count }, () => ds.append(payload)));
511
+ metrics.bytesTransferred = operation.count * operation.size;
512
+ metrics.messagesProcessed = operation.count;
513
+ break;
514
+ }
515
+ case `throughput_read`: {
516
+ const url = `${serverUrl}${operation.path}`;
517
+ const res = await (0, __durable_streams_client.stream)({
518
+ url,
519
+ live: false
520
+ });
521
+ let count = 0;
522
+ let bytes = 0;
523
+ for await (const msg of res.jsonStream()) {
524
+ count++;
525
+ bytes += JSON.stringify(msg).length;
526
+ }
527
+ metrics.bytesTransferred = bytes;
528
+ metrics.messagesProcessed = count;
529
+ break;
530
+ }
531
+ default: return {
532
+ type: `error`,
533
+ success: false,
534
+ commandType: `benchmark`,
535
+ errorCode: require_protocol.ErrorCodes.NOT_SUPPORTED,
536
+ message: `Unknown benchmark operation: ${operation.op}`
537
+ };
538
+ }
539
+ const endTime = process.hrtime.bigint();
540
+ const durationNs = endTime - startTime;
541
+ return {
542
+ type: `benchmark`,
543
+ success: true,
544
+ iterationId,
545
+ durationNs: durationNs.toString(),
546
+ metrics
547
+ };
548
+ } catch (err) {
549
+ return {
550
+ type: `error`,
551
+ success: false,
552
+ commandType: `benchmark`,
553
+ errorCode: require_protocol.ErrorCodes.INTERNAL_ERROR,
554
+ message: err instanceof Error ? err.message : String(err)
555
+ };
556
+ }
557
+ }
558
+ async function main() {
559
+ const rl = (0, node_readline.createInterface)({
560
+ input: process.stdin,
561
+ output: process.stdout,
562
+ terminal: false
563
+ });
564
+ for await (const line of rl) {
565
+ if (!line.trim()) continue;
566
+ try {
567
+ const command = require_protocol.parseCommand(line);
568
+ const result = await handleCommand(command);
569
+ console.log(require_protocol.serializeResult(result));
570
+ if (command.type === `shutdown`) break;
571
+ } catch (err) {
572
+ console.log(require_protocol.serializeResult({
573
+ type: `error`,
574
+ success: false,
575
+ commandType: `init`,
576
+ errorCode: require_protocol.ErrorCodes.PARSE_ERROR,
577
+ message: `Failed to parse command: ${err}`
578
+ }));
579
+ }
580
+ }
581
+ process.exit(0);
582
+ }
583
+ main().catch((err) => {
584
+ console.error(`Fatal error:`, err);
585
+ process.exit(1);
586
+ });
587
+
588
+ //#endregion
@@ -0,0 +1 @@
1
+ export { };
File without changes