@lovrozagar/honey 0.1.0 → 0.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lovrozagar/honey",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Type-safe API framework built on Web Standards. Builder-pattern DX, precompiled radix tree in production.",
5
5
  "keywords": [
6
6
  "api",
@@ -11,10 +11,16 @@
11
11
  "type-safe",
12
12
  "web-standards"
13
13
  ],
14
+ "homepage": "https://github.com/lovrozagar/honey#readme",
15
+ "bugs": {
16
+ "url": "https://github.com/lovrozagar/honey/issues"
17
+ },
14
18
  "license": "MIT",
19
+ "author": "lovrozagar",
15
20
  "repository": {
16
21
  "type": "git",
17
- "url": "https://github.com/lovrozagar/honey"
22
+ "url": "git+https://github.com/lovrozagar/honey.git",
23
+ "directory": "packages/core"
18
24
  },
19
25
  "publishConfig": {
20
26
  "access": "public"
package/src/cli.ts CHANGED
@@ -74,7 +74,7 @@ async function loadConfigFromVite(configPath: string): Promise<HoneyVitePluginCo
74
74
  await jiti.import(configPath)
75
75
 
76
76
  /* read stashed config from the same module graph */
77
- const pluginMod = (await jiti.import("honey/plugin")) as Record<string, unknown>
77
+ const pluginMod = (await jiti.import("@lovrozagar/honey/plugin")) as Record<string, unknown>
78
78
  const getter = pluginMod.getLastHoneyConfig as typeof getLastHoneyConfig | undefined
79
79
  return getter?.()
80
80
  }
package/src/index.ts CHANGED
@@ -231,6 +231,15 @@ function walkTreeHandlers(
231
231
  const EMPTY_PARAMS = EMPTY_OBJ as Record<string, string>
232
232
  const EMPTY_MW: RuntimeMiddleware[] = []
233
233
 
234
+ function requestIsWsUpgrade(request: Request): boolean {
235
+ try {
236
+ return request.headers.get("upgrade")?.toLowerCase() === "websocket";
237
+ } catch {
238
+ /* Deno closes the Request after upgradeWebSocket */
239
+ return false;
240
+ }
241
+ }
242
+
234
243
  /** Find first '?' or '#' in url starting from pos */
235
244
  function findSearchOrHash(url: string, pos: number): number {
236
245
  for (let i = pos; i < url.length; i++) {
@@ -262,8 +271,32 @@ type FetchCtx<TEnv> = {
262
271
  request: Request;
263
272
  startTime: number;
264
273
  url: () => URL;
274
+ /** Already known from fetch() before Deno.upgradeWebSocket consumes the request. */
275
+ wsUpgrade?: boolean;
276
+ /** Headers copied before Deno.upgradeWebSocket closes the Request. */
277
+ headerSnap?: Headers;
265
278
  };
266
279
 
280
+ function requestWithHeaders(req: Request, headers: Headers): Request {
281
+ return new Proxy(req, {
282
+ get(target, prop) {
283
+ if (prop === "headers") return headers
284
+ const value = Reflect.get(target, prop, target)
285
+ return typeof value === "function" ? (value as (...args: never[]) => unknown).bind(target) : value
286
+ },
287
+ })
288
+ }
289
+
290
+ function ctxRequest(fc: FetchCtx<unknown>): Request {
291
+ if (!fc.headerSnap) return fc.request
292
+ try {
293
+ void fc.request.headers.get("upgrade")
294
+ return fc.request
295
+ } catch {
296
+ return requestWithHeaders(fc.request, fc.headerSnap)
297
+ }
298
+ }
299
+
267
300
  function defaultErrorFormatter(
268
301
  _error: HoneyError,
269
302
  defaultShape: Record<string, unknown>,
@@ -1778,6 +1811,7 @@ export class Honey<
1778
1811
  return this._doFetch(request, env, executionCtx);
1779
1812
  }
1780
1813
  const isWsUpgrade = request.headers.get("upgrade")?.toLowerCase() === "websocket"
1814
+ const headerSnap = isWsUpgrade ? new Headers(request.headers) : undefined
1781
1815
  const path = this.pathFromRequest(request)
1782
1816
  const canPreUpgrade =
1783
1817
  isWsUpgrade &&
@@ -1785,7 +1819,14 @@ export class Honey<
1785
1819
  !this.trailingSlashRedirects(path) &&
1786
1820
  matchWsRoute(this._root, this.pathAfterPrefix(path)) !== null
1787
1821
  const pre = canPreUpgrade ? this._wsAdapter?.preUpgrade?.(request) : undefined
1788
- const work = this._doFetch(request, env, executionCtx)
1822
+ /* After Deno.upgradeWebSocket the Request is closed. A sync throw here
1823
+ * used to be boxed by async _doFetch; keep 101 returning either way. */
1824
+ let work: Response | Promise<Response>
1825
+ try {
1826
+ work = this._doFetch(request, env, executionCtx, isWsUpgrade === true, headerSnap)
1827
+ } catch (err) {
1828
+ work = Promise.reject(err)
1829
+ }
1789
1830
  if (!pre) return work
1790
1831
  void Promise.resolve(work)
1791
1832
  .then((res) => {
@@ -1842,6 +1883,8 @@ export class Honey<
1842
1883
  request: Request,
1843
1884
  env: TEnv,
1844
1885
  executionCtx?: { waitUntil?: (p: Promise<unknown>) => void },
1886
+ knownWsUpgrade = false,
1887
+ headerSnap?: Headers,
1845
1888
  ): Response | Promise<Response> {
1846
1889
  const startTime = performance.now();
1847
1890
 
@@ -1875,6 +1918,8 @@ export class Honey<
1875
1918
  request,
1876
1919
  startTime,
1877
1920
  url: getUrl,
1921
+ wsUpgrade: knownWsUpgrade,
1922
+ headerSnap,
1878
1923
  };
1879
1924
 
1880
1925
  if (this._telemetry !== null) {
@@ -1913,8 +1958,8 @@ export class Honey<
1913
1958
  }
1914
1959
  }
1915
1960
 
1916
- /* WebSocket route check */
1917
- const isWsUpgrade = request.headers.get("upgrade") === "websocket";
1961
+ /* WebSocket route check — do not re-read headers after Deno.upgradeWebSocket */
1962
+ const isWsUpgrade = knownWsUpgrade || requestIsWsUpgrade(request);
1918
1963
  if (isWsUpgrade) {
1919
1964
  const wsMatch = matchWsRoute(this._root, path);
1920
1965
  if (wsMatch !== null) {
@@ -2085,8 +2130,7 @@ export class Honey<
2085
2130
  return this._handleRealtime(fc, wsMatch, realtimeConfig);
2086
2131
  }
2087
2132
 
2088
- const isUpgrade =
2089
- fc.request.headers.get("upgrade")?.toLowerCase() === "websocket";
2133
+ const isUpgrade = fc.wsUpgrade === true || requestIsWsUpgrade(fc.request);
2090
2134
  if (!isUpgrade) {
2091
2135
  return new Response(null, {
2092
2136
  headers: { upgrade: "websocket" },
@@ -2108,7 +2152,7 @@ export class Honey<
2108
2152
  env: fc.env,
2109
2153
  executionCtx: fc.executionCtx,
2110
2154
  params: wsMatch.params,
2111
- req: fc.request,
2155
+ req: ctxRequest(fc),
2112
2156
  urlFn: fc.url,
2113
2157
  });
2114
2158
  if (this._contextValues) Object.assign(wsCtx, this._contextValues);
@@ -2188,8 +2232,7 @@ export class Honey<
2188
2232
  wsMatch: { handler: WSRouteHandler; params: Record<string, string> },
2189
2233
  config: { handler: RealtimeRouteOpts["handler"]; middlewares?: RealtimeRouteOpts["use"]; reconnectBuffer?: number },
2190
2234
  ): Promise<Response> {
2191
- const isUpgrade =
2192
- fc.request.headers.get("upgrade")?.toLowerCase() === "websocket";
2235
+ const isUpgrade = fc.wsUpgrade === true || requestIsWsUpgrade(fc.request);
2193
2236
  if (!isUpgrade) {
2194
2237
  return new Response(null, {
2195
2238
  headers: { upgrade: "websocket" },
@@ -2217,7 +2260,7 @@ export class Honey<
2217
2260
  env: fc.env,
2218
2261
  executionCtx: fc.executionCtx,
2219
2262
  params: wsMatch.params,
2220
- req: fc.request,
2263
+ req: ctxRequest(fc),
2221
2264
  urlFn: fc.url,
2222
2265
  });
2223
2266
  if (this._contextValues) Object.assign(ctx, this._contextValues);
@@ -121,7 +121,8 @@ export class NodeRequest {
121
121
  get body(): ReadableStream<Uint8Array> | null {
122
122
  if (!this.#hasBody) return null
123
123
  if (this.#webBody === undefined) {
124
- this.#webBody = Readable.toWeb(this.#incoming) as ReadableStream<Uint8Array>
124
+ // Node's web stream and the DOM/Bun ReadableStream brands do not overlap.
125
+ this.#webBody = Readable.toWeb(this.#incoming) as unknown as ReadableStream<Uint8Array>
125
126
  }
126
127
  return this.#webBody
127
128
  }
package/src/node.ts CHANGED
@@ -16,6 +16,11 @@ type ServeOptions<TEnv> = {
16
16
  const BUFFER_BODY_MAX = 256 * 1024
17
17
  const RAW_BODY = Symbol.for("honey.rawBody")
18
18
 
19
+ // Node's `Readable.fromWeb` wants `node:stream/web` streams; DOM/Bun brands do not overlap.
20
+ function asNodeWebStream(stream: ReadableStream<Uint8Array>): import("node:stream/web").ReadableStream {
21
+ return stream as unknown as import("node:stream/web").ReadableStream
22
+ }
23
+
19
24
  function incomingToRequest(req: IncomingMessage): Request {
20
25
  return incomingToNodeRequest(req)
21
26
  }
@@ -79,7 +84,7 @@ async function responseToNode(response: Response, res: ServerResponse): Promise<
79
84
  }
80
85
  res.writeHead(response.status, response.plainHeaders)
81
86
  try {
82
- await pipeline(Readable.fromWeb(stream as import("node:stream/web").ReadableStream), res)
87
+ await pipeline(Readable.fromWeb(asNodeWebStream(stream)), res)
83
88
  } catch {
84
89
  if (!res.destroyed) res.destroy()
85
90
  }
@@ -114,7 +119,7 @@ async function responseToNode(response: Response, res: ServerResponse): Promise<
114
119
 
115
120
  res.writeHead(response.status, collectNodeHeaders(response))
116
121
  try {
117
- await pipeline(Readable.fromWeb(body as import("node:stream/web").ReadableStream), res)
122
+ await pipeline(Readable.fromWeb(asNodeWebStream(body)), res)
118
123
  } catch {
119
124
  if (!res.destroyed) res.destroy()
120
125
  }