@spikard/node 0.13.0 → 0.15.1

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/index.js DELETED
@@ -1,2059 +0,0 @@
1
- "use strict";
2
- var __create = Object.create;
3
- var __defProp = Object.defineProperty;
4
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
- var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __getProtoOf = Object.getPrototypeOf;
7
- var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __export = (target, all) => {
9
- for (var name in all)
10
- __defProp(target, name, { get: all[name], enumerable: true });
11
- };
12
- var __copyProps = (to, from, except, desc) => {
13
- if (from && typeof from === "object" || typeof from === "function") {
14
- for (let key of __getOwnPropNames(from))
15
- if (!__hasOwnProp.call(to, key) && key !== except)
16
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
- }
18
- return to;
19
- };
20
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
- // If the importer is in node compatibility mode or this is not an ESM
22
- // file that has been converted to a CommonJS file using a Babel-
23
- // compatible transform (i.e. "__esModule" has not been set), then set
24
- // "default" to the CommonJS "module.exports" for node compatibility.
25
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
- mod
27
- ));
28
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
-
30
- // src/index.ts
31
- var index_exports = {};
32
- __export(index_exports, {
33
- GrpcError: () => GrpcError,
34
- GrpcService: () => GrpcService,
35
- GrpcStatusCode: () => GrpcStatusCode,
36
- Spikard: () => Spikard,
37
- StreamingResponse: () => StreamingResponse,
38
- TestClient: () => TestClient,
39
- UploadFile: () => UploadFile,
40
- background: () => background_exports,
41
- createServiceHandler: () => createServiceHandler,
42
- createUnaryHandler: () => createUnaryHandler,
43
- del: () => del,
44
- get: () => get,
45
- patch: () => patch,
46
- post: () => post,
47
- put: () => put,
48
- route: () => route,
49
- runServer: () => runServer,
50
- wrapBodyHandler: () => wrapBodyHandler,
51
- wrapHandler: () => wrapHandler
52
- });
53
- module.exports = __toCommonJS(index_exports);
54
-
55
- // ../../node_modules/.pnpm/tsup@8.5.1_postcss@8.5.8_tsx@4.21.0_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js
56
- var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
57
- var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
58
-
59
- // src/server.ts
60
- var import_node_module2 = require("module");
61
-
62
- // src/upload.ts
63
- var UploadFile = class {
64
- /** Original filename from the client */
65
- filename;
66
- /** MIME type of the uploaded file */
67
- contentType;
68
- /** Size of the file in bytes */
69
- size;
70
- /** Additional headers associated with this file field */
71
- headers;
72
- /** Internal buffer storing file contents */
73
- _content;
74
- /** Current read position in the buffer */
75
- _position = 0;
76
- /**
77
- * Create a new UploadFile instance
78
- *
79
- * @param filename - Original filename from the client
80
- * @param content - File contents as Buffer
81
- * @param contentType - MIME type (defaults to "application/octet-stream")
82
- * @param size - File size in bytes (computed from content if not provided)
83
- * @param headers - Additional headers from the multipart field
84
- */
85
- constructor(filename, content, contentType = null, size = null, headers = null) {
86
- this.filename = filename;
87
- this.contentType = contentType ?? "application/octet-stream";
88
- this.size = size ?? content.length;
89
- this.headers = headers ?? {};
90
- this._content = content;
91
- }
92
- /**
93
- * Read file contents synchronously
94
- *
95
- * @param size - Number of bytes to read (-1 for all remaining)
96
- * @returns File contents as Buffer
97
- */
98
- read(size = -1) {
99
- if (size === -1) {
100
- const result2 = this._content.subarray(this._position);
101
- this._position = this._content.length;
102
- return result2;
103
- }
104
- const end = Math.min(this._position + size, this._content.length);
105
- const result = this._content.subarray(this._position, end);
106
- this._position = end;
107
- return result;
108
- }
109
- /**
110
- * Read file contents asynchronously
111
- *
112
- * Since the file is already in memory from Rust parsing, this is a simple wrapper.
113
- *
114
- * @param size - Number of bytes to read (-1 for all remaining)
115
- * @returns File contents as Buffer
116
- */
117
- async readAsync(size = -1) {
118
- return this.read(size);
119
- }
120
- /**
121
- * Read entire file as UTF-8 text
122
- *
123
- * @returns File contents as string
124
- */
125
- text() {
126
- return this._content.toString("utf-8");
127
- }
128
- /**
129
- * Read entire file as UTF-8 text asynchronously
130
- *
131
- * @returns File contents as string
132
- */
133
- async textAsync() {
134
- return this.text();
135
- }
136
- /**
137
- * Seek to a position in the file
138
- *
139
- * @param offset - Position to seek to
140
- * @param whence - How to interpret offset (0=absolute, 1=relative, 2=from end)
141
- * @returns New absolute position
142
- */
143
- seek(offset, whence = 0) {
144
- switch (whence) {
145
- case 0:
146
- this._position = Math.max(0, Math.min(offset, this._content.length));
147
- break;
148
- case 1:
149
- this._position = Math.max(0, Math.min(this._position + offset, this._content.length));
150
- break;
151
- case 2:
152
- this._position = Math.max(0, Math.min(this._content.length + offset, this._content.length));
153
- break;
154
- default:
155
- throw new Error(`Invalid whence value: ${whence}`);
156
- }
157
- return this._position;
158
- }
159
- /**
160
- * Seek to a position in the file asynchronously
161
- *
162
- * @param offset - Position to seek to
163
- * @param whence - How to interpret offset (0=absolute, 1=relative, 2=from end)
164
- * @returns New absolute position
165
- */
166
- async seekAsync(offset, whence = 0) {
167
- return this.seek(offset, whence);
168
- }
169
- /**
170
- * Get current position in the file
171
- *
172
- * @returns Current byte position
173
- */
174
- tell() {
175
- return this._position;
176
- }
177
- /**
178
- * Get the underlying Buffer
179
- *
180
- * @returns Complete file contents as Buffer
181
- */
182
- getBuffer() {
183
- return this._content;
184
- }
185
- /**
186
- * Close the file (no-op for in-memory files, provided for API compatibility)
187
- */
188
- close() {
189
- }
190
- /**
191
- * Close the file asynchronously (no-op, provided for API compatibility)
192
- */
193
- async closeAsync() {
194
- }
195
- /**
196
- * String representation of the upload file
197
- */
198
- toString() {
199
- return `UploadFile(filename=${JSON.stringify(this.filename)}, contentType=${JSON.stringify(this.contentType)}, size=${this.size})`;
200
- }
201
- /**
202
- * JSON representation for debugging
203
- */
204
- toJSON() {
205
- return {
206
- filename: this.filename,
207
- contentType: this.contentType,
208
- size: this.size,
209
- headers: this.headers
210
- };
211
- }
212
- };
213
-
214
- // src/converters.ts
215
- function isFileMetadata(value) {
216
- return typeof value === "object" && value !== null && "filename" in value && "content" in value;
217
- }
218
- function convertFileMetadataToUploadFile(fileData) {
219
- const { filename, content, size, content_type, content_encoding } = fileData;
220
- let buffer;
221
- if (content_encoding === "base64") {
222
- buffer = Buffer.from(content, "base64");
223
- } else {
224
- buffer = Buffer.from(content, "utf-8");
225
- }
226
- return new UploadFile(filename, buffer, content_type ?? null, size ?? null);
227
- }
228
- function processUploadFileFields(value) {
229
- if (value === null || value === void 0) {
230
- return value;
231
- }
232
- if (typeof value !== "object") {
233
- return value;
234
- }
235
- if (Array.isArray(value)) {
236
- return value.map((item) => {
237
- if (isFileMetadata(item)) {
238
- return convertFileMetadataToUploadFile(item);
239
- }
240
- return processUploadFileFields(item);
241
- });
242
- }
243
- if (isFileMetadata(value)) {
244
- return convertFileMetadataToUploadFile(value);
245
- }
246
- const result = {};
247
- for (const [key, val] of Object.entries(value)) {
248
- result[key] = processUploadFileFields(val);
249
- }
250
- return result;
251
- }
252
- function convertMultipartTestPayload(payload) {
253
- const result = {};
254
- if (payload.fields) {
255
- Object.assign(result, payload.fields);
256
- }
257
- if (payload.files && payload.files.length > 0) {
258
- const filesByName = {};
259
- for (const file of payload.files) {
260
- const fileMetadata = {
261
- filename: file.filename || file.name,
262
- content: file.content,
263
- content_type: file.contentType
264
- };
265
- const uploadFile = convertFileMetadataToUploadFile(fileMetadata);
266
- if (!filesByName[file.name]) {
267
- filesByName[file.name] = [];
268
- }
269
- const files = filesByName[file.name];
270
- if (files) {
271
- files.push(uploadFile);
272
- }
273
- }
274
- for (const [name, files] of Object.entries(filesByName)) {
275
- result[name] = files.length === 1 ? files[0] : files;
276
- }
277
- }
278
- return result;
279
- }
280
- function convertHandlerBody(body) {
281
- if (typeof body === "object" && body !== null && "__spikard_multipart__" in body && typeof body.__spikard_multipart__ === "object") {
282
- const multipart = body.__spikard_multipart__;
283
- return convertMultipartTestPayload(multipart);
284
- }
285
- return processUploadFileFields(body);
286
- }
287
-
288
- // src/request.ts
289
- var RequestImpl = class {
290
- method;
291
- path;
292
- params;
293
- pathParams;
294
- query;
295
- queryParams;
296
- headers;
297
- cookies;
298
- body;
299
- dependencies;
300
- #jsonCache;
301
- #formCache;
302
- constructor(data) {
303
- this.method = data.method;
304
- this.path = data.path;
305
- const pathParams = data.params ?? data.pathParams ?? {};
306
- const queryParams = data.query ?? extractQueryParams(data.queryParams) ?? {};
307
- this.params = pathParams;
308
- this.pathParams = pathParams;
309
- this.query = queryParams;
310
- this.queryParams = queryParams;
311
- this.headers = normalizeHeaders(data.headers ?? {});
312
- this.cookies = data.cookies ?? {};
313
- this.body = convertBodyToBuffer(data.body);
314
- this.dependencies = data.dependencies;
315
- }
316
- json() {
317
- if (this.#jsonCache !== void 0) {
318
- return this.#jsonCache;
319
- }
320
- if (!this.body || this.body.length === 0) {
321
- throw new Error("No body available to parse as JSON");
322
- }
323
- const raw = this.body.toString("utf-8");
324
- const parsed = JSON.parse(raw);
325
- const converted = convertHandlerBody(parsed);
326
- this.#jsonCache = converted;
327
- return converted;
328
- }
329
- form() {
330
- if (this.#formCache !== void 0) {
331
- return this.#formCache;
332
- }
333
- if (!this.body || this.body.length === 0) {
334
- throw new Error("No body available to parse as form data");
335
- }
336
- const text = this.body.toString("utf-8");
337
- const params = new URLSearchParams(text);
338
- const form = {};
339
- for (const [key, value] of params.entries()) {
340
- form[key] = value;
341
- }
342
- this.#formCache = form;
343
- return form;
344
- }
345
- };
346
- var normalizeHeaders = (headers) => Object.fromEntries(Object.entries(headers).map(([key, value]) => [key.toLowerCase(), value]));
347
- var extractQueryParams = (queryParams) => {
348
- if (!queryParams || typeof queryParams !== "object") {
349
- return void 0;
350
- }
351
- const result = {};
352
- for (const [key, value] of Object.entries(queryParams)) {
353
- result[key] = String(value);
354
- }
355
- return result;
356
- };
357
- var convertBodyToBuffer = (body) => {
358
- if (body === null || body === void 0) {
359
- return null;
360
- }
361
- if (Array.isArray(body)) {
362
- if (body.length === 0 || body.every((b) => typeof b === "number")) {
363
- return Buffer.from(body);
364
- }
365
- return Buffer.from(JSON.stringify(body), "utf-8");
366
- }
367
- if (typeof body === "object") {
368
- return Buffer.from(JSON.stringify(body), "utf-8");
369
- }
370
- if (typeof body === "string") {
371
- return Buffer.from(body, "utf-8");
372
- }
373
- return null;
374
- };
375
- function createRequest(data) {
376
- return new RequestImpl(data);
377
- }
378
-
379
- // src/streaming.ts
380
- var import_node_module = require("module");
381
- var STREAM_HANDLE_PROP = "__spikard_stream_handle";
382
- var nativeBinding = null;
383
- var loadBinding = () => {
384
- try {
385
- const require2 = (0, import_node_module.createRequire)(importMetaUrl);
386
- return require2("../index.js");
387
- } catch {
388
- console.warn("[spikard-node] Native binding not found. Please run: pnpm build:native");
389
- return null;
390
- }
391
- };
392
- nativeBinding = loadBinding();
393
- var createHandle = (iterator, init) => {
394
- if (nativeBinding && typeof nativeBinding.createStreamingHandle === "function") {
395
- return { kind: "native", handle: nativeBinding.createStreamingHandle(iterator, init), init };
396
- }
397
- return { kind: "js", iterator, init };
398
- };
399
- var StreamingResponse = class {
400
- [STREAM_HANDLE_PROP];
401
- constructor(stream, init) {
402
- const iterator = toAsyncIterator(stream);
403
- this[STREAM_HANDLE_PROP] = createHandle(iterator, init ?? {});
404
- }
405
- };
406
- function isStreamingResponse(value) {
407
- return Boolean(value) && value instanceof StreamingResponse;
408
- }
409
- var getStreamingHandle = (response) => response[STREAM_HANDLE_PROP];
410
- function toAsyncIterator(source) {
411
- if (source && typeof source.next === "function") {
412
- const iterator = source;
413
- if (typeof iterator[Symbol.asyncIterator] === "function") {
414
- return iterator;
415
- }
416
- return {
417
- next: (...args) => iterator.next(...args),
418
- [Symbol.asyncIterator]() {
419
- return this;
420
- }
421
- };
422
- }
423
- if (source && typeof source[Symbol.asyncIterator] === "function") {
424
- return source[Symbol.asyncIterator]();
425
- }
426
- throw new TypeError("StreamingResponse requires an async iterator or generator");
427
- }
428
-
429
- // src/handler-wrapper.ts
430
- var NATIVE_HANDLER_FLAG = /* @__PURE__ */ Symbol("spikard.nativeHandler");
431
- var formatHandlerResult = (result, objectMode) => {
432
- if (objectMode) {
433
- if (result !== void 0 && result !== null && typeof result === "object") {
434
- if ("status" in result) {
435
- return result;
436
- }
437
- if (isStreamingResponse(result)) {
438
- return result;
439
- }
440
- return { status: 200, body: result };
441
- }
442
- return result === void 0 ? { status: 200, body: null } : { status: 200, body: result };
443
- }
444
- if (isStreamingResponse(result)) {
445
- return result;
446
- }
447
- if (result === void 0) {
448
- return "null";
449
- }
450
- if (typeof result === "string") {
451
- return result;
452
- }
453
- return JSON.stringify(result);
454
- };
455
- var isNativeHandler = (handler) => Boolean(handler?.[NATIVE_HANDLER_FLAG]);
456
- function markNative(handler) {
457
- handler[NATIVE_HANDLER_FLAG] = true;
458
- return handler;
459
- }
460
- function markRawBody(handler, prefersRaw) {
461
- handler.__spikard_raw_body = prefersRaw;
462
- return handler;
463
- }
464
- function prepareRequest(requestInput) {
465
- let actualInput = requestInput;
466
- if (Array.isArray(requestInput) && requestInput.length === 1) {
467
- actualInput = requestInput[0];
468
- }
469
- const objectMode = typeof actualInput === "object" && actualInput !== null;
470
- const data = objectMode ? actualInput : JSON.parse(actualInput);
471
- const request = createRequest(data);
472
- return { request, objectMode };
473
- }
474
- function wrapHandler(handler) {
475
- const isHandlerAsync = handler.constructor.name === "AsyncFunction";
476
- if (isHandlerAsync) {
477
- const nativeHandler2 = async (requestInput) => {
478
- const { request, objectMode } = prepareRequest(requestInput);
479
- const result = await handler(request);
480
- return formatHandlerResult(result, objectMode);
481
- };
482
- return markNative(markRawBody(nativeHandler2, true));
483
- }
484
- const nativeHandler = (requestInput) => {
485
- const { request, objectMode } = prepareRequest(requestInput);
486
- const result = handler(request);
487
- return formatHandlerResult(result, objectMode);
488
- };
489
- return markNative(markRawBody(nativeHandler, true));
490
- }
491
- function wrapBodyHandler(handler) {
492
- const isHandlerAsync = handler.constructor.name === "AsyncFunction";
493
- if (isHandlerAsync) {
494
- const nativeHandler2 = async (requestInput) => {
495
- const { request, objectMode } = prepareRequest(requestInput);
496
- const body = request.json();
497
- const result = await handler(body, request);
498
- return formatHandlerResult(result, objectMode);
499
- };
500
- return markNative(markRawBody(nativeHandler2, true));
501
- }
502
- const nativeHandler = (requestInput) => {
503
- const { request, objectMode } = prepareRequest(requestInput);
504
- const body = request.json();
505
- const result = handler(body, request);
506
- return formatHandlerResult(result, objectMode);
507
- };
508
- return markNative(markRawBody(nativeHandler, true));
509
- }
510
-
511
- // src/server.ts
512
- var nativeBinding2;
513
- function loadBinding2() {
514
- try {
515
- const require2 = (0, import_node_module2.createRequire)(importMetaUrl);
516
- return require2("../index.js");
517
- } catch {
518
- console.warn("[spikard-node] Native binding not found. Please run: pnpm build:native");
519
- return {
520
- runServer: () => {
521
- throw new Error("Native binding not built. Run: pnpm build:native");
522
- }
523
- };
524
- }
525
- }
526
- nativeBinding2 = loadBinding2();
527
- function runServer(app, config = {}) {
528
- const handlers = {};
529
- const routes = (app.routes || []).map((route2) => {
530
- const handler = app.handlers?.[route2.handler_name];
531
- if (!handler) return route2;
532
- const nativeHandler = isNativeHandler(handler) ? handler : wrapHandler(handler);
533
- handlers[route2.handler_name] = nativeHandler;
534
- const isAsync = nativeHandler.constructor.name === "AsyncFunction";
535
- return { ...route2, is_async: isAsync };
536
- });
537
- nativeBinding2.runServer({ ...app, handlers, routes }, config);
538
- }
539
-
540
- // src/app.ts
541
- var ASYNC_GENERATOR_REGISTRY = /* @__PURE__ */ new Map();
542
- var ASYNC_GENERATOR_COUNTER = 0;
543
- var isAsyncGenerator = (value) => {
544
- return typeof value === "object" && value !== null && Symbol.asyncIterator in value;
545
- };
546
- var normalizeDependencyKey = (key) => key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/-/g, "_").toLowerCase();
547
- var inferDependsOn = (factory) => {
548
- const source = factory.toString();
549
- const parenMatch = source.match(/^[^(]*\(([^)]*)\)/);
550
- if (parenMatch) {
551
- const rawParams = (parenMatch[1] ?? "").split(",").map((param) => param.trim()).filter((param) => param.length > 0);
552
- return rawParams.filter((param) => !param.startsWith("{") && !param.startsWith("[")).map((param) => param.replace(/^\.{3}/, "")).map((param) => param.split("=")[0]?.trim() ?? "").filter((param) => param.length > 0).map((param) => normalizeDependencyKey(param));
553
- }
554
- const arrowMatch = source.match(/^\s*([A-Za-z0-9_$]+)\s*=>/);
555
- if (arrowMatch) {
556
- return [normalizeDependencyKey(arrowMatch[1] ?? "")].filter((param) => param.length > 0);
557
- }
558
- return [];
559
- };
560
- var parseDependencies = (payload) => {
561
- if (typeof payload === "string") {
562
- try {
563
- const parsed = JSON.parse(payload);
564
- return parsed ?? {};
565
- } catch {
566
- return {};
567
- }
568
- }
569
- if (typeof payload === "object" && payload !== null) {
570
- return payload;
571
- }
572
- return {};
573
- };
574
- var wrapDependencyFactory = (factory) => {
575
- return async (payload) => {
576
- const deps = parseDependencies(payload);
577
- const cleanupId = deps.__cleanup_id__;
578
- if (typeof cleanupId === "string") {
579
- const generator = ASYNC_GENERATOR_REGISTRY.get(cleanupId);
580
- if (generator) {
581
- ASYNC_GENERATOR_REGISTRY.delete(cleanupId);
582
- if (typeof generator.return === "function") {
583
- await generator.return(void 0);
584
- }
585
- }
586
- return JSON.stringify({ ok: true });
587
- }
588
- const result = await factory(deps);
589
- if (isAsyncGenerator(result)) {
590
- const { value } = await result.next();
591
- const id = `gen_${ASYNC_GENERATOR_COUNTER++}`;
592
- ASYNC_GENERATOR_REGISTRY.set(id, result);
593
- return JSON.stringify({ __async_generator__: true, value, cleanup_id: id });
594
- }
595
- return result === void 0 ? "null" : JSON.stringify(result);
596
- };
597
- };
598
- var Spikard = class {
599
- routes = [];
600
- handlers = {};
601
- websocketRoutes = [];
602
- websocketHandlers = {};
603
- grpcMethods = [];
604
- grpcHandlers = {};
605
- lifecycleHooks = {
606
- onRequest: [],
607
- preValidation: [],
608
- preHandler: [],
609
- onResponse: [],
610
- onError: []
611
- };
612
- dependencies = {};
613
- /**
614
- * Add a route to the application
615
- *
616
- * @param metadata - Route configuration metadata
617
- * @param handler - Handler function (sync or async)
618
- */
619
- addRoute(metadata, handler) {
620
- this.routes.push(metadata);
621
- this.handlers[metadata.handler_name] = handler;
622
- }
623
- /**
624
- * Register a WebSocket route (message-based)
625
- */
626
- websocket(path2, handler, options = {}) {
627
- const handlerName = options.handlerName ?? `ws_${this.websocketRoutes.length}_${path2}`.replace(/[^a-zA-Z0-9_]/g, "_");
628
- const handlerWrapper = { handleMessage: handler };
629
- if (options.messageSchema) {
630
- handlerWrapper._messageSchema = options.messageSchema;
631
- }
632
- if (options.responseSchema) {
633
- handlerWrapper._responseSchema = options.responseSchema;
634
- }
635
- const route2 = {
636
- method: "GET",
637
- path: path2,
638
- handler_name: handlerName,
639
- request_schema: options.messageSchema,
640
- response_schema: options.responseSchema,
641
- parameter_schema: void 0,
642
- file_params: void 0,
643
- is_async: true
644
- };
645
- this.websocketRoutes.push(route2);
646
- this.websocketHandlers[handlerName] = handlerWrapper;
647
- }
648
- /**
649
- * Register a unary gRPC method on the application.
650
- *
651
- * @param serviceName - Fully-qualified service name
652
- * @param methodName - gRPC method name
653
- * @param handler - gRPC handler implementation
654
- * @returns The application for chaining
655
- */
656
- addGrpcUnary(serviceName, methodName, handler) {
657
- if (typeof handler?.handleRequest !== "function") {
658
- throw new TypeError("Unary handler must implement handleRequest(request)");
659
- }
660
- return this.registerGrpcMethod(serviceName, methodName, "unary", {
661
- handleRequest: (request) => handler.handleRequest(request)
662
- });
663
- }
664
- addGrpcServerStreaming(serviceName, methodName, handler) {
665
- if (typeof handler?.handleServerStream !== "function") {
666
- throw new TypeError("Server-streaming handler must implement handleServerStream(request)");
667
- }
668
- return this.registerGrpcMethod(serviceName, methodName, "serverStreaming", {
669
- handleServerStream: (request) => handler.handleServerStream(request)
670
- });
671
- }
672
- addGrpcClientStreaming(serviceName, methodName, handler) {
673
- if (typeof handler?.handleClientStream !== "function") {
674
- throw new TypeError("Client-streaming handler must implement handleClientStream(request)");
675
- }
676
- return this.registerGrpcMethod(serviceName, methodName, "clientStreaming", {
677
- handleClientStream: (request) => handler.handleClientStream(request)
678
- });
679
- }
680
- addGrpcBidirectionalStreaming(serviceName, methodName, handler) {
681
- if (typeof handler?.handleBidiStream !== "function") {
682
- throw new TypeError("Bidirectional-streaming handler must implement handleBidiStream(request)");
683
- }
684
- return this.registerGrpcMethod(serviceName, methodName, "bidirectionalStreaming", {
685
- handleBidiStream: (request) => handler.handleBidiStream(request)
686
- });
687
- }
688
- registerGrpcMethod(serviceName, methodName, rpcMode, handlerWrapper) {
689
- if (!serviceName) {
690
- throw new Error("Service name cannot be empty");
691
- }
692
- if (!methodName) {
693
- throw new Error("Method name cannot be empty");
694
- }
695
- const previous = this.grpcMethods.find(
696
- (entry) => entry.serviceName === serviceName && entry.methodName === methodName
697
- );
698
- if (previous) {
699
- delete this.grpcHandlers[previous.handlerName];
700
- }
701
- const handlerName = `grpc_${this.grpcMethods.length}_${serviceName}_${methodName}`.replace(/[^a-zA-Z0-9_]/g, "_");
702
- this.grpcHandlers[handlerName] = handlerWrapper;
703
- this.grpcMethods = this.grpcMethods.filter(
704
- (entry) => !(entry.serviceName === serviceName && entry.methodName === methodName)
705
- );
706
- this.grpcMethods.push({ serviceName, methodName, rpcMode, handlerName });
707
- return this;
708
- }
709
- /**
710
- * Mount all handlers from a gRPC service registry on the application.
711
- *
712
- * @param service - Registry containing one or more service methods
713
- * @returns The application for chaining
714
- */
715
- useGrpc(service) {
716
- for (const method of service.entries()) {
717
- switch (method.rpcMode) {
718
- case "unary":
719
- this.addGrpcUnary(method.serviceName, method.methodName, method.handler);
720
- break;
721
- case "serverStreaming":
722
- this.addGrpcServerStreaming(
723
- method.serviceName,
724
- method.methodName,
725
- method.handler
726
- );
727
- break;
728
- case "clientStreaming":
729
- this.addGrpcClientStreaming(
730
- method.serviceName,
731
- method.methodName,
732
- method.handler
733
- );
734
- break;
735
- case "bidirectionalStreaming":
736
- this.addGrpcBidirectionalStreaming(
737
- method.serviceName,
738
- method.methodName,
739
- method.handler
740
- );
741
- break;
742
- }
743
- }
744
- return this;
745
- }
746
- /**
747
- * Run the server
748
- *
749
- * @param config - Server configuration
750
- */
751
- run(config = {}) {
752
- runServer(this, config);
753
- }
754
- /**
755
- * Register an onRequest lifecycle hook
756
- *
757
- * Runs before routing. Can inspect/modify the request or short-circuit with a response.
758
- *
759
- * @param hook - Async function that receives a request and returns either:
760
- * - The (possibly modified) request to continue processing
761
- * - A Response object to short-circuit the request pipeline
762
- * @returns The hook function (for decorator usage)
763
- *
764
- * @example
765
- * ```typescript
766
- * app.onRequest(async (request) => {
767
- * console.log(`Request: ${request.method} ${request.path}`);
768
- * return request;
769
- * });
770
- * ```
771
- */
772
- onRequest(hook) {
773
- this.lifecycleHooks.onRequest.push(hook);
774
- return hook;
775
- }
776
- /**
777
- * Register a preValidation lifecycle hook
778
- *
779
- * Runs after routing but before validation. Useful for rate limiting.
780
- *
781
- * @param hook - Async function that receives a request and returns either:
782
- * - The (possibly modified) request to continue processing
783
- * - A Response object to short-circuit the request pipeline
784
- * @returns The hook function (for decorator usage)
785
- *
786
- * @example
787
- * ```typescript
788
- * app.preValidation(async (request) => {
789
- * if (tooManyRequests()) {
790
- * return { error: "Rate limit exceeded", status: 429 };
791
- * }
792
- * return request;
793
- * });
794
- * ```
795
- */
796
- preValidation(hook) {
797
- this.lifecycleHooks.preValidation.push(hook);
798
- return hook;
799
- }
800
- /**
801
- * Register a preHandler lifecycle hook
802
- *
803
- * Runs after validation but before the handler. Ideal for authentication/authorization.
804
- *
805
- * @param hook - Async function that receives a request and returns either:
806
- * - The (possibly modified) request to continue processing
807
- * - A Response object to short-circuit the request pipeline
808
- * @returns The hook function (for decorator usage)
809
- *
810
- * @example
811
- * ```typescript
812
- * app.preHandler(async (request) => {
813
- * if (!validToken(request.headers.authorization)) {
814
- * return { error: "Unauthorized", status: 401 };
815
- * }
816
- * return request;
817
- * });
818
- * ```
819
- */
820
- preHandler(hook) {
821
- this.lifecycleHooks.preHandler.push(hook);
822
- return hook;
823
- }
824
- /**
825
- * Register an onResponse lifecycle hook
826
- *
827
- * Runs after the handler executes. Can modify the response.
828
- *
829
- * @param hook - Async function that receives a response and returns the (possibly modified) response
830
- * @returns The hook function (for decorator usage)
831
- *
832
- * @example
833
- * ```typescript
834
- * app.onResponse(async (response) => {
835
- * response.headers["X-Frame-Options"] = "DENY";
836
- * return response;
837
- * });
838
- * ```
839
- */
840
- onResponse(hook) {
841
- this.lifecycleHooks.onResponse.push(hook);
842
- return hook;
843
- }
844
- /**
845
- * Register an onError lifecycle hook
846
- *
847
- * Runs when an error occurs. Can customize error responses.
848
- *
849
- * @param hook - Async function that receives an error response and returns a (possibly modified) response
850
- * @returns The hook function (for decorator usage)
851
- *
852
- * @example
853
- * ```typescript
854
- * app.onError(async (response) => {
855
- * response.headers["Content-Type"] = "application/json";
856
- * return response;
857
- * });
858
- * ```
859
- */
860
- onError(hook) {
861
- this.lifecycleHooks.onError.push(hook);
862
- return hook;
863
- }
864
- /**
865
- * Register a dependency value or factory
866
- *
867
- * Provides a value or factory function to be injected into handlers.
868
- * Dependencies are matched by parameter name or can be accessed via the
869
- * request context.
870
- *
871
- * @param key - Unique identifier for the dependency
872
- * @param valueOrFactory - Static value or factory function
873
- * @param options - Configuration options for the dependency
874
- * @returns The Spikard instance for method chaining
875
- *
876
- * @example
877
- * ```typescript
878
- * // Simple value dependency
879
- * app.provide('app_name', 'MyApp');
880
- * app.provide('max_connections', 100);
881
- *
882
- * // Factory dependency
883
- * app.provide('db_pool', async ({ database_url }) => {
884
- * return await createPool(database_url);
885
- * }, { dependsOn: ['database_url'], singleton: true });
886
- *
887
- * // Use in handler
888
- * app.get('/config', async (request, { app_name, db_pool }) => {
889
- * return { app: app_name, pool: db_pool };
890
- * });
891
- * ```
892
- */
893
- provide(key, valueOrFactory, options) {
894
- const normalizedKey = normalizeDependencyKey(key);
895
- const isFactory = typeof valueOrFactory === "function";
896
- const factory = isFactory ? wrapDependencyFactory(valueOrFactory) : void 0;
897
- const explicitDependsOn = options?.dependsOn;
898
- const inferredDependsOn = isFactory && explicitDependsOn === void 0 ? inferDependsOn(valueOrFactory) : [];
899
- const dependsOn = (explicitDependsOn ?? inferredDependsOn).map((depKey) => normalizeDependencyKey(depKey));
900
- this.dependencies[normalizedKey] = {
901
- isFactory,
902
- value: isFactory ? void 0 : valueOrFactory,
903
- factory,
904
- dependsOn,
905
- singleton: options?.singleton ?? false,
906
- cacheable: options?.cacheable ?? !isFactory
907
- };
908
- return this;
909
- }
910
- /**
911
- * Get all registered lifecycle hooks
912
- *
913
- * @returns Dictionary of hook lists by type
914
- */
915
- getLifecycleHooks() {
916
- return {
917
- onRequest: [...this.lifecycleHooks.onRequest ?? []],
918
- preValidation: [...this.lifecycleHooks.preValidation ?? []],
919
- preHandler: [...this.lifecycleHooks.preHandler ?? []],
920
- onResponse: [...this.lifecycleHooks.onResponse ?? []],
921
- onError: [...this.lifecycleHooks.onError ?? []]
922
- };
923
- }
924
- };
925
-
926
- // src/background.ts
927
- var background_exports = {};
928
- __export(background_exports, {
929
- run: () => run
930
- });
931
- var import__ = require("../index.js");
932
- function run(work) {
933
- const task = async () => {
934
- await work();
935
- return void 0;
936
- };
937
- (0, import__.backgroundRun)(task);
938
- }
939
-
940
- // src/grpc.ts
941
- var GrpcStatusCode = /* @__PURE__ */ ((GrpcStatusCode2) => {
942
- GrpcStatusCode2[GrpcStatusCode2["OK"] = 0] = "OK";
943
- GrpcStatusCode2[GrpcStatusCode2["CANCELLED"] = 1] = "CANCELLED";
944
- GrpcStatusCode2[GrpcStatusCode2["UNKNOWN"] = 2] = "UNKNOWN";
945
- GrpcStatusCode2[GrpcStatusCode2["INVALID_ARGUMENT"] = 3] = "INVALID_ARGUMENT";
946
- GrpcStatusCode2[GrpcStatusCode2["DEADLINE_EXCEEDED"] = 4] = "DEADLINE_EXCEEDED";
947
- GrpcStatusCode2[GrpcStatusCode2["NOT_FOUND"] = 5] = "NOT_FOUND";
948
- GrpcStatusCode2[GrpcStatusCode2["ALREADY_EXISTS"] = 6] = "ALREADY_EXISTS";
949
- GrpcStatusCode2[GrpcStatusCode2["PERMISSION_DENIED"] = 7] = "PERMISSION_DENIED";
950
- GrpcStatusCode2[GrpcStatusCode2["RESOURCE_EXHAUSTED"] = 8] = "RESOURCE_EXHAUSTED";
951
- GrpcStatusCode2[GrpcStatusCode2["FAILED_PRECONDITION"] = 9] = "FAILED_PRECONDITION";
952
- GrpcStatusCode2[GrpcStatusCode2["ABORTED"] = 10] = "ABORTED";
953
- GrpcStatusCode2[GrpcStatusCode2["OUT_OF_RANGE"] = 11] = "OUT_OF_RANGE";
954
- GrpcStatusCode2[GrpcStatusCode2["UNIMPLEMENTED"] = 12] = "UNIMPLEMENTED";
955
- GrpcStatusCode2[GrpcStatusCode2["INTERNAL"] = 13] = "INTERNAL";
956
- GrpcStatusCode2[GrpcStatusCode2["UNAVAILABLE"] = 14] = "UNAVAILABLE";
957
- GrpcStatusCode2[GrpcStatusCode2["DATA_LOSS"] = 15] = "DATA_LOSS";
958
- GrpcStatusCode2[GrpcStatusCode2["UNAUTHENTICATED"] = 16] = "UNAUTHENTICATED";
959
- return GrpcStatusCode2;
960
- })(GrpcStatusCode || {});
961
- var GrpcError = class extends Error {
962
- /**
963
- * gRPC status code
964
- */
965
- code;
966
- /**
967
- * Create a new gRPC error
968
- *
969
- * @param code - gRPC status code
970
- * @param message - Error message
971
- */
972
- constructor(code, message) {
973
- super(message);
974
- this.code = code;
975
- this.name = "GrpcError";
976
- }
977
- };
978
- var GrpcService = class {
979
- methods = /* @__PURE__ */ new Map();
980
- methodKey(serviceName, methodName) {
981
- return `${serviceName}/${methodName}`;
982
- }
983
- registerMethod(config) {
984
- if (!config.serviceName) {
985
- throw new Error("Service name cannot be empty");
986
- }
987
- if (!config.methodName) {
988
- throw new Error("Method name cannot be empty");
989
- }
990
- switch (config.rpcMode) {
991
- case "unary":
992
- if (typeof config.handler?.handleRequest !== "function") {
993
- throw new TypeError("Unary handler must implement handleRequest(request)");
994
- }
995
- break;
996
- case "serverStreaming":
997
- if (typeof config.handler?.handleServerStream !== "function") {
998
- throw new TypeError("Server-streaming handler must implement handleServerStream(request)");
999
- }
1000
- break;
1001
- case "clientStreaming":
1002
- if (typeof config.handler?.handleClientStream !== "function") {
1003
- throw new TypeError("Client-streaming handler must implement handleClientStream(request)");
1004
- }
1005
- break;
1006
- case "bidirectionalStreaming":
1007
- if (typeof config.handler?.handleBidiStream !== "function") {
1008
- throw new TypeError("Bidirectional-streaming handler must implement handleBidiStream(request)");
1009
- }
1010
- break;
1011
- }
1012
- this.methods.set(this.methodKey(config.serviceName, config.methodName), config);
1013
- return this;
1014
- }
1015
- /**
1016
- * Register a unary handler for a fully-qualified service method.
1017
- *
1018
- * @param serviceName - Service name such as `mypackage.UserService`
1019
- * @param methodName - Method name such as `GetUser`
1020
- * @param handler - Handler implementation for that method
1021
- * @returns The registry for chaining
1022
- */
1023
- registerUnary(serviceName, methodName, handler) {
1024
- return this.registerMethod({ serviceName, methodName, rpcMode: "unary", handler });
1025
- }
1026
- registerServerStreaming(serviceName, methodName, handler) {
1027
- return this.registerMethod({ serviceName, methodName, rpcMode: "serverStreaming", handler });
1028
- }
1029
- registerClientStreaming(serviceName, methodName, handler) {
1030
- return this.registerMethod({ serviceName, methodName, rpcMode: "clientStreaming", handler });
1031
- }
1032
- registerBidirectionalStreaming(serviceName, methodName, handler) {
1033
- return this.registerMethod({ serviceName, methodName, rpcMode: "bidirectionalStreaming", handler });
1034
- }
1035
- /**
1036
- * Remove a handler from the registry.
1037
- *
1038
- * @param serviceName - Fully-qualified service name
1039
- * @param methodName - Method name
1040
- */
1041
- unregister(serviceName, methodName) {
1042
- if (!this.methods.delete(this.methodKey(serviceName, methodName))) {
1043
- throw new Error(`No handler registered for method: ${serviceName}/${methodName}`);
1044
- }
1045
- }
1046
- /**
1047
- * Get the registration for a service method.
1048
- *
1049
- * @param serviceName - Fully-qualified service name
1050
- * @param methodName - Method name
1051
- * @returns The registered method configuration, if present
1052
- */
1053
- getMethod(serviceName, methodName) {
1054
- return this.methods.get(this.methodKey(serviceName, methodName));
1055
- }
1056
- /**
1057
- * List all registered service names.
1058
- *
1059
- * @returns Fully-qualified service names
1060
- */
1061
- serviceNames() {
1062
- return Array.from(new Set(Array.from(this.methods.values(), (entry) => entry.serviceName)));
1063
- }
1064
- methodNames(serviceName) {
1065
- return Array.from(this.methods.values()).filter((entry) => entry.serviceName === serviceName).map((entry) => entry.methodName);
1066
- }
1067
- /**
1068
- * Check whether a specific service method is registered.
1069
- *
1070
- * @param serviceName - Fully-qualified service name
1071
- * @param methodName - Method name
1072
- * @returns True when a handler is registered for the method
1073
- */
1074
- hasMethod(serviceName, methodName) {
1075
- return this.methods.has(this.methodKey(serviceName, methodName));
1076
- }
1077
- /**
1078
- * Return registered method entries.
1079
- */
1080
- entries() {
1081
- return Array.from(this.methods.values());
1082
- }
1083
- /**
1084
- * Route a unary request to the registered method handler.
1085
- *
1086
- * @param request - Incoming gRPC request
1087
- * @returns Promise resolving to the handler response
1088
- * @throws GrpcError when no service is registered
1089
- */
1090
- async handleRequest(request) {
1091
- const method = this.getMethod(request.serviceName, request.methodName);
1092
- if (!method) {
1093
- throw new GrpcError(
1094
- 12 /* UNIMPLEMENTED */,
1095
- `No handler registered for method: ${request.serviceName}/${request.methodName}`
1096
- );
1097
- }
1098
- if (method.rpcMode !== "unary") {
1099
- throw new GrpcError(
1100
- 12 /* UNIMPLEMENTED */,
1101
- `Method ${request.serviceName}/${request.methodName} is registered as ${method.rpcMode}`
1102
- );
1103
- }
1104
- return method.handler.handleRequest(request);
1105
- }
1106
- };
1107
- function createUnaryHandler(methodName, handler, requestType, responseType) {
1108
- return {
1109
- async handleRequest(request) {
1110
- if (request.methodName !== methodName) {
1111
- throw new GrpcError(12 /* UNIMPLEMENTED */, `Method ${request.methodName} not implemented`);
1112
- }
1113
- const req = requestType.decode(request.payload);
1114
- const result = await handler(req, request.metadata);
1115
- let response;
1116
- let responseMetadata;
1117
- if (result && typeof result === "object" && "response" in result) {
1118
- response = result.response;
1119
- responseMetadata = result.metadata;
1120
- } else {
1121
- response = result;
1122
- }
1123
- const encoded = responseType.encode(response).finish();
1124
- if (responseMetadata) {
1125
- return {
1126
- payload: Buffer.from(encoded),
1127
- metadata: responseMetadata
1128
- };
1129
- }
1130
- return {
1131
- payload: Buffer.from(encoded)
1132
- };
1133
- }
1134
- };
1135
- }
1136
- function createServiceHandler(methods) {
1137
- return {
1138
- async handleRequest(request) {
1139
- const handler = methods[request.methodName];
1140
- if (!handler) {
1141
- throw new GrpcError(12 /* UNIMPLEMENTED */, `Method ${request.methodName} not implemented`);
1142
- }
1143
- return handler.handleRequest(request);
1144
- }
1145
- };
1146
- }
1147
-
1148
- // src/routing.ts
1149
- function route(path2, options = {}) {
1150
- return (handler) => {
1151
- const methods = options.methods ? Array.isArray(options.methods) ? options.methods : [options.methods] : ["GET"];
1152
- const metadata = {
1153
- method: methods.join(","),
1154
- path: path2,
1155
- handler_name: handler.name || "anonymous",
1156
- request_schema: options.bodySchema ?? void 0,
1157
- response_schema: options.responseSchema ?? void 0,
1158
- parameter_schema: options.parameterSchema ?? void 0,
1159
- cors: options.cors ?? void 0,
1160
- is_async: true
1161
- };
1162
- handler.__route_metadata__ = metadata;
1163
- return handler;
1164
- };
1165
- }
1166
- function get(path2, options = {}) {
1167
- return route(path2, { ...options, methods: ["GET"] });
1168
- }
1169
- function post(path2, options = {}) {
1170
- return route(path2, { ...options, methods: ["POST"] });
1171
- }
1172
- function put(path2, options = {}) {
1173
- return route(path2, { ...options, methods: ["PUT"] });
1174
- }
1175
- function del(path2, options = {}) {
1176
- return route(path2, { ...options, methods: ["DELETE"] });
1177
- }
1178
- function patch(path2, options = {}) {
1179
- return route(path2, { ...options, methods: ["PATCH"] });
1180
- }
1181
-
1182
- // src/testing.ts
1183
- var import_promises = __toESM(require("fs/promises"));
1184
- var import_node_module3 = require("module");
1185
- var import_node_path = __toESM(require("path"));
1186
- var import_node_zlib = require("zlib");
1187
- var GRAPHQL_WS_TIMEOUT_MS = 2e3;
1188
- var GRAPHQL_WS_MAX_CONTROL_MESSAGES = 32;
1189
- var withTimeout = async (promise, timeoutMs, context) => {
1190
- let timer;
1191
- try {
1192
- return await Promise.race([
1193
- promise,
1194
- new Promise((_resolve, reject) => {
1195
- timer = setTimeout(() => reject(new Error(`Timed out waiting for ${context}`)), timeoutMs);
1196
- })
1197
- ]);
1198
- } finally {
1199
- if (timer) {
1200
- clearTimeout(timer);
1201
- }
1202
- }
1203
- };
1204
- var decodeGraphqlWsMessage = (value) => {
1205
- if (typeof value === "string") {
1206
- const parsed = JSON.parse(value);
1207
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
1208
- return parsed;
1209
- }
1210
- throw new Error("Expected GraphQL WebSocket JSON object message");
1211
- }
1212
- if (value && typeof value === "object" && !Array.isArray(value)) {
1213
- return value;
1214
- }
1215
- throw new Error("Expected GraphQL WebSocket message object");
1216
- };
1217
- var MockWebSocketConnection = class {
1218
- handler;
1219
- queue = [];
1220
- constructor(handler) {
1221
- this.handler = handler;
1222
- }
1223
- async send_json(message) {
1224
- const response = await this.handler(message);
1225
- this.queue.push(response);
1226
- }
1227
- async sendJson(message) {
1228
- return this.send_json(message);
1229
- }
1230
- async sendText(text) {
1231
- return this.send_json(text);
1232
- }
1233
- async receive_json() {
1234
- return this.queue.shift();
1235
- }
1236
- async receiveJson() {
1237
- return this.receive_json();
1238
- }
1239
- async receiveText() {
1240
- const value = await this.receive_json();
1241
- return typeof value === "string" ? value : JSON.stringify(value);
1242
- }
1243
- async receiveBytes() {
1244
- const value = await this.receive_json();
1245
- if (Buffer.isBuffer(value)) {
1246
- return value;
1247
- }
1248
- if (typeof value === "string") {
1249
- return Buffer.from(value);
1250
- }
1251
- return Buffer.from(JSON.stringify(value));
1252
- }
1253
- async receiveMessage() {
1254
- return this.receive_json();
1255
- }
1256
- async close() {
1257
- }
1258
- };
1259
- var nativeTestClient = null;
1260
- var loadNativeTestClient = () => {
1261
- try {
1262
- const require2 = (0, import_node_module3.createRequire)(importMetaUrl);
1263
- const binding = require2("../index.js");
1264
- return binding.TestClient;
1265
- } catch {
1266
- return null;
1267
- }
1268
- };
1269
- nativeTestClient = loadNativeTestClient();
1270
- var isNativeCtor = nativeTestClient !== null;
1271
- var JsTestResponse = class {
1272
- constructor(statusCode, headers, body) {
1273
- this.statusCode = statusCode;
1274
- this.headerBag = headers;
1275
- this.body = body;
1276
- }
1277
- body;
1278
- headerBag;
1279
- headers() {
1280
- return this.headerBag;
1281
- }
1282
- text() {
1283
- return this.decodeBody().toString("utf-8");
1284
- }
1285
- json() {
1286
- const raw = this.text();
1287
- if (raw.length === 0) {
1288
- return void 0;
1289
- }
1290
- try {
1291
- return JSON.parse(raw);
1292
- } catch {
1293
- return raw;
1294
- }
1295
- }
1296
- bytes() {
1297
- return this.decodeBody();
1298
- }
1299
- decodeBody() {
1300
- const encoding = (this.headerBag["content-encoding"] ?? "").toLowerCase();
1301
- if (encoding === "gzip") {
1302
- try {
1303
- return (0, import_node_zlib.gunzipSync)(this.body);
1304
- } catch {
1305
- return this.body;
1306
- }
1307
- }
1308
- return this.body;
1309
- }
1310
- /**
1311
- * Extract GraphQL data from response
1312
- *
1313
- * @returns The data field from GraphQL response, or null/undefined if not present
1314
- */
1315
- graphqlData() {
1316
- const response = this.json();
1317
- return typeof response === "object" && response !== null && "data" in response ? response.data : void 0;
1318
- }
1319
- /**
1320
- * Extract GraphQL errors from response
1321
- *
1322
- * @returns Array of GraphQL error objects, or empty array if none present
1323
- */
1324
- graphqlErrors() {
1325
- const response = this.json();
1326
- return typeof response === "object" && response !== null && Array.isArray(response.errors) ? response.errors : [];
1327
- }
1328
- };
1329
- var JsNativeClient = class {
1330
- routes;
1331
- handlers;
1332
- dependencies;
1333
- config;
1334
- rateLimitBuckets;
1335
- websocketRoutes;
1336
- websocketHandlers;
1337
- constructor(routesJson, _websocketRoutesJson, handlers, websocketHandlers, dependencies, _lifecycleHooks, config) {
1338
- this.routes = JSON.parse(routesJson);
1339
- this.websocketRoutes = JSON.parse(_websocketRoutesJson ?? "[]");
1340
- this.handlers = handlers;
1341
- this.websocketHandlers = websocketHandlers;
1342
- this.dependencies = dependencies;
1343
- this.config = config;
1344
- this.rateLimitBuckets = /* @__PURE__ */ new Map();
1345
- }
1346
- matchRoute(method, path2) {
1347
- const cleanedPath = path2.split("?")[0] ?? path2;
1348
- for (const route2 of this.routes) {
1349
- if (route2.method.toUpperCase() !== method.toUpperCase()) {
1350
- continue;
1351
- }
1352
- const params = this.extractParams(route2.path, cleanedPath);
1353
- if (params) {
1354
- return { handlerName: route2.handler_name, params, route: route2 };
1355
- }
1356
- }
1357
- throw new Error(`No route matched ${method} ${path2}`);
1358
- }
1359
- extractParams(pattern, actual) {
1360
- const patternParts = pattern.split("/").filter(Boolean);
1361
- const actualParts = actual.split("/").filter(Boolean);
1362
- const hasTailParam = patternParts.some((part) => part.includes(":path"));
1363
- if (!hasTailParam && patternParts.length !== actualParts.length) {
1364
- return null;
1365
- }
1366
- if (hasTailParam && actualParts.length < patternParts.length - 1) {
1367
- return null;
1368
- }
1369
- const params = {};
1370
- for (let i = 0; i < patternParts.length; i += 1) {
1371
- const patternPart = patternParts[i];
1372
- const actualPart = actualParts[i];
1373
- if (!patternPart || !actualPart) {
1374
- return null;
1375
- }
1376
- if (patternPart.startsWith(":") || patternPart.startsWith("{") && patternPart.endsWith("}")) {
1377
- const isPathTailParam = patternPart.includes(":path");
1378
- const rawKey = patternPart.startsWith("{") ? patternPart.slice(1, -1).split(":")[0] : patternPart.slice(1);
1379
- const key = rawKey ?? "";
1380
- if (isPathTailParam) {
1381
- params[key] = decodeURIComponent(actualParts.slice(i).join("/"));
1382
- return params;
1383
- }
1384
- params[key] = decodeURIComponent(actualPart);
1385
- continue;
1386
- }
1387
- if (patternPart !== actualPart) {
1388
- return null;
1389
- }
1390
- }
1391
- return params;
1392
- }
1393
- buildQuery(path2) {
1394
- const query = {};
1395
- const url = new URL(path2, "http://localhost");
1396
- url.searchParams.forEach((value, key) => {
1397
- if (!(key in query)) {
1398
- query[key] = value;
1399
- }
1400
- });
1401
- return query;
1402
- }
1403
- validateParams(route2, params) {
1404
- const schema = route2.parameter_schema;
1405
- if (!schema?.properties) {
1406
- return null;
1407
- }
1408
- for (const [key, meta] of Object.entries(schema.properties)) {
1409
- if (meta.source !== "path") {
1410
- continue;
1411
- }
1412
- const raw = params[key];
1413
- if (raw === void 0) {
1414
- continue;
1415
- }
1416
- if (meta.format === "date" || meta.format === "date-time") {
1417
- const parsed = new Date(raw);
1418
- if (Number.isNaN(parsed.valueOf())) {
1419
- return new JsTestResponse(422, {}, Buffer.from(""));
1420
- }
1421
- params[key] = parsed;
1422
- }
1423
- }
1424
- return null;
1425
- }
1426
- async invoke(method, path2, headers, body) {
1427
- const routeMatch = (() => {
1428
- try {
1429
- return this.matchRoute(method, path2);
1430
- } catch {
1431
- return null;
1432
- }
1433
- })();
1434
- if (!routeMatch) {
1435
- const staticResponse = await this.serveStatic(path2);
1436
- if (staticResponse) {
1437
- return staticResponse;
1438
- }
1439
- throw new Error(`No route matched ${method} ${path2}`);
1440
- }
1441
- const { handlerName, params, route: route2 } = routeMatch;
1442
- const handler = this.handlers[handlerName];
1443
- if (!handler) {
1444
- throw new Error(`Handler not found for ${handlerName}`);
1445
- }
1446
- if (this.config?.rateLimit && this.isRateLimited(route2.path)) {
1447
- return new JsTestResponse(429, {}, Buffer.from(""));
1448
- }
1449
- const validationResponse = this.validateParams(route2, params);
1450
- if (validationResponse) {
1451
- return validationResponse;
1452
- }
1453
- const requestPayload = {
1454
- method,
1455
- path: path2,
1456
- params,
1457
- query: this.buildQuery(path2),
1458
- headers: headers ?? {},
1459
- cookies: {},
1460
- body: this.encodeBody(body),
1461
- dependencies: this.dependencies ?? void 0
1462
- };
1463
- const result = await handler(this.safeStringify(requestPayload));
1464
- const response = await this.toResponse(result);
1465
- return this.applyCompression(response, headers);
1466
- }
1467
- async toResponse(result) {
1468
- if (isStreamingResponse(result)) {
1469
- const handle = getStreamingHandle(result);
1470
- if (handle.kind === "js") {
1471
- const buffers = [];
1472
- for await (const chunk of handle.iterator) {
1473
- if (chunk === null || chunk === void 0) {
1474
- continue;
1475
- }
1476
- if (Buffer.isBuffer(chunk)) {
1477
- buffers.push(chunk);
1478
- continue;
1479
- }
1480
- if (typeof chunk === "string") {
1481
- buffers.push(Buffer.from(chunk));
1482
- continue;
1483
- }
1484
- if (chunk instanceof ArrayBuffer || ArrayBuffer.isView(chunk)) {
1485
- const view = ArrayBuffer.isView(chunk) ? Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength) : Buffer.from(chunk);
1486
- buffers.push(view);
1487
- continue;
1488
- }
1489
- buffers.push(Buffer.from(this.safeStringify(chunk)));
1490
- }
1491
- const bodyBuffer = buffers.length === 0 ? Buffer.alloc(0) : Buffer.concat(buffers);
1492
- return new JsTestResponse(handle.init.statusCode ?? 200, handle.init.headers ?? {}, bodyBuffer);
1493
- }
1494
- }
1495
- if (typeof result === "string") {
1496
- try {
1497
- const parsed = JSON.parse(result);
1498
- if (parsed && typeof parsed === "object" && ("status" in parsed || "statusCode" in parsed || "body" in parsed)) {
1499
- const statusCode = parsed.status ?? parsed.statusCode ?? 200;
1500
- const textBody = typeof parsed.body === "string" || parsed.body === void 0 ? parsed.body ?? "" : JSON.stringify(parsed.body);
1501
- return new JsTestResponse(statusCode, parsed.headers ?? {}, Buffer.from(textBody));
1502
- }
1503
- } catch {
1504
- }
1505
- return new JsTestResponse(200, {}, Buffer.from(result));
1506
- }
1507
- if (result && typeof result === "object" && ("status" in result || "statusCode" in result)) {
1508
- const payload = result;
1509
- const statusCode = payload.status ?? payload.statusCode ?? 200;
1510
- const textBody = typeof payload.body === "string" || payload.body === void 0 ? payload.body ?? "" : JSON.stringify(payload.body);
1511
- return new JsTestResponse(statusCode, payload.headers ?? {}, Buffer.from(textBody));
1512
- }
1513
- const text = this.safeStringify(result);
1514
- return new JsTestResponse(200, {}, Buffer.from(text));
1515
- }
1516
- isRateLimited(routePath) {
1517
- if (!this.config?.rateLimit) {
1518
- return false;
1519
- }
1520
- const now = Math.floor(Date.now() / 1e3);
1521
- const key = routePath;
1522
- const existing = this.rateLimitBuckets.get(key);
1523
- const bucket = existing && existing.resetAt === now ? existing : { tokens: this.config.rateLimit.burst, resetAt: now };
1524
- if (bucket.tokens <= 0) {
1525
- this.rateLimitBuckets.set(key, bucket);
1526
- return true;
1527
- }
1528
- bucket.tokens -= 1;
1529
- this.rateLimitBuckets.set(key, bucket);
1530
- return false;
1531
- }
1532
- async serveStatic(targetPath) {
1533
- const normalized = targetPath.split("?")[0] ?? targetPath;
1534
- const staticConfig = this.config?.staticFiles ?? [];
1535
- for (const entry of staticConfig) {
1536
- if (!normalized.startsWith(entry.routePrefix)) {
1537
- continue;
1538
- }
1539
- const relative = normalized.slice(entry.routePrefix.length);
1540
- const resolved = relative === "/" || relative === "" ? "index.html" : relative.replace(/^\//, "");
1541
- const filePath = import_node_path.default.join(entry.directory, resolved);
1542
- try {
1543
- const contents = await import_promises.default.readFile(filePath);
1544
- const contentType = this.detectContentType(filePath);
1545
- const headers = {
1546
- "content-type": contentType
1547
- };
1548
- if (entry.cacheControl) {
1549
- headers["cache-control"] = entry.cacheControl;
1550
- }
1551
- const bodyBuffer = contentType.startsWith("text/") ? Buffer.from(contents.toString("utf-8").replace(/\n$/, "")) : contents;
1552
- return new JsTestResponse(200, headers, bodyBuffer);
1553
- } catch {
1554
- }
1555
- }
1556
- return null;
1557
- }
1558
- detectContentType(filePath) {
1559
- const ext = import_node_path.default.extname(filePath).toLowerCase();
1560
- switch (ext) {
1561
- case ".txt":
1562
- return "text/plain";
1563
- case ".html":
1564
- case ".htm":
1565
- return "text/html";
1566
- case ".json":
1567
- return "application/json";
1568
- case ".xml":
1569
- return "application/xml";
1570
- case ".csv":
1571
- return "text/csv";
1572
- case ".png":
1573
- return "image/png";
1574
- case ".jpg":
1575
- case ".jpeg":
1576
- return "image/jpeg";
1577
- case ".pdf":
1578
- return "application/pdf";
1579
- default:
1580
- return "application/octet-stream";
1581
- }
1582
- }
1583
- applyCompression(response, requestHeaders) {
1584
- const config = this.config?.compression;
1585
- const acceptsGzip = (this.lookupHeader(requestHeaders, "accept-encoding") ?? "").includes("gzip");
1586
- if (!config || !config.gzip || !acceptsGzip) {
1587
- return response;
1588
- }
1589
- const rawBody = response.bytes();
1590
- if (rawBody.length < (config.minSize ?? 0)) {
1591
- return response;
1592
- }
1593
- const gzipped = (0, import_node_zlib.gzipSync)(rawBody, { level: config.quality ?? 6 });
1594
- const headers = { ...response.headers(), "content-encoding": "gzip" };
1595
- return new JsTestResponse(response.statusCode, headers, gzipped);
1596
- }
1597
- lookupHeader(headers, name) {
1598
- if (!headers) {
1599
- return void 0;
1600
- }
1601
- const target = name.toLowerCase();
1602
- for (const [key, value] of Object.entries(headers)) {
1603
- if (key.toLowerCase() === target) {
1604
- return value;
1605
- }
1606
- }
1607
- return void 0;
1608
- }
1609
- safeStringify(value) {
1610
- return JSON.stringify(value, (_key, val) => {
1611
- if (typeof val === "bigint") {
1612
- return val.toString();
1613
- }
1614
- return val;
1615
- });
1616
- }
1617
- encodeBody(body) {
1618
- if (body === null) {
1619
- return null;
1620
- }
1621
- if (typeof body === "object" && ("__spikard_multipart__" in body || "__spikard_form__" in body)) {
1622
- return Array.from(Buffer.from(this.safeStringify(body)));
1623
- }
1624
- if (Buffer.isBuffer(body)) {
1625
- return Array.from(body);
1626
- }
1627
- return body;
1628
- }
1629
- async get(path2, headers) {
1630
- return this.invoke("GET", path2, headers, null);
1631
- }
1632
- async post(path2, headers, body) {
1633
- return this.invoke("POST", path2, headers, body);
1634
- }
1635
- async put(path2, headers, body) {
1636
- return this.invoke("PUT", path2, headers, body);
1637
- }
1638
- async delete(path2, headers) {
1639
- return this.invoke("DELETE", path2, headers, null);
1640
- }
1641
- async patch(path2, headers, body) {
1642
- return this.invoke("PATCH", path2, headers, body);
1643
- }
1644
- async head(path2, headers) {
1645
- return this.invoke("HEAD", path2, headers, null);
1646
- }
1647
- async options(path2, headers) {
1648
- return this.invoke("OPTIONS", path2, headers, null);
1649
- }
1650
- async trace(path2, headers) {
1651
- return this.invoke("TRACE", path2, headers, null);
1652
- }
1653
- async websocket(_path) {
1654
- const match = this.websocketRoutes?.find((route2) => route2.path === _path);
1655
- if (!match) {
1656
- throw new Error("WebSocket testing is not available in the JS fallback client");
1657
- }
1658
- const handlerEntry = this.websocketHandlers?.[match.handler_name];
1659
- if (!handlerEntry) {
1660
- throw new Error("WebSocket testing is not available in the JS fallback client");
1661
- }
1662
- const handler = handlerEntry && typeof handlerEntry.handleMessage === "function" ? handlerEntry.handleMessage : null;
1663
- if (!handler) {
1664
- throw new Error("WebSocket testing is not available in the JS fallback client");
1665
- }
1666
- const mock = new MockWebSocketConnection(async (msg) => handler(msg));
1667
- return mock;
1668
- }
1669
- };
1670
- var defaultNativeClientFactory = (routesJson, websocketRoutesJson, handlers, websocketHandlers, dependencies, lifecycleHooks, config) => {
1671
- if (isNativeCtor && nativeTestClient) {
1672
- return new nativeTestClient(
1673
- routesJson,
1674
- websocketRoutesJson,
1675
- handlers,
1676
- websocketHandlers,
1677
- dependencies,
1678
- lifecycleHooks,
1679
- config
1680
- );
1681
- }
1682
- return new JsNativeClient(
1683
- routesJson,
1684
- websocketRoutesJson,
1685
- handlers,
1686
- websocketHandlers,
1687
- dependencies,
1688
- lifecycleHooks,
1689
- config
1690
- );
1691
- };
1692
- var nativeClientFactory = defaultNativeClientFactory;
1693
- var TestClient = class {
1694
- app;
1695
- nativeClient;
1696
- looksLikeStringHandler(fn) {
1697
- const source = fn.toString();
1698
- return source.includes("requestJson") || source.includes("request_json") || source.includes("JSON.parse") || source.includes("JSON.parse(");
1699
- }
1700
- /**
1701
- * Create a new test client
1702
- *
1703
- * @param app - Spikard application with routes and handlers
1704
- */
1705
- constructor(app) {
1706
- if (!app || !Array.isArray(app.routes)) {
1707
- throw new Error("Invalid Spikard app: missing routes array");
1708
- }
1709
- this.app = app;
1710
- const routesJson = JSON.stringify(app.routes);
1711
- const websocketRoutesJson = JSON.stringify(app.websocketRoutes ?? []);
1712
- const handlerEntries = Object.entries(app.handlers || {});
1713
- const handlersMap = Object.fromEntries(
1714
- handlerEntries.map(([name, handler]) => {
1715
- const prefersParsedBody = this.looksLikeStringHandler(handler);
1716
- const nativeHandler = isNativeHandler(handler) || prefersParsedBody ? handler : wrapHandler(handler);
1717
- if (prefersParsedBody) {
1718
- nativeHandler.__spikard_raw_body = false;
1719
- } else if (nativeHandler.__spikard_raw_body === void 0) {
1720
- nativeHandler.__spikard_raw_body = true;
1721
- }
1722
- return [name, nativeHandler];
1723
- })
1724
- );
1725
- const websocketHandlersMap = app.websocketHandlers || {};
1726
- const config = app.config ?? null;
1727
- const dependencies = app.dependencies ?? null;
1728
- const lifecycleHooks = app.getLifecycleHooks?.() ?? null;
1729
- this.nativeClient = nativeClientFactory(
1730
- routesJson,
1731
- websocketRoutesJson,
1732
- handlersMap,
1733
- websocketHandlersMap,
1734
- dependencies,
1735
- lifecycleHooks,
1736
- config
1737
- );
1738
- }
1739
- /**
1740
- * Make a GET request
1741
- *
1742
- * @param path - Request path
1743
- * @param headers - Optional request headers
1744
- * @returns Response promise
1745
- */
1746
- async get(path2, headers) {
1747
- return this.nativeClient.get(path2, headers || null);
1748
- }
1749
- /**
1750
- * Make a POST request
1751
- *
1752
- * @param path - Request path
1753
- * @param options - Request options
1754
- * @returns Response promise
1755
- */
1756
- async post(path2, options) {
1757
- return this.nativeClient.post(path2, this.buildHeaders(options), this.buildBody(options));
1758
- }
1759
- /**
1760
- * Make a PUT request
1761
- *
1762
- * @param path - Request path
1763
- * @param options - Request options
1764
- * @returns Response promise
1765
- */
1766
- async put(path2, options) {
1767
- return this.nativeClient.put(path2, this.buildHeaders(options), this.buildBody(options));
1768
- }
1769
- /**
1770
- * Make a DELETE request
1771
- *
1772
- * @param path - Request path
1773
- * @param headers - Optional request headers
1774
- * @returns Response promise
1775
- */
1776
- async delete(path2, headers) {
1777
- return this.nativeClient.delete(path2, headers || null);
1778
- }
1779
- /**
1780
- * Make a PATCH request
1781
- *
1782
- * @param path - Request path
1783
- * @param options - Request options
1784
- * @returns Response promise
1785
- */
1786
- async patch(path2, options) {
1787
- return this.nativeClient.patch(path2, this.buildHeaders(options), this.buildBody(options));
1788
- }
1789
- /**
1790
- * Make a HEAD request
1791
- *
1792
- * @param path - Request path
1793
- * @param headers - Optional request headers
1794
- * @returns Response promise
1795
- */
1796
- async head(path2, headers) {
1797
- return this.nativeClient.head(path2, headers || null);
1798
- }
1799
- /**
1800
- * Make an OPTIONS request
1801
- *
1802
- * @param path - Request path
1803
- * @param options - Request options
1804
- * @returns Response promise
1805
- */
1806
- async options(path2, options) {
1807
- return this.nativeClient.options(path2, this.buildHeaders(options));
1808
- }
1809
- /**
1810
- * Make a TRACE request
1811
- *
1812
- * @param path - Request path
1813
- * @param headers - Optional request headers
1814
- * @returns Response promise
1815
- */
1816
- async trace(path2, options) {
1817
- return this.nativeClient.trace(path2, this.buildHeaders(options));
1818
- }
1819
- buildHeaders(options) {
1820
- if (!options?.headers || Object.keys(options.headers).length === 0) {
1821
- return null;
1822
- }
1823
- return options.headers;
1824
- }
1825
- buildBody(options) {
1826
- if (!options) {
1827
- return null;
1828
- }
1829
- if (options.multipart) {
1830
- return {
1831
- __spikard_multipart__: {
1832
- fields: options.multipart.fields ?? {},
1833
- files: options.multipart.files ?? []
1834
- }
1835
- };
1836
- }
1837
- if (options.form) {
1838
- return {
1839
- __spikard_form__: options.form
1840
- };
1841
- }
1842
- if ("json" in options) {
1843
- return options.json ?? null;
1844
- }
1845
- return null;
1846
- }
1847
- /**
1848
- * Connect to a WebSocket endpoint
1849
- *
1850
- * Uses the native test client to create an in-memory WebSocket connection.
1851
- *
1852
- * @param path - WebSocket path
1853
- * @returns WebSocket test connection
1854
- */
1855
- async websocketConnect(path2) {
1856
- const handlerName = this.app.websocketRoutes?.find((r) => r.path === path2)?.handler_name;
1857
- const handlerEntry = handlerName ? this.app.websocketHandlers?.[handlerName] : void 0;
1858
- const handler = handlerEntry && typeof handlerEntry.handleMessage === "function" ? handlerEntry.handleMessage : null;
1859
- if (handler) {
1860
- const mock = new MockWebSocketConnection(async (msg) => handler(msg));
1861
- return mock;
1862
- }
1863
- const routeMatch = this.app.routes.find((r) => r.path === path2);
1864
- if (routeMatch) {
1865
- const handlerFn = this.app.handlers?.[routeMatch.handler_name];
1866
- if (handlerFn) {
1867
- const mock = new MockWebSocketConnection(async (msg) => {
1868
- const payload = typeof msg === "string" ? msg : JSON.stringify(msg);
1869
- const result = await handlerFn(payload);
1870
- if (typeof result === "string") {
1871
- try {
1872
- return JSON.parse(result);
1873
- } catch {
1874
- return result;
1875
- }
1876
- }
1877
- return result;
1878
- });
1879
- return mock;
1880
- }
1881
- }
1882
- return this.nativeClient.websocket(path2);
1883
- }
1884
- /**
1885
- * Send a GraphQL query/mutation
1886
- *
1887
- * Convenience method for sending GraphQL queries and mutations.
1888
- *
1889
- * @param query - GraphQL query string
1890
- * @param variables - Optional GraphQL variables object
1891
- * @param operationName - Optional GraphQL operation name
1892
- * @returns Response promise
1893
- *
1894
- * @example
1895
- * ```typescript
1896
- * const response = await client.graphql('query { user(id: "1") { id name } }');
1897
- * const user = response.graphqlData().user;
1898
- * ```
1899
- */
1900
- async graphql(query, variables, operationName) {
1901
- const json = { query };
1902
- if (variables !== null && variables !== void 0) {
1903
- json.variables = variables;
1904
- }
1905
- if (operationName !== null && operationName !== void 0) {
1906
- json.operationName = operationName;
1907
- }
1908
- return this.post("/graphql", { json });
1909
- }
1910
- /**
1911
- * Send a GraphQL query and get HTTP status separately
1912
- *
1913
- * Returns status information alongside the response for cases where
1914
- * you need both the HTTP status and the full response details.
1915
- *
1916
- * @param query - GraphQL query string
1917
- * @param variables - Optional GraphQL variables object
1918
- * @param operationName - Optional GraphQL operation name
1919
- * @returns Promise with status and response details
1920
- */
1921
- async graphqlWithStatus(query, variables, operationName) {
1922
- const response = await this.graphql(query, variables, operationName);
1923
- return {
1924
- status: response.statusCode,
1925
- statusCode: response.statusCode,
1926
- headers: JSON.stringify(response.headers()),
1927
- bodyText: response.text()
1928
- };
1929
- }
1930
- /**
1931
- * Send a GraphQL subscription over WebSocket and return the first event payload.
1932
- */
1933
- async graphqlSubscription(query, variables, operationName, path2 = "/graphql") {
1934
- const operationId = "spikard-subscription-1";
1935
- const subscriptionPayload = { query };
1936
- if (variables !== null && variables !== void 0) {
1937
- subscriptionPayload.variables = variables;
1938
- }
1939
- if (operationName !== null && operationName !== void 0) {
1940
- subscriptionPayload.operationName = operationName;
1941
- }
1942
- const ws = await this.websocketConnect(path2);
1943
- try {
1944
- await ws.sendJson({ type: "connection_init" });
1945
- let acknowledged = false;
1946
- for (let i = 0; i < GRAPHQL_WS_MAX_CONTROL_MESSAGES; i++) {
1947
- const message = decodeGraphqlWsMessage(
1948
- await withTimeout(ws.receiveJson(), GRAPHQL_WS_TIMEOUT_MS, "GraphQL connection_ack")
1949
- );
1950
- const messageType = typeof message.type === "string" ? message.type : "";
1951
- if (messageType === "connection_ack") {
1952
- acknowledged = true;
1953
- break;
1954
- }
1955
- if (messageType === "ping") {
1956
- const pong = { type: "pong" };
1957
- if ("payload" in message) {
1958
- pong.payload = message.payload;
1959
- }
1960
- await ws.sendJson(pong);
1961
- continue;
1962
- }
1963
- if (messageType === "connection_error" || messageType === "error") {
1964
- throw new Error(`GraphQL subscription rejected during init: ${JSON.stringify(message)}`);
1965
- }
1966
- }
1967
- if (!acknowledged) {
1968
- throw new Error("No GraphQL connection_ack received");
1969
- }
1970
- await ws.sendJson({
1971
- id: operationId,
1972
- type: "subscribe",
1973
- payload: subscriptionPayload
1974
- });
1975
- let event = null;
1976
- const errors = [];
1977
- let completeReceived = false;
1978
- for (let i = 0; i < GRAPHQL_WS_MAX_CONTROL_MESSAGES; i++) {
1979
- const message = decodeGraphqlWsMessage(
1980
- await withTimeout(ws.receiveJson(), GRAPHQL_WS_TIMEOUT_MS, "GraphQL subscription message")
1981
- );
1982
- const messageType = typeof message.type === "string" ? message.type : "";
1983
- const messageId = typeof message.id === "string" ? message.id : void 0;
1984
- const idMatches = messageId === void 0 || messageId === operationId;
1985
- if (messageType === "next" && idMatches) {
1986
- event = "payload" in message ? message.payload : null;
1987
- await ws.sendJson({ id: operationId, type: "complete" });
1988
- try {
1989
- const maybeComplete = decodeGraphqlWsMessage(
1990
- await withTimeout(ws.receiveJson(), GRAPHQL_WS_TIMEOUT_MS, "GraphQL complete message")
1991
- );
1992
- const completeType = typeof maybeComplete.type === "string" ? maybeComplete.type : "";
1993
- const completeId = typeof maybeComplete.id === "string" ? maybeComplete.id : void 0;
1994
- if (completeType === "complete" && (completeId === void 0 || completeId === operationId)) {
1995
- completeReceived = true;
1996
- }
1997
- } catch {
1998
- }
1999
- break;
2000
- }
2001
- if (messageType === "error") {
2002
- errors.push("payload" in message ? message.payload : message);
2003
- break;
2004
- }
2005
- if (messageType === "complete" && idMatches) {
2006
- completeReceived = true;
2007
- break;
2008
- }
2009
- if (messageType === "ping") {
2010
- const pong = { type: "pong" };
2011
- if ("payload" in message) {
2012
- pong.payload = message.payload;
2013
- }
2014
- await ws.sendJson(pong);
2015
- }
2016
- }
2017
- if (event === null && errors.length === 0 && !completeReceived) {
2018
- throw new Error("No GraphQL subscription event received before timeout");
2019
- }
2020
- return {
2021
- operationId,
2022
- acknowledged,
2023
- event,
2024
- errors,
2025
- completeReceived
2026
- };
2027
- } finally {
2028
- await ws.close();
2029
- }
2030
- }
2031
- /**
2032
- * Cleanup resources when test client is done
2033
- */
2034
- async cleanup() {
2035
- }
2036
- };
2037
- // Annotate the CommonJS export names for ESM import in node:
2038
- 0 && (module.exports = {
2039
- GrpcError,
2040
- GrpcService,
2041
- GrpcStatusCode,
2042
- Spikard,
2043
- StreamingResponse,
2044
- TestClient,
2045
- UploadFile,
2046
- background,
2047
- createServiceHandler,
2048
- createUnaryHandler,
2049
- del,
2050
- get,
2051
- patch,
2052
- post,
2053
- put,
2054
- route,
2055
- runServer,
2056
- wrapBodyHandler,
2057
- wrapHandler
2058
- });
2059
- //# sourceMappingURL=index.js.map