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