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