@spikard/node 0.9.0 → 0.10.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 ADDED
@@ -0,0 +1,1645 @@
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
+ lifecycleHooks = {
552
+ onRequest: [],
553
+ preValidation: [],
554
+ preHandler: [],
555
+ onResponse: [],
556
+ onError: []
557
+ };
558
+ dependencies = {};
559
+ /**
560
+ * Add a route to the application
561
+ *
562
+ * @param metadata - Route configuration metadata
563
+ * @param handler - Handler function (sync or async)
564
+ */
565
+ addRoute(metadata, handler) {
566
+ this.routes.push(metadata);
567
+ this.handlers[metadata.handler_name] = handler;
568
+ }
569
+ /**
570
+ * Register a WebSocket route (message-based)
571
+ */
572
+ websocket(path2, handler, options = {}) {
573
+ const handlerName = options.handlerName ?? `ws_${this.websocketRoutes.length}_${path2}`.replace(/[^a-zA-Z0-9_]/g, "_");
574
+ const handlerWrapper = { handleMessage: handler };
575
+ if (options.messageSchema) {
576
+ handlerWrapper._messageSchema = options.messageSchema;
577
+ }
578
+ if (options.responseSchema) {
579
+ handlerWrapper._responseSchema = options.responseSchema;
580
+ }
581
+ const route2 = {
582
+ method: "GET",
583
+ path: path2,
584
+ handler_name: handlerName,
585
+ request_schema: options.messageSchema,
586
+ response_schema: options.responseSchema,
587
+ parameter_schema: void 0,
588
+ file_params: void 0,
589
+ is_async: true
590
+ };
591
+ this.websocketRoutes.push(route2);
592
+ this.websocketHandlers[handlerName] = handlerWrapper;
593
+ }
594
+ /**
595
+ * Run the server
596
+ *
597
+ * @param options - Server configuration
598
+ */
599
+ run(options = {}) {
600
+ runServer(this, options);
601
+ }
602
+ /**
603
+ * Register an onRequest lifecycle hook
604
+ *
605
+ * Runs before routing. Can inspect/modify the request or short-circuit with a response.
606
+ *
607
+ * @param hook - Async function that receives a request and returns either:
608
+ * - The (possibly modified) request to continue processing
609
+ * - A Response object to short-circuit the request pipeline
610
+ * @returns The hook function (for decorator usage)
611
+ *
612
+ * @example
613
+ * ```typescript
614
+ * app.onRequest(async (request) => {
615
+ * console.log(`Request: ${request.method} ${request.path}`);
616
+ * return request;
617
+ * });
618
+ * ```
619
+ */
620
+ onRequest(hook) {
621
+ this.lifecycleHooks.onRequest.push(hook);
622
+ return hook;
623
+ }
624
+ /**
625
+ * Register a preValidation lifecycle hook
626
+ *
627
+ * Runs after routing but before validation. Useful for rate limiting.
628
+ *
629
+ * @param hook - Async function that receives a request and returns either:
630
+ * - The (possibly modified) request to continue processing
631
+ * - A Response object to short-circuit the request pipeline
632
+ * @returns The hook function (for decorator usage)
633
+ *
634
+ * @example
635
+ * ```typescript
636
+ * app.preValidation(async (request) => {
637
+ * if (tooManyRequests()) {
638
+ * return { error: "Rate limit exceeded", status: 429 };
639
+ * }
640
+ * return request;
641
+ * });
642
+ * ```
643
+ */
644
+ preValidation(hook) {
645
+ this.lifecycleHooks.preValidation.push(hook);
646
+ return hook;
647
+ }
648
+ /**
649
+ * Register a preHandler lifecycle hook
650
+ *
651
+ * Runs after validation but before the handler. Ideal for authentication/authorization.
652
+ *
653
+ * @param hook - Async function that receives a request and returns either:
654
+ * - The (possibly modified) request to continue processing
655
+ * - A Response object to short-circuit the request pipeline
656
+ * @returns The hook function (for decorator usage)
657
+ *
658
+ * @example
659
+ * ```typescript
660
+ * app.preHandler(async (request) => {
661
+ * if (!validToken(request.headers.authorization)) {
662
+ * return { error: "Unauthorized", status: 401 };
663
+ * }
664
+ * return request;
665
+ * });
666
+ * ```
667
+ */
668
+ preHandler(hook) {
669
+ this.lifecycleHooks.preHandler.push(hook);
670
+ return hook;
671
+ }
672
+ /**
673
+ * Register an onResponse lifecycle hook
674
+ *
675
+ * Runs after the handler executes. Can modify the response.
676
+ *
677
+ * @param hook - Async function that receives a response and returns the (possibly modified) response
678
+ * @returns The hook function (for decorator usage)
679
+ *
680
+ * @example
681
+ * ```typescript
682
+ * app.onResponse(async (response) => {
683
+ * response.headers["X-Frame-Options"] = "DENY";
684
+ * return response;
685
+ * });
686
+ * ```
687
+ */
688
+ onResponse(hook) {
689
+ this.lifecycleHooks.onResponse.push(hook);
690
+ return hook;
691
+ }
692
+ /**
693
+ * Register an onError lifecycle hook
694
+ *
695
+ * Runs when an error occurs. Can customize error responses.
696
+ *
697
+ * @param hook - Async function that receives an error response and returns a (possibly modified) response
698
+ * @returns The hook function (for decorator usage)
699
+ *
700
+ * @example
701
+ * ```typescript
702
+ * app.onError(async (response) => {
703
+ * response.headers["Content-Type"] = "application/json";
704
+ * return response;
705
+ * });
706
+ * ```
707
+ */
708
+ onError(hook) {
709
+ this.lifecycleHooks.onError.push(hook);
710
+ return hook;
711
+ }
712
+ /**
713
+ * Register a dependency value or factory
714
+ *
715
+ * Provides a value or factory function to be injected into handlers.
716
+ * Dependencies are matched by parameter name or can be accessed via the
717
+ * request context.
718
+ *
719
+ * @param key - Unique identifier for the dependency
720
+ * @param valueOrFactory - Static value or factory function
721
+ * @param options - Configuration options for the dependency
722
+ * @returns The Spikard instance for method chaining
723
+ *
724
+ * @example
725
+ * ```typescript
726
+ * // Simple value dependency
727
+ * app.provide('app_name', 'MyApp');
728
+ * app.provide('max_connections', 100);
729
+ *
730
+ * // Factory dependency
731
+ * app.provide('db_pool', async ({ database_url }) => {
732
+ * return await createPool(database_url);
733
+ * }, { dependsOn: ['database_url'], singleton: true });
734
+ *
735
+ * // Use in handler
736
+ * app.get('/config', async (request, { app_name, db_pool }) => {
737
+ * return { app: app_name, pool: db_pool };
738
+ * });
739
+ * ```
740
+ */
741
+ provide(key, valueOrFactory, options) {
742
+ const normalizedKey = normalizeDependencyKey(key);
743
+ const isFactory = typeof valueOrFactory === "function";
744
+ const factory = isFactory ? wrapDependencyFactory(valueOrFactory) : void 0;
745
+ const explicitDependsOn = options?.dependsOn;
746
+ const inferredDependsOn = isFactory && explicitDependsOn === void 0 ? inferDependsOn(valueOrFactory) : [];
747
+ const dependsOn = (explicitDependsOn ?? inferredDependsOn).map((depKey) => normalizeDependencyKey(depKey));
748
+ this.dependencies[normalizedKey] = {
749
+ isFactory,
750
+ value: isFactory ? void 0 : valueOrFactory,
751
+ factory,
752
+ dependsOn,
753
+ singleton: options?.singleton ?? false,
754
+ cacheable: options?.cacheable ?? !isFactory
755
+ };
756
+ return this;
757
+ }
758
+ /**
759
+ * Get all registered lifecycle hooks
760
+ *
761
+ * @returns Dictionary of hook lists by type
762
+ */
763
+ getLifecycleHooks() {
764
+ return {
765
+ onRequest: [...this.lifecycleHooks.onRequest ?? []],
766
+ preValidation: [...this.lifecycleHooks.preValidation ?? []],
767
+ preHandler: [...this.lifecycleHooks.preHandler ?? []],
768
+ onResponse: [...this.lifecycleHooks.onResponse ?? []],
769
+ onError: [...this.lifecycleHooks.onError ?? []]
770
+ };
771
+ }
772
+ };
773
+
774
+ // src/background.ts
775
+ var background_exports = {};
776
+ __export(background_exports, {
777
+ run: () => run
778
+ });
779
+ import { backgroundRun } from "../index.js";
780
+ function run(work) {
781
+ const task = async () => {
782
+ await work();
783
+ return void 0;
784
+ };
785
+ backgroundRun(task);
786
+ }
787
+
788
+ // src/grpc.ts
789
+ var GrpcStatusCode = /* @__PURE__ */ ((GrpcStatusCode2) => {
790
+ GrpcStatusCode2[GrpcStatusCode2["OK"] = 0] = "OK";
791
+ GrpcStatusCode2[GrpcStatusCode2["CANCELLED"] = 1] = "CANCELLED";
792
+ GrpcStatusCode2[GrpcStatusCode2["UNKNOWN"] = 2] = "UNKNOWN";
793
+ GrpcStatusCode2[GrpcStatusCode2["INVALID_ARGUMENT"] = 3] = "INVALID_ARGUMENT";
794
+ GrpcStatusCode2[GrpcStatusCode2["DEADLINE_EXCEEDED"] = 4] = "DEADLINE_EXCEEDED";
795
+ GrpcStatusCode2[GrpcStatusCode2["NOT_FOUND"] = 5] = "NOT_FOUND";
796
+ GrpcStatusCode2[GrpcStatusCode2["ALREADY_EXISTS"] = 6] = "ALREADY_EXISTS";
797
+ GrpcStatusCode2[GrpcStatusCode2["PERMISSION_DENIED"] = 7] = "PERMISSION_DENIED";
798
+ GrpcStatusCode2[GrpcStatusCode2["RESOURCE_EXHAUSTED"] = 8] = "RESOURCE_EXHAUSTED";
799
+ GrpcStatusCode2[GrpcStatusCode2["FAILED_PRECONDITION"] = 9] = "FAILED_PRECONDITION";
800
+ GrpcStatusCode2[GrpcStatusCode2["ABORTED"] = 10] = "ABORTED";
801
+ GrpcStatusCode2[GrpcStatusCode2["OUT_OF_RANGE"] = 11] = "OUT_OF_RANGE";
802
+ GrpcStatusCode2[GrpcStatusCode2["UNIMPLEMENTED"] = 12] = "UNIMPLEMENTED";
803
+ GrpcStatusCode2[GrpcStatusCode2["INTERNAL"] = 13] = "INTERNAL";
804
+ GrpcStatusCode2[GrpcStatusCode2["UNAVAILABLE"] = 14] = "UNAVAILABLE";
805
+ GrpcStatusCode2[GrpcStatusCode2["DATA_LOSS"] = 15] = "DATA_LOSS";
806
+ GrpcStatusCode2[GrpcStatusCode2["UNAUTHENTICATED"] = 16] = "UNAUTHENTICATED";
807
+ return GrpcStatusCode2;
808
+ })(GrpcStatusCode || {});
809
+ var GrpcError = class extends Error {
810
+ /**
811
+ * gRPC status code
812
+ */
813
+ code;
814
+ /**
815
+ * Create a new gRPC error
816
+ *
817
+ * @param code - gRPC status code
818
+ * @param message - Error message
819
+ */
820
+ constructor(code, message) {
821
+ super(message);
822
+ this.code = code;
823
+ this.name = "GrpcError";
824
+ }
825
+ };
826
+ function createUnaryHandler(methodName, handler, requestType, responseType) {
827
+ return {
828
+ async handleRequest(request) {
829
+ if (request.methodName !== methodName) {
830
+ throw new GrpcError(12 /* UNIMPLEMENTED */, `Method ${request.methodName} not implemented`);
831
+ }
832
+ const req = requestType.decode(request.payload);
833
+ const result = await handler(req, request.metadata);
834
+ let response;
835
+ let responseMetadata;
836
+ if (result && typeof result === "object" && "response" in result) {
837
+ response = result.response;
838
+ responseMetadata = result.metadata;
839
+ } else {
840
+ response = result;
841
+ }
842
+ const encoded = responseType.encode(response).finish();
843
+ if (responseMetadata) {
844
+ return {
845
+ payload: Buffer.from(encoded),
846
+ metadata: responseMetadata
847
+ };
848
+ }
849
+ return {
850
+ payload: Buffer.from(encoded)
851
+ };
852
+ }
853
+ };
854
+ }
855
+ function createServiceHandler(methods) {
856
+ return {
857
+ async handleRequest(request) {
858
+ const handler = methods[request.methodName];
859
+ if (!handler) {
860
+ throw new GrpcError(12 /* UNIMPLEMENTED */, `Method ${request.methodName} not implemented`);
861
+ }
862
+ return handler.handleRequest(request);
863
+ }
864
+ };
865
+ }
866
+
867
+ // src/routing.ts
868
+ function route(path2, options = {}) {
869
+ return (handler) => {
870
+ const methods = options.methods ? Array.isArray(options.methods) ? options.methods : [options.methods] : ["GET"];
871
+ const metadata = {
872
+ method: methods.join(","),
873
+ path: path2,
874
+ handler_name: handler.name || "anonymous",
875
+ request_schema: options.bodySchema ?? void 0,
876
+ response_schema: options.responseSchema ?? void 0,
877
+ parameter_schema: options.parameterSchema ?? void 0,
878
+ cors: options.cors ?? void 0,
879
+ is_async: true
880
+ };
881
+ handler.__route_metadata__ = metadata;
882
+ return handler;
883
+ };
884
+ }
885
+ function get(path2, options = {}) {
886
+ return route(path2, { ...options, methods: ["GET"] });
887
+ }
888
+ function post(path2, options = {}) {
889
+ return route(path2, { ...options, methods: ["POST"] });
890
+ }
891
+ function put(path2, options = {}) {
892
+ return route(path2, { ...options, methods: ["PUT"] });
893
+ }
894
+ function del(path2, options = {}) {
895
+ return route(path2, { ...options, methods: ["DELETE"] });
896
+ }
897
+ function patch(path2, options = {}) {
898
+ return route(path2, { ...options, methods: ["PATCH"] });
899
+ }
900
+
901
+ // src/testing.ts
902
+ import fs from "fs/promises";
903
+ import { createRequire as createRequire3 } from "module";
904
+ import path from "path";
905
+ import { gunzipSync, gzipSync } from "zlib";
906
+ var MockWebSocketConnection = class {
907
+ handler;
908
+ queue = [];
909
+ constructor(handler) {
910
+ this.handler = handler;
911
+ }
912
+ async send_json(message) {
913
+ const response = await this.handler(message);
914
+ this.queue.push(response);
915
+ }
916
+ async sendJson(message) {
917
+ return this.send_json(message);
918
+ }
919
+ async sendText(text) {
920
+ return this.send_json(text);
921
+ }
922
+ async receive_json() {
923
+ return this.queue.shift();
924
+ }
925
+ async receiveJson() {
926
+ return this.receive_json();
927
+ }
928
+ async receiveText() {
929
+ const value = await this.receive_json();
930
+ return typeof value === "string" ? value : JSON.stringify(value);
931
+ }
932
+ async receiveBytes() {
933
+ const value = await this.receive_json();
934
+ if (Buffer.isBuffer(value)) {
935
+ return value;
936
+ }
937
+ if (typeof value === "string") {
938
+ return Buffer.from(value);
939
+ }
940
+ return Buffer.from(JSON.stringify(value));
941
+ }
942
+ async receiveMessage() {
943
+ return this.receive_json();
944
+ }
945
+ async close() {
946
+ }
947
+ };
948
+ var nativeTestClient = null;
949
+ var loadNativeTestClient = () => {
950
+ try {
951
+ const require2 = createRequire3(import.meta.url);
952
+ const binding = require2("../index.js");
953
+ return binding.TestClient;
954
+ } catch {
955
+ return null;
956
+ }
957
+ };
958
+ nativeTestClient = loadNativeTestClient();
959
+ var isNativeCtor = nativeTestClient !== null;
960
+ var JsTestResponse = class {
961
+ constructor(statusCode, headers, body) {
962
+ this.statusCode = statusCode;
963
+ this.headerBag = headers;
964
+ this.body = body;
965
+ }
966
+ body;
967
+ headerBag;
968
+ headers() {
969
+ return this.headerBag;
970
+ }
971
+ text() {
972
+ return this.decodeBody().toString("utf-8");
973
+ }
974
+ json() {
975
+ const raw = this.text();
976
+ if (raw.length === 0) {
977
+ return void 0;
978
+ }
979
+ try {
980
+ return JSON.parse(raw);
981
+ } catch {
982
+ return raw;
983
+ }
984
+ }
985
+ bytes() {
986
+ return this.decodeBody();
987
+ }
988
+ decodeBody() {
989
+ const encoding = (this.headerBag["content-encoding"] ?? "").toLowerCase();
990
+ if (encoding === "gzip") {
991
+ try {
992
+ return gunzipSync(this.body);
993
+ } catch {
994
+ return this.body;
995
+ }
996
+ }
997
+ return this.body;
998
+ }
999
+ /**
1000
+ * Extract GraphQL data from response
1001
+ *
1002
+ * @returns The data field from GraphQL response, or null/undefined if not present
1003
+ */
1004
+ graphqlData() {
1005
+ const response = this.json();
1006
+ return typeof response === "object" && response !== null && "data" in response ? response.data : void 0;
1007
+ }
1008
+ /**
1009
+ * Extract GraphQL errors from response
1010
+ *
1011
+ * @returns Array of GraphQL error objects, or empty array if none present
1012
+ */
1013
+ graphqlErrors() {
1014
+ const response = this.json();
1015
+ return typeof response === "object" && response !== null && Array.isArray(response.errors) ? response.errors : [];
1016
+ }
1017
+ };
1018
+ var JsNativeClient = class {
1019
+ routes;
1020
+ handlers;
1021
+ dependencies;
1022
+ config;
1023
+ rateLimitBuckets;
1024
+ websocketRoutes;
1025
+ websocketHandlers;
1026
+ constructor(routesJson, _websocketRoutesJson, handlers, websocketHandlers, dependencies, _lifecycleHooks, config) {
1027
+ this.routes = JSON.parse(routesJson);
1028
+ this.websocketRoutes = JSON.parse(_websocketRoutesJson ?? "[]");
1029
+ this.handlers = handlers;
1030
+ this.websocketHandlers = websocketHandlers;
1031
+ this.dependencies = dependencies;
1032
+ this.config = config;
1033
+ this.rateLimitBuckets = /* @__PURE__ */ new Map();
1034
+ }
1035
+ matchRoute(method, path2) {
1036
+ const cleanedPath = path2.split("?")[0] ?? path2;
1037
+ for (const route2 of this.routes) {
1038
+ if (route2.method.toUpperCase() !== method.toUpperCase()) {
1039
+ continue;
1040
+ }
1041
+ const params = this.extractParams(route2.path, cleanedPath);
1042
+ if (params) {
1043
+ return { handlerName: route2.handler_name, params, route: route2 };
1044
+ }
1045
+ }
1046
+ throw new Error(`No route matched ${method} ${path2}`);
1047
+ }
1048
+ extractParams(pattern, actual) {
1049
+ const patternParts = pattern.split("/").filter(Boolean);
1050
+ const actualParts = actual.split("/").filter(Boolean);
1051
+ const hasTailParam = patternParts.some((part) => part.includes(":path"));
1052
+ if (!hasTailParam && patternParts.length !== actualParts.length) {
1053
+ return null;
1054
+ }
1055
+ if (hasTailParam && actualParts.length < patternParts.length - 1) {
1056
+ return null;
1057
+ }
1058
+ const params = {};
1059
+ for (let i = 0; i < patternParts.length; i += 1) {
1060
+ const patternPart = patternParts[i];
1061
+ const actualPart = actualParts[i];
1062
+ if (!patternPart || !actualPart) {
1063
+ return null;
1064
+ }
1065
+ if (patternPart.startsWith(":") || patternPart.startsWith("{") && patternPart.endsWith("}")) {
1066
+ const isPathTailParam = patternPart.includes(":path");
1067
+ const rawKey = patternPart.startsWith("{") ? patternPart.slice(1, -1).split(":")[0] : patternPart.slice(1);
1068
+ const key = rawKey ?? "";
1069
+ if (isPathTailParam) {
1070
+ params[key] = decodeURIComponent(actualParts.slice(i).join("/"));
1071
+ return params;
1072
+ }
1073
+ params[key] = decodeURIComponent(actualPart);
1074
+ continue;
1075
+ }
1076
+ if (patternPart !== actualPart) {
1077
+ return null;
1078
+ }
1079
+ }
1080
+ return params;
1081
+ }
1082
+ buildQuery(path2) {
1083
+ const query = {};
1084
+ const url = new URL(path2, "http://localhost");
1085
+ url.searchParams.forEach((value, key) => {
1086
+ if (!(key in query)) {
1087
+ query[key] = value;
1088
+ }
1089
+ });
1090
+ return query;
1091
+ }
1092
+ validateParams(route2, params) {
1093
+ const schema = route2.parameter_schema;
1094
+ if (!schema?.properties) {
1095
+ return null;
1096
+ }
1097
+ for (const [key, meta] of Object.entries(schema.properties)) {
1098
+ if (meta.source !== "path") {
1099
+ continue;
1100
+ }
1101
+ const raw = params[key];
1102
+ if (raw === void 0) {
1103
+ continue;
1104
+ }
1105
+ if (meta.format === "date" || meta.format === "date-time") {
1106
+ const parsed = new Date(raw);
1107
+ if (Number.isNaN(parsed.valueOf())) {
1108
+ return new JsTestResponse(422, {}, Buffer.from(""));
1109
+ }
1110
+ params[key] = parsed;
1111
+ }
1112
+ }
1113
+ return null;
1114
+ }
1115
+ async invoke(method, path2, headers, body) {
1116
+ const routeMatch = (() => {
1117
+ try {
1118
+ return this.matchRoute(method, path2);
1119
+ } catch {
1120
+ return null;
1121
+ }
1122
+ })();
1123
+ if (!routeMatch) {
1124
+ const staticResponse = await this.serveStatic(path2);
1125
+ if (staticResponse) {
1126
+ return staticResponse;
1127
+ }
1128
+ throw new Error(`No route matched ${method} ${path2}`);
1129
+ }
1130
+ const { handlerName, params, route: route2 } = routeMatch;
1131
+ const handler = this.handlers[handlerName];
1132
+ if (!handler) {
1133
+ throw new Error(`Handler not found for ${handlerName}`);
1134
+ }
1135
+ if (this.config?.rateLimit && this.isRateLimited(route2.path)) {
1136
+ return new JsTestResponse(429, {}, Buffer.from(""));
1137
+ }
1138
+ const validationResponse = this.validateParams(route2, params);
1139
+ if (validationResponse) {
1140
+ return validationResponse;
1141
+ }
1142
+ const requestPayload = {
1143
+ method,
1144
+ path: path2,
1145
+ params,
1146
+ query: this.buildQuery(path2),
1147
+ headers: headers ?? {},
1148
+ cookies: {},
1149
+ body: this.encodeBody(body),
1150
+ dependencies: this.dependencies ?? void 0
1151
+ };
1152
+ const result = await handler(this.safeStringify(requestPayload));
1153
+ const response = await this.toResponse(result);
1154
+ return this.applyCompression(response, headers);
1155
+ }
1156
+ async toResponse(result) {
1157
+ if (isStreamingResponse(result)) {
1158
+ const handle = getStreamingHandle(result);
1159
+ if (handle.kind === "js") {
1160
+ const buffers = [];
1161
+ for await (const chunk of handle.iterator) {
1162
+ if (chunk === null || chunk === void 0) {
1163
+ continue;
1164
+ }
1165
+ if (Buffer.isBuffer(chunk)) {
1166
+ buffers.push(chunk);
1167
+ continue;
1168
+ }
1169
+ if (typeof chunk === "string") {
1170
+ buffers.push(Buffer.from(chunk));
1171
+ continue;
1172
+ }
1173
+ if (chunk instanceof ArrayBuffer || ArrayBuffer.isView(chunk)) {
1174
+ const view = ArrayBuffer.isView(chunk) ? Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength) : Buffer.from(chunk);
1175
+ buffers.push(view);
1176
+ continue;
1177
+ }
1178
+ buffers.push(Buffer.from(this.safeStringify(chunk)));
1179
+ }
1180
+ const bodyBuffer = buffers.length === 0 ? Buffer.alloc(0) : Buffer.concat(buffers);
1181
+ return new JsTestResponse(handle.init.statusCode ?? 200, handle.init.headers ?? {}, bodyBuffer);
1182
+ }
1183
+ }
1184
+ if (typeof result === "string") {
1185
+ try {
1186
+ const parsed = JSON.parse(result);
1187
+ if (parsed && typeof parsed === "object" && ("status" in parsed || "statusCode" in parsed || "body" in parsed)) {
1188
+ const statusCode = parsed.status ?? parsed.statusCode ?? 200;
1189
+ const textBody = typeof parsed.body === "string" || parsed.body === void 0 ? parsed.body ?? "" : JSON.stringify(parsed.body);
1190
+ return new JsTestResponse(statusCode, parsed.headers ?? {}, Buffer.from(textBody));
1191
+ }
1192
+ } catch {
1193
+ }
1194
+ return new JsTestResponse(200, {}, Buffer.from(result));
1195
+ }
1196
+ if (result && typeof result === "object" && ("status" in result || "statusCode" in result)) {
1197
+ const payload = result;
1198
+ const statusCode = payload.status ?? payload.statusCode ?? 200;
1199
+ const textBody = typeof payload.body === "string" || payload.body === void 0 ? payload.body ?? "" : JSON.stringify(payload.body);
1200
+ return new JsTestResponse(statusCode, payload.headers ?? {}, Buffer.from(textBody));
1201
+ }
1202
+ const text = this.safeStringify(result);
1203
+ return new JsTestResponse(200, {}, Buffer.from(text));
1204
+ }
1205
+ isRateLimited(routePath) {
1206
+ if (!this.config?.rateLimit) {
1207
+ return false;
1208
+ }
1209
+ const now = Math.floor(Date.now() / 1e3);
1210
+ const key = routePath;
1211
+ const existing = this.rateLimitBuckets.get(key);
1212
+ const bucket = existing && existing.resetAt === now ? existing : { tokens: this.config.rateLimit.burst, resetAt: now };
1213
+ if (bucket.tokens <= 0) {
1214
+ this.rateLimitBuckets.set(key, bucket);
1215
+ return true;
1216
+ }
1217
+ bucket.tokens -= 1;
1218
+ this.rateLimitBuckets.set(key, bucket);
1219
+ return false;
1220
+ }
1221
+ async serveStatic(targetPath) {
1222
+ const normalized = targetPath.split("?")[0] ?? targetPath;
1223
+ const staticConfig = this.config?.staticFiles ?? [];
1224
+ for (const entry of staticConfig) {
1225
+ if (!normalized.startsWith(entry.routePrefix)) {
1226
+ continue;
1227
+ }
1228
+ const relative = normalized.slice(entry.routePrefix.length);
1229
+ const resolved = relative === "/" || relative === "" ? "index.html" : relative.replace(/^\//, "");
1230
+ const filePath = path.join(entry.directory, resolved);
1231
+ try {
1232
+ const contents = await fs.readFile(filePath);
1233
+ const contentType = this.detectContentType(filePath);
1234
+ const headers = {
1235
+ "content-type": contentType
1236
+ };
1237
+ if (entry.cacheControl) {
1238
+ headers["cache-control"] = entry.cacheControl;
1239
+ }
1240
+ const bodyBuffer = contentType.startsWith("text/") ? Buffer.from(contents.toString("utf-8").replace(/\n$/, "")) : contents;
1241
+ return new JsTestResponse(200, headers, bodyBuffer);
1242
+ } catch {
1243
+ }
1244
+ }
1245
+ return null;
1246
+ }
1247
+ detectContentType(filePath) {
1248
+ const ext = path.extname(filePath).toLowerCase();
1249
+ switch (ext) {
1250
+ case ".txt":
1251
+ return "text/plain";
1252
+ case ".html":
1253
+ case ".htm":
1254
+ return "text/html";
1255
+ case ".json":
1256
+ return "application/json";
1257
+ case ".xml":
1258
+ return "application/xml";
1259
+ case ".csv":
1260
+ return "text/csv";
1261
+ case ".png":
1262
+ return "image/png";
1263
+ case ".jpg":
1264
+ case ".jpeg":
1265
+ return "image/jpeg";
1266
+ case ".pdf":
1267
+ return "application/pdf";
1268
+ default:
1269
+ return "application/octet-stream";
1270
+ }
1271
+ }
1272
+ applyCompression(response, requestHeaders) {
1273
+ const config = this.config?.compression;
1274
+ const acceptsGzip = (this.lookupHeader(requestHeaders, "accept-encoding") ?? "").includes("gzip");
1275
+ if (!config || !config.gzip || !acceptsGzip) {
1276
+ return response;
1277
+ }
1278
+ const rawBody = response.bytes();
1279
+ if (rawBody.length < (config.minSize ?? 0)) {
1280
+ return response;
1281
+ }
1282
+ const gzipped = gzipSync(rawBody, { level: config.quality ?? 6 });
1283
+ const headers = { ...response.headers(), "content-encoding": "gzip" };
1284
+ return new JsTestResponse(response.statusCode, headers, gzipped);
1285
+ }
1286
+ lookupHeader(headers, name) {
1287
+ if (!headers) {
1288
+ return void 0;
1289
+ }
1290
+ const target = name.toLowerCase();
1291
+ for (const [key, value] of Object.entries(headers)) {
1292
+ if (key.toLowerCase() === target) {
1293
+ return value;
1294
+ }
1295
+ }
1296
+ return void 0;
1297
+ }
1298
+ safeStringify(value) {
1299
+ return JSON.stringify(value, (_key, val) => {
1300
+ if (typeof val === "bigint") {
1301
+ return val.toString();
1302
+ }
1303
+ return val;
1304
+ });
1305
+ }
1306
+ encodeBody(body) {
1307
+ if (body === null) {
1308
+ return null;
1309
+ }
1310
+ if (typeof body === "object" && ("__spikard_multipart__" in body || "__spikard_form__" in body)) {
1311
+ return Array.from(Buffer.from(this.safeStringify(body)));
1312
+ }
1313
+ if (Buffer.isBuffer(body)) {
1314
+ return Array.from(body);
1315
+ }
1316
+ return body;
1317
+ }
1318
+ async get(path2, headers) {
1319
+ return this.invoke("GET", path2, headers, null);
1320
+ }
1321
+ async post(path2, headers, body) {
1322
+ return this.invoke("POST", path2, headers, body);
1323
+ }
1324
+ async put(path2, headers, body) {
1325
+ return this.invoke("PUT", path2, headers, body);
1326
+ }
1327
+ async delete(path2, headers) {
1328
+ return this.invoke("DELETE", path2, headers, null);
1329
+ }
1330
+ async patch(path2, headers, body) {
1331
+ return this.invoke("PATCH", path2, headers, body);
1332
+ }
1333
+ async head(path2, headers) {
1334
+ return this.invoke("HEAD", path2, headers, null);
1335
+ }
1336
+ async options(path2, headers) {
1337
+ return this.invoke("OPTIONS", path2, headers, null);
1338
+ }
1339
+ async trace(path2, headers) {
1340
+ return this.invoke("TRACE", path2, headers, null);
1341
+ }
1342
+ async websocket(_path) {
1343
+ const match = this.websocketRoutes?.find((route2) => route2.path === _path);
1344
+ if (!match) {
1345
+ throw new Error("WebSocket testing is not available in the JS fallback client");
1346
+ }
1347
+ const handlerEntry = this.websocketHandlers?.[match.handler_name];
1348
+ if (!handlerEntry) {
1349
+ throw new Error("WebSocket testing is not available in the JS fallback client");
1350
+ }
1351
+ const handler = handlerEntry && typeof handlerEntry.handleMessage === "function" ? handlerEntry.handleMessage : null;
1352
+ if (!handler) {
1353
+ throw new Error("WebSocket testing is not available in the JS fallback client");
1354
+ }
1355
+ const mock = new MockWebSocketConnection(async (msg) => handler(msg));
1356
+ return mock;
1357
+ }
1358
+ };
1359
+ var defaultNativeClientFactory = (routesJson, websocketRoutesJson, handlers, websocketHandlers, dependencies, lifecycleHooks, config) => {
1360
+ if (isNativeCtor && nativeTestClient) {
1361
+ return new nativeTestClient(
1362
+ routesJson,
1363
+ websocketRoutesJson,
1364
+ handlers,
1365
+ websocketHandlers,
1366
+ dependencies,
1367
+ lifecycleHooks,
1368
+ config
1369
+ );
1370
+ }
1371
+ return new JsNativeClient(
1372
+ routesJson,
1373
+ websocketRoutesJson,
1374
+ handlers,
1375
+ websocketHandlers,
1376
+ dependencies,
1377
+ lifecycleHooks,
1378
+ config
1379
+ );
1380
+ };
1381
+ var nativeClientFactory = defaultNativeClientFactory;
1382
+ var TestClient = class {
1383
+ app;
1384
+ nativeClient;
1385
+ looksLikeStringHandler(fn) {
1386
+ const source = fn.toString();
1387
+ return source.includes("requestJson") || source.includes("request_json") || source.includes("JSON.parse") || source.includes("JSON.parse(");
1388
+ }
1389
+ /**
1390
+ * Create a new test client
1391
+ *
1392
+ * @param app - Spikard application with routes and handlers
1393
+ */
1394
+ constructor(app) {
1395
+ if (!app || !Array.isArray(app.routes)) {
1396
+ throw new Error("Invalid Spikard app: missing routes array");
1397
+ }
1398
+ this.app = app;
1399
+ const routesJson = JSON.stringify(app.routes);
1400
+ const websocketRoutesJson = JSON.stringify(app.websocketRoutes ?? []);
1401
+ const handlerEntries = Object.entries(app.handlers || {});
1402
+ const handlersMap = Object.fromEntries(
1403
+ handlerEntries.map(([name, handler]) => {
1404
+ const prefersParsedBody = this.looksLikeStringHandler(handler);
1405
+ const nativeHandler = isNativeHandler(handler) || prefersParsedBody ? handler : wrapHandler(handler);
1406
+ if (prefersParsedBody) {
1407
+ nativeHandler.__spikard_raw_body = false;
1408
+ } else if (nativeHandler.__spikard_raw_body === void 0) {
1409
+ nativeHandler.__spikard_raw_body = true;
1410
+ }
1411
+ return [name, nativeHandler];
1412
+ })
1413
+ );
1414
+ const websocketHandlersMap = app.websocketHandlers || {};
1415
+ const config = app.config ?? null;
1416
+ const dependencies = app.dependencies ?? null;
1417
+ const lifecycleHooks = app.getLifecycleHooks?.() ?? null;
1418
+ this.nativeClient = nativeClientFactory(
1419
+ routesJson,
1420
+ websocketRoutesJson,
1421
+ handlersMap,
1422
+ websocketHandlersMap,
1423
+ dependencies,
1424
+ lifecycleHooks,
1425
+ config
1426
+ );
1427
+ }
1428
+ /**
1429
+ * Make a GET request
1430
+ *
1431
+ * @param path - Request path
1432
+ * @param headers - Optional request headers
1433
+ * @returns Response promise
1434
+ */
1435
+ async get(path2, headers) {
1436
+ return this.nativeClient.get(path2, headers || null);
1437
+ }
1438
+ /**
1439
+ * Make a POST request
1440
+ *
1441
+ * @param path - Request path
1442
+ * @param options - Request options
1443
+ * @returns Response promise
1444
+ */
1445
+ async post(path2, options) {
1446
+ return this.nativeClient.post(path2, this.buildHeaders(options), this.buildBody(options));
1447
+ }
1448
+ /**
1449
+ * Make a PUT request
1450
+ *
1451
+ * @param path - Request path
1452
+ * @param options - Request options
1453
+ * @returns Response promise
1454
+ */
1455
+ async put(path2, options) {
1456
+ return this.nativeClient.put(path2, this.buildHeaders(options), this.buildBody(options));
1457
+ }
1458
+ /**
1459
+ * Make a DELETE request
1460
+ *
1461
+ * @param path - Request path
1462
+ * @param headers - Optional request headers
1463
+ * @returns Response promise
1464
+ */
1465
+ async delete(path2, headers) {
1466
+ return this.nativeClient.delete(path2, headers || null);
1467
+ }
1468
+ /**
1469
+ * Make a PATCH request
1470
+ *
1471
+ * @param path - Request path
1472
+ * @param options - Request options
1473
+ * @returns Response promise
1474
+ */
1475
+ async patch(path2, options) {
1476
+ return this.nativeClient.patch(path2, this.buildHeaders(options), this.buildBody(options));
1477
+ }
1478
+ /**
1479
+ * Make a HEAD request
1480
+ *
1481
+ * @param path - Request path
1482
+ * @param headers - Optional request headers
1483
+ * @returns Response promise
1484
+ */
1485
+ async head(path2, headers) {
1486
+ return this.nativeClient.head(path2, headers || null);
1487
+ }
1488
+ /**
1489
+ * Make an OPTIONS request
1490
+ *
1491
+ * @param path - Request path
1492
+ * @param options - Request options
1493
+ * @returns Response promise
1494
+ */
1495
+ async options(path2, options) {
1496
+ return this.nativeClient.options(path2, this.buildHeaders(options));
1497
+ }
1498
+ /**
1499
+ * Make a TRACE request
1500
+ *
1501
+ * @param path - Request path
1502
+ * @param headers - Optional request headers
1503
+ * @returns Response promise
1504
+ */
1505
+ async trace(path2, options) {
1506
+ return this.nativeClient.trace(path2, this.buildHeaders(options));
1507
+ }
1508
+ buildHeaders(options) {
1509
+ if (!options?.headers || Object.keys(options.headers).length === 0) {
1510
+ return null;
1511
+ }
1512
+ return options.headers;
1513
+ }
1514
+ buildBody(options) {
1515
+ if (!options) {
1516
+ return null;
1517
+ }
1518
+ if (options.multipart) {
1519
+ return {
1520
+ __spikard_multipart__: {
1521
+ fields: options.multipart.fields ?? {},
1522
+ files: options.multipart.files ?? []
1523
+ }
1524
+ };
1525
+ }
1526
+ if (options.form) {
1527
+ return {
1528
+ __spikard_form__: options.form
1529
+ };
1530
+ }
1531
+ if ("json" in options) {
1532
+ return options.json ?? null;
1533
+ }
1534
+ return null;
1535
+ }
1536
+ /**
1537
+ * Connect to a WebSocket endpoint
1538
+ *
1539
+ * Uses the native test client to create an in-memory WebSocket connection.
1540
+ *
1541
+ * @param path - WebSocket path
1542
+ * @returns WebSocket test connection
1543
+ */
1544
+ async websocketConnect(path2) {
1545
+ const handlerName = this.app.websocketRoutes?.find((r) => r.path === path2)?.handler_name;
1546
+ const handlerEntry = handlerName ? this.app.websocketHandlers?.[handlerName] : void 0;
1547
+ const handler = handlerEntry && typeof handlerEntry.handleMessage === "function" ? handlerEntry.handleMessage : null;
1548
+ if (handler) {
1549
+ const mock = new MockWebSocketConnection(async (msg) => handler(msg));
1550
+ return mock;
1551
+ }
1552
+ const routeMatch = this.app.routes.find((r) => r.path === path2);
1553
+ if (routeMatch) {
1554
+ const handlerFn = this.app.handlers?.[routeMatch.handler_name];
1555
+ if (handlerFn) {
1556
+ const mock = new MockWebSocketConnection(async (msg) => {
1557
+ const payload = typeof msg === "string" ? msg : JSON.stringify(msg);
1558
+ const result = await handlerFn(payload);
1559
+ if (typeof result === "string") {
1560
+ try {
1561
+ return JSON.parse(result);
1562
+ } catch {
1563
+ return result;
1564
+ }
1565
+ }
1566
+ return result;
1567
+ });
1568
+ return mock;
1569
+ }
1570
+ }
1571
+ return this.nativeClient.websocket(path2);
1572
+ }
1573
+ /**
1574
+ * Send a GraphQL query/mutation
1575
+ *
1576
+ * Convenience method for sending GraphQL queries and mutations.
1577
+ *
1578
+ * @param query - GraphQL query string
1579
+ * @param variables - Optional GraphQL variables object
1580
+ * @param operationName - Optional GraphQL operation name
1581
+ * @returns Response promise
1582
+ *
1583
+ * @example
1584
+ * ```typescript
1585
+ * const response = await client.graphql('query { user(id: "1") { id name } }');
1586
+ * const user = response.graphqlData().user;
1587
+ * ```
1588
+ */
1589
+ async graphql(query, variables, operationName) {
1590
+ const json = { query };
1591
+ if (variables !== null && variables !== void 0) {
1592
+ json.variables = variables;
1593
+ }
1594
+ if (operationName !== null && operationName !== void 0) {
1595
+ json.operationName = operationName;
1596
+ }
1597
+ return this.post("/graphql", { json });
1598
+ }
1599
+ /**
1600
+ * Send a GraphQL query and get HTTP status separately
1601
+ *
1602
+ * Returns status information alongside the response for cases where
1603
+ * you need both the HTTP status and the full response details.
1604
+ *
1605
+ * @param query - GraphQL query string
1606
+ * @param variables - Optional GraphQL variables object
1607
+ * @param operationName - Optional GraphQL operation name
1608
+ * @returns Promise with status and response details
1609
+ */
1610
+ async graphqlWithStatus(query, variables, operationName) {
1611
+ const response = await this.graphql(query, variables, operationName);
1612
+ return {
1613
+ status: response.statusCode,
1614
+ statusCode: response.statusCode,
1615
+ headers: JSON.stringify(response.headers()),
1616
+ bodyText: response.text()
1617
+ };
1618
+ }
1619
+ /**
1620
+ * Cleanup resources when test client is done
1621
+ */
1622
+ async cleanup() {
1623
+ }
1624
+ };
1625
+ export {
1626
+ GrpcError,
1627
+ GrpcStatusCode,
1628
+ Spikard,
1629
+ StreamingResponse,
1630
+ TestClient,
1631
+ UploadFile,
1632
+ background_exports as background,
1633
+ createServiceHandler,
1634
+ createUnaryHandler,
1635
+ del,
1636
+ get,
1637
+ patch,
1638
+ post,
1639
+ put,
1640
+ route,
1641
+ runServer,
1642
+ wrapBodyHandler,
1643
+ wrapHandler
1644
+ };
1645
+ //# sourceMappingURL=index.mjs.map