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