@bquery/bquery 1.10.0 → 1.11.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.
Files changed (155) hide show
  1. package/README.md +91 -65
  2. package/dist/{a11y-DG2i4iZN.js → a11y-DgUQ8-fI.js} +1 -1
  3. package/dist/{a11y-DG2i4iZN.js.map → a11y-DgUQ8-fI.js.map} +1 -1
  4. package/dist/a11y.es.mjs +1 -1
  5. package/dist/{component-DRotf1hl.js → component-D8ydhe58.js} +2 -2
  6. package/dist/{component-DRotf1hl.js.map → component-D8ydhe58.js.map} +1 -1
  7. package/dist/component.es.mjs +1 -1
  8. package/dist/concurrency-BU1wPEsZ.js.map +1 -1
  9. package/dist/{constraints-CqjhmpZC.js → constraints-Dlbx_m1b.js} +1 -1
  10. package/dist/{constraints-CqjhmpZC.js.map → constraints-Dlbx_m1b.js.map} +1 -1
  11. package/dist/{core-EMYSLzaT.js → core-tOP6QOrY.js} +2 -2
  12. package/dist/{core-EMYSLzaT.js.map → core-tOP6QOrY.js.map} +1 -1
  13. package/dist/core.es.mjs +1 -1
  14. package/dist/{custom-directives-BjFzFhuf.js → custom-directives-5DlKqvd2.js} +1 -1
  15. package/dist/{custom-directives-BjFzFhuf.js.map → custom-directives-5DlKqvd2.js.map} +1 -1
  16. package/dist/{devtools-C5FExMwv.js → devtools-QosAqo0T.js} +2 -2
  17. package/dist/{devtools-C5FExMwv.js.map → devtools-QosAqo0T.js.map} +1 -1
  18. package/dist/devtools.es.mjs +1 -1
  19. package/dist/{dnd-BAqzPlSo.js → dnd-d2OU4len.js} +1 -1
  20. package/dist/{dnd-BAqzPlSo.js.map → dnd-d2OU4len.js.map} +1 -1
  21. package/dist/dnd.es.mjs +1 -1
  22. package/dist/{forms-Dx1Scvh0.js → forms-BLx4ZzT7.js} +1 -1
  23. package/dist/{forms-Dx1Scvh0.js.map → forms-BLx4ZzT7.js.map} +1 -1
  24. package/dist/forms.es.mjs +1 -1
  25. package/dist/full.d.ts +4 -2
  26. package/dist/full.d.ts.map +1 -1
  27. package/dist/full.es.mjs +258 -219
  28. package/dist/full.iife.js +41 -37
  29. package/dist/full.iife.js.map +1 -1
  30. package/dist/full.umd.js +41 -37
  31. package/dist/full.umd.js.map +1 -1
  32. package/dist/{i18n-Cazyk9RD.js → i18n--p7PM-9r.js} +1 -1
  33. package/dist/{i18n-Cazyk9RD.js.map → i18n--p7PM-9r.js.map} +1 -1
  34. package/dist/i18n.es.mjs +1 -1
  35. package/dist/index.d.ts +3 -2
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.es.mjs +291 -252
  38. package/dist/match-CrZRVC4z.js +174 -0
  39. package/dist/match-CrZRVC4z.js.map +1 -0
  40. package/dist/{media-dAKIGPk3.js → media-gjbWNq50.js} +1 -1
  41. package/dist/{media-dAKIGPk3.js.map → media-gjbWNq50.js.map} +1 -1
  42. package/dist/media.es.mjs +1 -1
  43. package/dist/motion-BBMso9Ir.js.map +1 -1
  44. package/dist/{mount-C8O2vXkQ.js → mount-0A9qtcRJ.js} +3 -3
  45. package/dist/{mount-C8O2vXkQ.js.map → mount-0A9qtcRJ.js.map} +1 -1
  46. package/dist/platform-BPHIXbw8.js.map +1 -1
  47. package/dist/{plugin-DjTqWg-P.js → plugin-SZEirbwq.js} +2 -2
  48. package/dist/{plugin-DjTqWg-P.js.map → plugin-SZEirbwq.js.map} +1 -1
  49. package/dist/plugin.es.mjs +1 -1
  50. package/dist/reactive-BAd2hfl8.js.map +1 -1
  51. package/dist/{registry-Cr6VH8CR.js → registry-jpUQHf4E.js} +1 -1
  52. package/dist/{registry-Cr6VH8CR.js.map → registry-jpUQHf4E.js.map} +1 -1
  53. package/dist/router-C4weu0QL.js +333 -0
  54. package/dist/router-C4weu0QL.js.map +1 -0
  55. package/dist/router.es.mjs +1 -1
  56. package/dist/{sanitize-B1V4JswB.js → sanitize-DOMkRO9G.js} +12 -7
  57. package/dist/{sanitize-B1V4JswB.js.map → sanitize-DOMkRO9G.js.map} +1 -1
  58. package/dist/security.es.mjs +1 -1
  59. package/dist/server/create-server.d.ts +25 -0
  60. package/dist/server/create-server.d.ts.map +1 -0
  61. package/dist/server/index.d.ts +11 -0
  62. package/dist/server/index.d.ts.map +1 -0
  63. package/dist/server/types.d.ts +396 -0
  64. package/dist/server/types.d.ts.map +1 -0
  65. package/dist/server-QdyKtCS1.js +349 -0
  66. package/dist/server-QdyKtCS1.js.map +1 -0
  67. package/dist/server.es.mjs +6 -0
  68. package/dist/ssr/adapters.d.ts +74 -0
  69. package/dist/ssr/adapters.d.ts.map +1 -0
  70. package/dist/ssr/async.d.ts +40 -0
  71. package/dist/ssr/async.d.ts.map +1 -0
  72. package/dist/ssr/config.d.ts +60 -0
  73. package/dist/ssr/config.d.ts.map +1 -0
  74. package/dist/ssr/context.d.ts +73 -0
  75. package/dist/ssr/context.d.ts.map +1 -0
  76. package/dist/ssr/defer-brand.d.ts +5 -0
  77. package/dist/ssr/defer-brand.d.ts.map +1 -0
  78. package/dist/ssr/escape.d.ts +17 -0
  79. package/dist/ssr/escape.d.ts.map +1 -0
  80. package/dist/ssr/expression.d.ts +44 -0
  81. package/dist/ssr/expression.d.ts.map +1 -0
  82. package/dist/ssr/hash.d.ts +39 -0
  83. package/dist/ssr/hash.d.ts.map +1 -0
  84. package/dist/ssr/head.d.ts +102 -0
  85. package/dist/ssr/head.d.ts.map +1 -0
  86. package/dist/ssr/html-parser.d.ts +58 -0
  87. package/dist/ssr/html-parser.d.ts.map +1 -0
  88. package/dist/ssr/index.d.ts +49 -43
  89. package/dist/ssr/index.d.ts.map +1 -1
  90. package/dist/ssr/mismatch.d.ts +60 -0
  91. package/dist/ssr/mismatch.d.ts.map +1 -0
  92. package/dist/ssr/render-async.d.ts +84 -0
  93. package/dist/ssr/render-async.d.ts.map +1 -0
  94. package/dist/ssr/render.d.ts.map +1 -1
  95. package/dist/ssr/renderer.d.ts +25 -0
  96. package/dist/ssr/renderer.d.ts.map +1 -0
  97. package/dist/ssr/resumability.d.ts +65 -0
  98. package/dist/ssr/resumability.d.ts.map +1 -0
  99. package/dist/ssr/router-bridge.d.ts +101 -0
  100. package/dist/ssr/router-bridge.d.ts.map +1 -0
  101. package/dist/ssr/runtime.d.ts +63 -0
  102. package/dist/ssr/runtime.d.ts.map +1 -0
  103. package/dist/ssr/serialize.d.ts.map +1 -1
  104. package/dist/ssr/store-snapshot.d.ts +87 -0
  105. package/dist/ssr/store-snapshot.d.ts.map +1 -0
  106. package/dist/ssr/strategies.d.ts +43 -0
  107. package/dist/ssr/strategies.d.ts.map +1 -0
  108. package/dist/ssr/suspense.d.ts +47 -0
  109. package/dist/ssr/suspense.d.ts.map +1 -0
  110. package/dist/ssr/types.d.ts +17 -0
  111. package/dist/ssr/types.d.ts.map +1 -1
  112. package/dist/ssr-Bt6BQA3J.js +2127 -0
  113. package/dist/ssr-Bt6BQA3J.js.map +1 -0
  114. package/dist/ssr.es.mjs +42 -7
  115. package/dist/{store-CjmEeX9-.js → store-DnXuu6Li.js} +2 -2
  116. package/dist/{store-CjmEeX9-.js.map → store-DnXuu6Li.js.map} +1 -1
  117. package/dist/store.es.mjs +2 -2
  118. package/dist/storybook.es.mjs +1 -1
  119. package/dist/{testing-TdfaL7VE.js → testing-CeMUwrRD.js} +2 -2
  120. package/dist/{testing-TdfaL7VE.js.map → testing-CeMUwrRD.js.map} +1 -1
  121. package/dist/testing.es.mjs +1 -1
  122. package/dist/view.es.mjs +1 -1
  123. package/package.json +19 -14
  124. package/src/full.ts +99 -0
  125. package/src/index.ts +5 -2
  126. package/src/server/create-server.ts +754 -0
  127. package/src/server/index.ts +33 -0
  128. package/src/server/types.ts +490 -0
  129. package/src/ssr/adapters.ts +330 -0
  130. package/src/ssr/async.ts +125 -0
  131. package/src/ssr/config.ts +86 -0
  132. package/src/ssr/context.ts +245 -0
  133. package/src/ssr/defer-brand.ts +3 -0
  134. package/src/ssr/escape.ts +25 -0
  135. package/src/ssr/expression.ts +669 -0
  136. package/src/ssr/hash.ts +71 -0
  137. package/src/ssr/head.ts +240 -0
  138. package/src/ssr/html-parser.ts +387 -0
  139. package/src/ssr/index.ts +136 -43
  140. package/src/ssr/mismatch.ts +110 -0
  141. package/src/ssr/render-async.ts +286 -0
  142. package/src/ssr/render.ts +130 -59
  143. package/src/ssr/renderer.ts +453 -0
  144. package/src/ssr/resumability.ts +142 -0
  145. package/src/ssr/router-bridge.ts +177 -0
  146. package/src/ssr/runtime.ts +131 -0
  147. package/src/ssr/serialize.ts +1 -27
  148. package/src/ssr/store-snapshot.ts +209 -0
  149. package/src/ssr/strategies.ts +245 -0
  150. package/src/ssr/suspense.ts +504 -0
  151. package/src/ssr/types.ts +18 -0
  152. package/dist/router-CCepRMpC.js +0 -493
  153. package/dist/router-CCepRMpC.js.map +0 -1
  154. package/dist/ssr-D-1IPcfw.js +0 -248
  155. package/dist/ssr-D-1IPcfw.js.map +0 -1
@@ -0,0 +1,754 @@
1
+ import { isPrototypePollutionKey } from '../core/utils/object';
2
+ import { sanitizeHtml } from '../security/index';
3
+ import { renderToString, serializeStoreState } from '../ssr/index';
4
+ import type {
5
+ CreateServerOptions,
6
+ ServerApp,
7
+ ServerContext,
8
+ ServerHandler,
9
+ ServerHtmlResponseInit,
10
+ ServerMiddleware,
11
+ ServerNext,
12
+ ServerQuery,
13
+ ServerRenderResponseOptions,
14
+ ServerResult,
15
+ ServerRequestInit,
16
+ ServerResponseInit,
17
+ ServerRoute,
18
+ ServerWebSocketConnection,
19
+ ServerWebSocketHandlerSet,
20
+ ServerWebSocketMiddleware,
21
+ ServerWebSocketNext,
22
+ ServerWebSocketPeer,
23
+ ServerWebSocketRouteHandler,
24
+ ServerWebSocketSession,
25
+ } from './types';
26
+
27
+ interface CompiledRoute {
28
+ handler: ServerHandler;
29
+ methods: Set<string> | null;
30
+ middlewares: ServerMiddleware[];
31
+ paramNames: string[];
32
+ path: string;
33
+ pattern: RegExp;
34
+ }
35
+
36
+ type CompiledWebSocketRoute = Omit<CompiledRoute, 'handler' | 'middlewares'> & {
37
+ handler: ServerWebSocketRouteHandler<unknown>;
38
+ middlewares: ServerWebSocketMiddleware[];
39
+ };
40
+
41
+ type PipelineHandler = (context: ServerContext, next: ServerNext) => Response | Promise<Response>;
42
+ type WebSocketPipelineHandler = (
43
+ context: ServerContext,
44
+ next: ServerWebSocketNext
45
+ ) => ServerResult | Promise<ServerResult>;
46
+
47
+ const DEFAULT_BASE_URL = 'http://localhost';
48
+ const JSON_ESCAPE_LOOKUP: Record<string, string> = {
49
+ '<': '\\u003C',
50
+ '>': '\\u003E',
51
+ '&': '\\u0026',
52
+ '\u2028': '\\u2028',
53
+ '\u2029': '\\u2029',
54
+ };
55
+ const JSON_ESCAPE_PATTERN = /[<>&\u2028\u2029]/g;
56
+ const METHOD_ALL = null;
57
+ const WEBSOCKET_PASSTHROUGH_HEADER = 'x-bquery-websocket-passthrough';
58
+
59
+ const escapeRegex = (value: string): string => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
60
+ /**
61
+ * Creates a null-prototype dictionary for request-derived data.
62
+ *
63
+ * Request-controlled keys such as query params and route params must never write
64
+ * into the default object prototype, otherwise names like `__proto__` can trigger
65
+ * prototype-pollution bugs. Using `Object.create(null)` keeps these maps isolated
66
+ * even before higher-level validation runs.
67
+ */
68
+ const createDictionary = <T>(): Record<string, T> => Object.create(null) as Record<string, T>;
69
+
70
+ const normalizePath = (path: string): string => {
71
+ if (!path) {
72
+ throw new Error(`route path must be a non-empty string; received ${String(path)}`);
73
+ }
74
+
75
+ if (path === '*' || path === '/*') {
76
+ return '/*';
77
+ }
78
+
79
+ const withLeadingSlash = path.startsWith('/') ? path : `/${path}`;
80
+ if (withLeadingSlash.length > 1 && withLeadingSlash.endsWith('/')) {
81
+ return withLeadingSlash.slice(0, -1);
82
+ }
83
+
84
+ return withLeadingSlash;
85
+ };
86
+
87
+ const compileRoutePath = (path: string): Pick<CompiledRoute, 'paramNames' | 'path' | 'pattern'> => {
88
+ const normalizedPath = normalizePath(path);
89
+
90
+ if (normalizedPath === '/*') {
91
+ return { path: normalizedPath, paramNames: [], pattern: /^\/.*$/ };
92
+ }
93
+
94
+ const segments = normalizedPath.split('/').filter(Boolean);
95
+ if (segments.length === 0) {
96
+ return { path: normalizedPath, paramNames: [], pattern: /^\/$/ };
97
+ }
98
+
99
+ const paramNames: string[] = [];
100
+ let source = '^';
101
+
102
+ for (const [index, segment] of segments.entries()) {
103
+ source += '/';
104
+ if (segment === '*') {
105
+ if (index !== segments.length - 1) {
106
+ throw new Error(`invalid route path: "*" must be the final segment in "${normalizedPath}"`);
107
+ }
108
+ source += '.*';
109
+ break;
110
+ }
111
+
112
+ if (segment.startsWith(':')) {
113
+ const paramName = segment.slice(1);
114
+ if (!/^[A-Za-z_$][\w$]*$/.test(paramName)) {
115
+ throw new Error(
116
+ `invalid route param name: ${paramName} - must start with a letter, $, or _ and contain only word characters`
117
+ );
118
+ }
119
+ if (isPrototypePollutionKey(paramName)) {
120
+ throw new Error(`invalid route param name: ${paramName} - reserved for object safety`);
121
+ }
122
+ paramNames.push(paramName);
123
+ source += '([^/]+)';
124
+ continue;
125
+ }
126
+
127
+ source += escapeRegex(segment);
128
+ }
129
+
130
+ source += '/?$';
131
+ return { path: normalizedPath, paramNames, pattern: new RegExp(source) };
132
+ };
133
+
134
+ const normalizeMethods = (method?: string | string[]): Set<string> | null => {
135
+ if (typeof method === 'undefined') {
136
+ return METHOD_ALL;
137
+ }
138
+
139
+ const values = Array.isArray(method) ? method : [method];
140
+ if (values.length === 0) {
141
+ throw new Error('route method must be specified - received empty array');
142
+ }
143
+
144
+ const normalizedMethods = new Set(
145
+ values.map((value) => value.trim().toUpperCase()).filter(Boolean)
146
+ );
147
+ if (normalizedMethods.size === 0) {
148
+ throw new Error(
149
+ `route method must include at least one non-empty method string; received ${JSON.stringify(method)}`
150
+ );
151
+ }
152
+
153
+ return normalizedMethods;
154
+ };
155
+
156
+ const parseQuery = (url: URL): ServerQuery => {
157
+ const query = createDictionary<string | string[] | undefined>() as ServerQuery;
158
+
159
+ for (const [key, value] of url.searchParams.entries()) {
160
+ if (isPrototypePollutionKey(key)) {
161
+ continue;
162
+ }
163
+ const current = query[key];
164
+ if (typeof current === 'undefined') {
165
+ query[key] = value;
166
+ } else if (Array.isArray(current)) {
167
+ current.push(value);
168
+ } else {
169
+ query[key] = [current, value];
170
+ }
171
+ }
172
+
173
+ return query;
174
+ };
175
+
176
+ const normalizeUrl = (value: string | URL, baseUrl: string): URL => {
177
+ return value instanceof URL ? new URL(value.toString()) : new URL(value, baseUrl);
178
+ };
179
+
180
+ const normalizeRequest = (
181
+ input: Request | string | URL | ServerRequestInit,
182
+ baseUrl: string
183
+ ): Request => {
184
+ if (input instanceof Request) {
185
+ return input;
186
+ }
187
+
188
+ if (typeof input === 'string' || input instanceof URL) {
189
+ return new Request(normalizeUrl(input, baseUrl).toString());
190
+ }
191
+
192
+ const { url, method = 'GET', headers, body = null } = input;
193
+ return new Request(normalizeUrl(url, baseUrl).toString(), { method, headers, body });
194
+ };
195
+
196
+ const normalizeWebSocketProtocols = (protocols?: string | string[]): string[] => {
197
+ if (typeof protocols === 'undefined') {
198
+ return [];
199
+ }
200
+
201
+ const values = Array.isArray(protocols) ? protocols : [protocols];
202
+ return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
203
+ };
204
+
205
+ const defaultDeserialize = <TReceive>(event: MessageEvent): TReceive => {
206
+ const raw = event.data;
207
+ if (typeof raw === 'string') {
208
+ try {
209
+ return JSON.parse(raw) as TReceive;
210
+ } catch {
211
+ // Match `useWebSocket()` in `src/reactive/websocket.ts`: malformed JSON
212
+ // payloads fall back to the original string instead of throwing.
213
+ return raw as TReceive;
214
+ }
215
+ }
216
+
217
+ return raw as TReceive;
218
+ };
219
+
220
+ const escapeJsonString = (value: string): string =>
221
+ value.replace(JSON_ESCAPE_PATTERN, (match) => JSON_ESCAPE_LOOKUP[match]);
222
+
223
+ const createHeaders = (headers?: HeadersInit): Headers => new Headers(headers);
224
+
225
+ const withContentType = (headers: Headers, contentType: string): Headers => {
226
+ if (!headers.has('content-type')) {
227
+ headers.set('content-type', contentType);
228
+ }
229
+ return headers;
230
+ };
231
+
232
+ const response = (body?: BodyInit | null, init: ServerResponseInit = {}): Response => {
233
+ const { headers, ...rest } = init;
234
+ return new Response(body, { ...rest, headers: createHeaders(headers) });
235
+ };
236
+
237
+ const text = (body: string, init: ServerResponseInit = {}): Response => {
238
+ const headers = withContentType(createHeaders(init.headers), 'text/plain; charset=utf-8');
239
+ return response(body, { ...init, headers });
240
+ };
241
+
242
+ const html = (body: string, init: ServerHtmlResponseInit = {}): Response => {
243
+ const { trusted = false, ...rest } = init;
244
+ const headers = withContentType(createHeaders(rest.headers), 'text/html; charset=utf-8');
245
+ return response(trusted ? body : sanitizeHtml(body), { ...rest, headers });
246
+ };
247
+
248
+ const json = (data: unknown, init: ServerResponseInit = {}): Response => {
249
+ const headers = withContentType(createHeaders(init.headers), 'application/json; charset=utf-8');
250
+ let serialized: string;
251
+ try {
252
+ serialized = JSON.stringify(data) ?? 'null';
253
+ } catch {
254
+ serialized = 'null';
255
+ }
256
+ return response(escapeJsonString(serialized), { ...init, headers });
257
+ };
258
+
259
+ const redirect = (location: string | URL, status = 302): Response => {
260
+ const headers = createHeaders({ location: location.toString() });
261
+ return response(null, { headers, status });
262
+ };
263
+
264
+ const render = (
265
+ template: string,
266
+ data: Parameters<typeof renderToString>[1],
267
+ options: ServerRenderResponseOptions = {}
268
+ ): Response => {
269
+ const { includeStoreState = false, status = 200, headers, ...renderOptions } = options;
270
+ const result = renderToString(template, data, { ...renderOptions, includeStoreState: false });
271
+ const storeState = includeStoreState
272
+ ? serializeStoreState({
273
+ storeIds: Array.isArray(includeStoreState) ? includeStoreState : undefined,
274
+ }).scriptTag
275
+ : '';
276
+ const body = `${result.html}${storeState}`;
277
+ return html(body, { headers, status, trusted: true });
278
+ };
279
+
280
+ /**
281
+ * Returns `true` when the request is a WebSocket upgrade handshake.
282
+ */
283
+ export const isWebSocketRequest = (request: Request): boolean => {
284
+ if (request.method.toUpperCase() !== 'GET') {
285
+ return false;
286
+ }
287
+
288
+ const upgrade = request.headers.get('upgrade');
289
+ if (typeof upgrade !== 'string' || upgrade.trim().toLowerCase() !== 'websocket') {
290
+ return false;
291
+ }
292
+
293
+ const connection = request.headers.get('connection');
294
+ if (typeof connection !== 'string') {
295
+ return false;
296
+ }
297
+
298
+ if (!connection.split(',').some((part) => part.trim().toLowerCase() === 'upgrade')) {
299
+ return false;
300
+ }
301
+
302
+ const version = request.headers.get('sec-websocket-version');
303
+ if (version?.trim() !== '13') {
304
+ return false;
305
+ }
306
+
307
+ const key = request.headers.get('sec-websocket-key')?.trim();
308
+ return typeof key === 'string' && /^[A-Za-z0-9+/]{22}==$/.test(key);
309
+ };
310
+
311
+ /**
312
+ * Type guard for values returned by `handleWebSocket()`.
313
+ */
314
+ export const isServerWebSocketSession = (value: unknown): value is ServerWebSocketSession => {
315
+ if (typeof value !== 'object' || value === null || value instanceof Response) {
316
+ return false;
317
+ }
318
+
319
+ const candidate = value as Record<string, unknown>;
320
+ return (
321
+ typeof candidate.open === 'function' &&
322
+ typeof candidate.message === 'function' &&
323
+ typeof candidate.close === 'function' &&
324
+ typeof candidate.error === 'function'
325
+ );
326
+ };
327
+
328
+ const createWebSocketConnectionFactory = () => {
329
+ const cache = new WeakMap<object, ServerWebSocketConnection>();
330
+
331
+ return (socket: ServerWebSocketPeer): ServerWebSocketConnection => {
332
+ const existing = cache.get(socket);
333
+ if (existing) {
334
+ return existing;
335
+ }
336
+
337
+ const connection: ServerWebSocketConnection = {
338
+ get protocol() {
339
+ return socket.protocol;
340
+ },
341
+ get readyState() {
342
+ return socket.readyState;
343
+ },
344
+ get url() {
345
+ return socket.url;
346
+ },
347
+ send(data) {
348
+ socket.send(data);
349
+ },
350
+ sendJson(data) {
351
+ const payload = JSON.stringify(data);
352
+ if (typeof payload !== 'string') {
353
+ throw new TypeError('socket.sendJson() does not support undefined values');
354
+ }
355
+ socket.send(payload);
356
+ },
357
+ close(code, reason) {
358
+ socket.close(code, reason);
359
+ },
360
+ };
361
+
362
+ cache.set(socket, connection);
363
+ return connection;
364
+ };
365
+ };
366
+
367
+ const createWebSocketSession = <TReceive>(
368
+ context: ServerContext,
369
+ handlers: ServerWebSocketHandlerSet<TReceive>
370
+ ): ServerWebSocketSession => {
371
+ const toConnection = createWebSocketConnectionFactory();
372
+ const deserialize = handlers.deserialize ?? defaultDeserialize<TReceive>;
373
+
374
+ return {
375
+ context,
376
+ protocols: normalizeWebSocketProtocols(handlers.protocols),
377
+ headers: handlers.headers,
378
+ async open(socket) {
379
+ if (handlers.onOpen) {
380
+ await handlers.onOpen(toConnection(socket), context);
381
+ }
382
+ },
383
+ async message(socket, event) {
384
+ if (handlers.onMessage) {
385
+ await handlers.onMessage(deserialize(event), toConnection(socket), context, event);
386
+ }
387
+ },
388
+ async close(socket, event) {
389
+ if (handlers.onClose) {
390
+ await handlers.onClose(event, toConnection(socket), context);
391
+ }
392
+ },
393
+ async error(socket, event) {
394
+ if (handlers.onError) {
395
+ await handlers.onError(event, toConnection(socket), context);
396
+ }
397
+ },
398
+ };
399
+ };
400
+
401
+ const createWebSocketPassthroughResponse = (): Response => {
402
+ const headers = createHeaders({
403
+ [WEBSOCKET_PASSTHROUGH_HEADER]: '1',
404
+ });
405
+ return response(null, { headers, status: 204 });
406
+ };
407
+
408
+ const isWebSocketPassthroughResponse = (value: Response): boolean => {
409
+ return value.headers.get(WEBSOCKET_PASSTHROUGH_HEADER) === '1';
410
+ };
411
+
412
+ const matchRoute = (
413
+ route: Pick<CompiledRoute, 'methods' | 'paramNames' | 'pattern'>,
414
+ method: string,
415
+ path: string
416
+ ): Record<string, string> | null => {
417
+ if (route.methods && !route.methods.has(method)) {
418
+ return null;
419
+ }
420
+
421
+ const match = route.pattern.exec(path);
422
+ if (!match) {
423
+ return null;
424
+ }
425
+
426
+ const params = createDictionary<string>();
427
+ for (const [index, paramName] of route.paramNames.entries()) {
428
+ try {
429
+ params[paramName] = decodeURIComponent(match[index + 1] ?? '');
430
+ } catch (error) {
431
+ if (error instanceof URIError) {
432
+ return null;
433
+ }
434
+ throw error;
435
+ }
436
+ }
437
+
438
+ return params;
439
+ };
440
+
441
+ const resolveMatchingRoute = <TRoute extends CompiledRoute | CompiledWebSocketRoute>(
442
+ routes: TRoute[],
443
+ method: string,
444
+ path: string,
445
+ context: ServerContext
446
+ ): TRoute | null => {
447
+ for (const candidate of routes) {
448
+ const params = matchRoute(candidate, method, path);
449
+ if (!params) {
450
+ continue;
451
+ }
452
+ context.params = params;
453
+ return candidate;
454
+ }
455
+
456
+ return null;
457
+ };
458
+
459
+ const runPipeline = async (
460
+ context: ServerContext,
461
+ handlers: PipelineHandler[],
462
+ terminal: ServerNext
463
+ ): Promise<Response> => {
464
+ const dispatch = async (index: number): Promise<Response> => {
465
+ const current = handlers[index];
466
+ if (!current) {
467
+ return terminal();
468
+ }
469
+
470
+ let advanced = false;
471
+ return await current(context, async () => {
472
+ if (advanced) {
473
+ throw new Error(
474
+ 'middleware next() called multiple times - if a middleware calls next(), it may only do so once'
475
+ );
476
+ }
477
+ advanced = true;
478
+ return await dispatch(index + 1);
479
+ });
480
+ };
481
+
482
+ return await dispatch(0);
483
+ };
484
+
485
+ const runWebSocketPipeline = async (
486
+ context: ServerContext,
487
+ handlers: WebSocketPipelineHandler[],
488
+ terminal: ServerWebSocketNext
489
+ ): Promise<ServerResult> => {
490
+ const dispatch = async (index: number): Promise<ServerResult> => {
491
+ const current = handlers[index];
492
+ if (!current) {
493
+ return terminal();
494
+ }
495
+
496
+ let advanced = false;
497
+ return await current(context, async () => {
498
+ if (advanced) {
499
+ throw new Error(
500
+ 'middleware next() called multiple times - if a middleware calls next(), it may only do so once'
501
+ );
502
+ }
503
+ advanced = true;
504
+ return await dispatch(index + 1);
505
+ });
506
+ };
507
+
508
+ return await dispatch(0);
509
+ };
510
+
511
+ const adaptHttpMiddlewareToWebSocket = (middleware: ServerMiddleware): WebSocketPipelineHandler => {
512
+ return async (context, next) => {
513
+ let downstream: ServerResult = null;
514
+ let downstreamResponse: Response | null = null;
515
+ const middlewareResponse = await middleware(context, async () => {
516
+ downstream = await next();
517
+ if (downstream instanceof Response) {
518
+ downstreamResponse = downstream;
519
+ return downstream;
520
+ }
521
+ return createWebSocketPassthroughResponse();
522
+ });
523
+
524
+ if (downstreamResponse) {
525
+ return middlewareResponse;
526
+ }
527
+
528
+ if (middlewareResponse instanceof Response && isWebSocketPassthroughResponse(middlewareResponse)) {
529
+ return downstream;
530
+ }
531
+
532
+ return middlewareResponse;
533
+ };
534
+ };
535
+
536
+ const compileRoute = (route: ServerRoute): CompiledRoute => {
537
+ const compiledPath = compileRoutePath(route.path);
538
+ return {
539
+ handler: route.handler,
540
+ methods: normalizeMethods(route.method),
541
+ middlewares: route.middlewares ?? [],
542
+ paramNames: compiledPath.paramNames,
543
+ path: compiledPath.path,
544
+ pattern: compiledPath.pattern,
545
+ };
546
+ };
547
+
548
+ /**
549
+ * Create a lightweight, Express-inspired request pipeline for SSR-aware
550
+ * backends without introducing runtime dependencies.
551
+ *
552
+ * @example
553
+ * ```ts
554
+ * import { createServer } from '@bquery/bquery/server';
555
+ *
556
+ * const app = createServer();
557
+ * app.get('/health', (ctx) => ctx.json({ ok: true }));
558
+ *
559
+ * const response = await app.handle('/health');
560
+ * ```
561
+ */
562
+ export const createServer = (options: CreateServerOptions = {}): ServerApp => {
563
+ const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
564
+ const middlewares = [...(options.middlewares ?? [])];
565
+ const routes: CompiledRoute[] = [];
566
+ const webSocketRoutes: CompiledWebSocketRoute[] = [];
567
+
568
+ const notFound =
569
+ options.notFound ??
570
+ ((context: ServerContext) => {
571
+ return context.text('Not Found', { status: 404 });
572
+ });
573
+
574
+ const onError =
575
+ options.onError ??
576
+ ((error: unknown, context: ServerContext) => {
577
+ if (error instanceof Response) {
578
+ return error;
579
+ }
580
+ return context.text('Internal Server Error', { status: 500 });
581
+ });
582
+
583
+ const addRoute = (
584
+ method: string | string[] | undefined,
585
+ path: string,
586
+ handler: ServerHandler,
587
+ routeMiddlewares?: ServerMiddleware[]
588
+ ): ServerApp => {
589
+ routes.push(
590
+ compileRoute({
591
+ handler,
592
+ method,
593
+ middlewares: routeMiddlewares,
594
+ path,
595
+ })
596
+ );
597
+ return app;
598
+ };
599
+
600
+ const addWebSocketRoute = (
601
+ path: string,
602
+ handler: ServerWebSocketRouteHandler<unknown>,
603
+ routeMiddlewares?: ServerWebSocketMiddleware[]
604
+ ): ServerApp => {
605
+ const compiledPath = compileRoutePath(path);
606
+ webSocketRoutes.push({
607
+ handler,
608
+ methods: new Set(['GET']),
609
+ middlewares: routeMiddlewares ?? [],
610
+ paramNames: compiledPath.paramNames,
611
+ path: compiledPath.path,
612
+ pattern: compiledPath.pattern,
613
+ });
614
+ return app;
615
+ };
616
+
617
+ const app: ServerApp = {
618
+ use(middleware) {
619
+ middlewares.push(middleware);
620
+ return app;
621
+ },
622
+
623
+ add(route) {
624
+ routes.push(compileRoute(route));
625
+ return app;
626
+ },
627
+
628
+ get(path, handler, routeMiddlewares) {
629
+ return addRoute('GET', path, handler, routeMiddlewares);
630
+ },
631
+
632
+ post(path, handler, routeMiddlewares) {
633
+ return addRoute('POST', path, handler, routeMiddlewares);
634
+ },
635
+
636
+ put(path, handler, routeMiddlewares) {
637
+ return addRoute('PUT', path, handler, routeMiddlewares);
638
+ },
639
+
640
+ patch(path, handler, routeMiddlewares) {
641
+ return addRoute('PATCH', path, handler, routeMiddlewares);
642
+ },
643
+
644
+ delete(path, handler, routeMiddlewares) {
645
+ return addRoute('DELETE', path, handler, routeMiddlewares);
646
+ },
647
+
648
+ all(path, handler, routeMiddlewares) {
649
+ return addRoute(undefined, path, handler, routeMiddlewares);
650
+ },
651
+
652
+ ws(path, handler, routeMiddlewares) {
653
+ return addWebSocketRoute(path, handler as ServerWebSocketRouteHandler<unknown>, routeMiddlewares);
654
+ },
655
+
656
+ async handle(input) {
657
+ const request = normalizeRequest(input, baseUrl);
658
+ const url = new URL(request.url);
659
+ const method = request.method.toUpperCase();
660
+ const path = normalizePath(url.pathname || '/');
661
+ const query = parseQuery(url);
662
+
663
+ const context: ServerContext = {
664
+ request,
665
+ url,
666
+ method,
667
+ path,
668
+ params: createDictionary<string>(),
669
+ query,
670
+ state: {},
671
+ response,
672
+ text,
673
+ html,
674
+ json,
675
+ redirect,
676
+ render,
677
+ isWebSocketRequest: isWebSocketRequest(request),
678
+ };
679
+
680
+ try {
681
+ const route = resolveMatchingRoute(routes, method, path, context);
682
+
683
+ if (!route) {
684
+ return await notFound(context);
685
+ }
686
+
687
+ const stack: PipelineHandler[] = [
688
+ ...middlewares,
689
+ ...route.middlewares,
690
+ async (ctx) => await route.handler(ctx),
691
+ ];
692
+
693
+ const result = await runPipeline(context, stack, async () => {
694
+ return await notFound(context);
695
+ });
696
+ return result;
697
+ } catch (error) {
698
+ return await onError(error, context);
699
+ }
700
+ },
701
+
702
+ async handleWebSocket(input) {
703
+ const request = normalizeRequest(input, baseUrl);
704
+ const url = new URL(request.url);
705
+ const method = request.method.toUpperCase();
706
+ const path = normalizePath(url.pathname || '/');
707
+ const query = parseQuery(url);
708
+
709
+ const context: ServerContext = {
710
+ request,
711
+ url,
712
+ method,
713
+ path,
714
+ params: createDictionary<string>(),
715
+ query,
716
+ state: {},
717
+ response,
718
+ text,
719
+ html,
720
+ json,
721
+ redirect,
722
+ render,
723
+ isWebSocketRequest: isWebSocketRequest(request),
724
+ };
725
+
726
+ if (!context.isWebSocketRequest) {
727
+ return null;
728
+ }
729
+
730
+ try {
731
+ const route = resolveMatchingRoute(webSocketRoutes, method, path, context);
732
+ if (!route) {
733
+ return null;
734
+ }
735
+
736
+ const stack: WebSocketPipelineHandler[] = [
737
+ ...middlewares.map(adaptHttpMiddlewareToWebSocket),
738
+ ...route.middlewares,
739
+ ];
740
+ return await runWebSocketPipeline(context, stack, async () => {
741
+ const handlers =
742
+ typeof route.handler === 'function'
743
+ ? await route.handler(context)
744
+ : route.handler;
745
+ return createWebSocketSession(context, handlers as ServerWebSocketHandlerSet<unknown>);
746
+ });
747
+ } catch (error) {
748
+ return await onError(error, context);
749
+ }
750
+ },
751
+ };
752
+
753
+ return app;
754
+ };