owebjs 1.4.7 → 1.4.9-dev

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.
Binary file
Binary file
package/avatar.png ADDED
Binary file
package/dist/index.d.ts CHANGED
@@ -40,10 +40,12 @@ declare class _FastifyInstance {
40
40
  }
41
41
  declare class Oweb extends _FastifyInstance {
42
42
  _options: OwebOptions;
43
+ _internalKV: Map<string, any>;
43
44
  private hmrDirectory;
44
45
  private hmrMatchersDirectory;
45
46
  private directory;
46
47
  private matchersDirectory;
48
+ uServer: any;
47
49
  routes: Map<string, any>;
48
50
  constructor(options?: OwebOptions);
49
51
  /**
@@ -51,6 +53,7 @@ declare class Oweb extends _FastifyInstance {
51
53
  * Returns a fastify instance with the Oweb prototype methods
52
54
  */
53
55
  setup(): Promise<Oweb>;
56
+ ws(url: string, behavior: any, hooks?: any[]): void;
54
57
  /**
55
58
  * Loads routes from a directory.
56
59
  * @param options.directory The directory to load routes from.
@@ -92,4 +95,45 @@ declare interface Hook {
92
95
  declare abstract class Hook {
93
96
  }
94
97
 
95
- export { type Awaitable, Hook, type LoadRoutesOptions, Oweb, type OwebOptions, Route, Oweb as default };
98
+ interface WebSocketAdapter {
99
+ send(message: string | ArrayBuffer, isBinary?: boolean, compress?: boolean): number;
100
+ close(code?: number, shortMessage?: string | ArrayBuffer): void;
101
+ end(code?: number, shortMessage?: string | ArrayBuffer): void;
102
+ cork(cb: () => void): void;
103
+ getUserData(): any;
104
+ getRemoteAddressAsText(): ArrayBuffer;
105
+ getRemoteAddress(): ArrayBuffer;
106
+ subscribe(topic: string): boolean;
107
+ unsubscribe(topic: string): boolean;
108
+ publish(topic: string, message: string | ArrayBuffer, isBinary?: boolean, compress?: boolean): boolean;
109
+ }
110
+ interface WebSocketRouteOptions {
111
+ compression?: number;
112
+ maxPayloadLength?: number;
113
+ idleTimeout?: number;
114
+ sendPingsAutomatically?: boolean;
115
+ }
116
+ declare abstract class WebSocketRoute {
117
+ _options: WebSocketRouteOptions;
118
+ constructor(options?: WebSocketRouteOptions);
119
+ /**
120
+ * Called when a client connects.
121
+ */
122
+ open?(ws: WebSocketAdapter, req: any): Awaitable<void>;
123
+ /**
124
+ * Called when a message is received.
125
+ */
126
+ message?(ws: WebSocketAdapter, message: ArrayBuffer, isBinary: boolean): Awaitable<void>;
127
+ /**
128
+ * Called when the connection is closed.
129
+ */
130
+ close?(ws: WebSocketAdapter, code: number, message: ArrayBuffer): Awaitable<void>;
131
+ /**
132
+ * Called when the socket buffer is empty (backpressure).
133
+ */
134
+ drain?(ws: WebSocketAdapter): Awaitable<void>;
135
+ ping?(ws: WebSocketAdapter, message: ArrayBuffer): Awaitable<void>;
136
+ pong?(ws: WebSocketAdapter, message: ArrayBuffer): Awaitable<void>;
137
+ }
138
+
139
+ export { type Awaitable, Hook, type LoadRoutesOptions, Oweb, type OwebOptions, Route, type WebSocketAdapter, WebSocketRoute, type WebSocketRouteOptions, Oweb as default };
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ var index_default = Oweb;
4
4
  export * from './structures/Oweb.js';
5
5
  export * from './structures/Route.js';
6
6
  export * from './structures/Hook.js';
7
+ export * from './structures/WebSocketRoute.js';
7
8
  export * from './types.js';
8
9
  export {
9
10
  index_default as default
@@ -0,0 +1,80 @@
1
+ import {
2
+ __name
3
+ } from "../chunk-SHUYVCID.js";
4
+ const topicSubscribers = /* @__PURE__ */ new Map();
5
+ class FastifyWebSocketAdapter {
6
+ static {
7
+ __name(this, "FastifyWebSocketAdapter");
8
+ }
9
+ ws;
10
+ req;
11
+ userData;
12
+ constructor(ws, req) {
13
+ this.ws = ws;
14
+ this.req = req;
15
+ this.userData = {};
16
+ }
17
+ send(message, isBinary, compress) {
18
+ if (this.ws.readyState === 1) {
19
+ this.ws.send(message, {
20
+ binary: isBinary,
21
+ compress
22
+ });
23
+ return 1;
24
+ }
25
+ return 0;
26
+ }
27
+ close(code, shortMessage) {
28
+ this.ws.close(code, shortMessage);
29
+ }
30
+ end(code, shortMessage) {
31
+ this.ws.close(code, shortMessage);
32
+ }
33
+ cork(cb) {
34
+ cb();
35
+ }
36
+ getUserData() {
37
+ return this.userData;
38
+ }
39
+ getRemoteAddressAsText() {
40
+ return Buffer.from(this.req.socket?.remoteAddress || "");
41
+ }
42
+ getRemoteAddress() {
43
+ return this.getRemoteAddressAsText();
44
+ }
45
+ subscribe(topic) {
46
+ if (!topicSubscribers.has(topic)) {
47
+ topicSubscribers.set(topic, /* @__PURE__ */ new Set());
48
+ }
49
+ topicSubscribers.get(topic).add(this);
50
+ return true;
51
+ }
52
+ unsubscribe(topic) {
53
+ const set = topicSubscribers.get(topic);
54
+ if (set) {
55
+ set.delete(this);
56
+ if (set.size === 0) topicSubscribers.delete(topic);
57
+ return true;
58
+ }
59
+ return false;
60
+ }
61
+ publish(topic, message, isBinary, compress) {
62
+ const set = topicSubscribers.get(topic);
63
+ if (!set) return false;
64
+ set.forEach((client) => {
65
+ if (client !== this && client.ws.readyState === 1) {
66
+ client.send(message, isBinary, compress);
67
+ }
68
+ });
69
+ return true;
70
+ }
71
+ cleanup() {
72
+ topicSubscribers.forEach((set, topic) => {
73
+ set.delete(this);
74
+ if (set.size === 0) topicSubscribers.delete(topic);
75
+ });
76
+ }
77
+ }
78
+ export {
79
+ FastifyWebSocketAdapter
80
+ };
@@ -5,6 +5,7 @@ import Fastify from "fastify";
5
5
  import { applyMatcherHMR, applyRouteHMR, assignRoutes } from '../utils/assignRoutes.js';
6
6
  import { watchDirectory } from '../utils/watcher.js';
7
7
  import { info, success, warn } from '../utils/logger.js';
8
+ import websocketPlugin from "@fastify/websocket";
8
9
  let _FastifyInstance = class _FastifyInstance2 {
9
10
  static {
10
11
  __name(this, "_FastifyInstance");
@@ -15,10 +16,12 @@ class Oweb extends _FastifyInstance {
15
16
  __name(this, "Oweb");
16
17
  }
17
18
  _options = {};
19
+ _internalKV = /* @__PURE__ */ new Map();
18
20
  hmrDirectory;
19
21
  hmrMatchersDirectory;
20
22
  directory;
21
23
  matchersDirectory;
24
+ uServer = null;
22
25
  routes = /* @__PURE__ */ new Map();
23
26
  constructor(options) {
24
27
  super();
@@ -38,12 +41,18 @@ class Oweb extends _FastifyInstance {
38
41
  if (this._options.uWebSocketsEnabled) {
39
42
  const serverimp = (await import("../uwebsocket/server.js")).default;
40
43
  const server = await serverimp({});
44
+ this.uServer = server;
41
45
  this._options.serverFactory = (handler) => {
42
46
  server.on("request", handler);
43
47
  return server;
44
48
  };
49
+ } else {
50
+ this.uServer = null;
45
51
  }
46
52
  const fastify = Fastify(this._options);
53
+ if (!this._options.uWebSocketsEnabled) {
54
+ await fastify.register(websocketPlugin);
55
+ }
47
56
  fastify.addHook("onRequest", (_, res, done) => {
48
57
  res.header("X-Powered-By", "Oweb");
49
58
  done();
@@ -52,6 +61,17 @@ class Oweb extends _FastifyInstance {
52
61
  if (key === "constructor") continue;
53
62
  Object.defineProperty(fastify, key, Object.getOwnPropertyDescriptor(Oweb.prototype, key));
54
63
  }
64
+ Object.defineProperty(fastify, "_internalKV", {
65
+ value: this._internalKV,
66
+ writable: true,
67
+ enumerable: false
68
+ });
69
+ Object.defineProperty(fastify, "uServer", {
70
+ value: this.uServer,
71
+ writable: true,
72
+ enumerable: false,
73
+ configurable: false
74
+ });
55
75
  Object.defineProperty(fastify, "_options", {
56
76
  value: this._options,
57
77
  writable: true,
@@ -60,6 +80,13 @@ class Oweb extends _FastifyInstance {
60
80
  });
61
81
  return fastify;
62
82
  }
83
+ ws(url, behavior, hooks = []) {
84
+ if (this.uServer) {
85
+ this.uServer.ws(url, behavior, hooks);
86
+ } else {
87
+ warn("Oweb#ws is only available when uWebSockets is enabled. For Fastify instances, Oweb automatically handles registrations.");
88
+ }
89
+ }
63
90
  /**
64
91
  * Loads routes from a directory.
65
92
  * @param options.directory The directory to load routes from.
@@ -77,6 +104,7 @@ class Oweb extends _FastifyInstance {
77
104
  if (hmr?.enabled) {
78
105
  this.hmrDirectory = hmr.directory;
79
106
  this.hmrMatchersDirectory = hmr.matchersDirectory;
107
+ this._internalKV.set("hmr", true);
80
108
  success(`Hot Module Replacement enabled. Watching changes in ${hmr.directory}`, "HMR");
81
109
  } else {
82
110
  warn('Hot Module Replacement is disabled. Use "await app.loadRoutes({ hmr: { enabled: true, directory: path } })" to enable it.', "HMR");
@@ -0,0 +1,19 @@
1
+ import {
2
+ __name
3
+ } from "../chunk-SHUYVCID.js";
4
+ class WebSocketRoute {
5
+ static {
6
+ __name(this, "WebSocketRoute");
7
+ }
8
+ _options = {};
9
+ constructor(options) {
10
+ this._options = options ?? {
11
+ compression: 0,
12
+ maxPayloadLength: 16 * 1024 * 1024,
13
+ idleTimeout: 120
14
+ };
15
+ }
16
+ }
17
+ export {
18
+ WebSocketRoute
19
+ };
@@ -5,10 +5,15 @@ import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { buildRoutePath, buildRouteURL } from './utils.js';
7
7
  import { walk } from './walk.js';
8
- import { success, warn } from './logger.js';
8
+ import { error, success, warn } from './logger.js';
9
9
  import { match } from "path-to-regexp";
10
10
  import generateFunctionFromTypescript from './generateFunctionFromTypescript.js';
11
11
  import { readdirSync } from "node:fs";
12
+ import { WebSocketRoute } from '../structures/WebSocketRoute.js';
13
+ import { FastifyWebSocketAdapter } from '../structures/FastifyWebSocketAdapter.js';
14
+ import { formatSSE } from './utils.js';
15
+ const websocketRoutes = {};
16
+ const registeredWebSockets = /* @__PURE__ */ new Set();
12
17
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
18
  let matcherOverrides = {};
14
19
  let routeFunctions = {
@@ -37,6 +42,40 @@ function removeExtension(filePath) {
37
42
  return filePath;
38
43
  }
39
44
  __name(removeExtension, "removeExtension");
45
+ function createWebSocketProxy(url) {
46
+ const getHandler = /* @__PURE__ */ __name(() => websocketRoutes[url], "getHandler");
47
+ return {
48
+ compression: getHandler()?._options.compression,
49
+ maxPayloadLength: getHandler()?._options.maxPayloadLength,
50
+ idleTimeout: getHandler()?._options.idleTimeout,
51
+ sendPingsAutomatically: getHandler()?._options.sendPingsAutomatically,
52
+ open: /* @__PURE__ */ __name((ws, req) => {
53
+ const handler = getHandler();
54
+ if (handler?.open) handler.open(ws, req);
55
+ }, "open"),
56
+ message: /* @__PURE__ */ __name((ws, message, isBinary) => {
57
+ const handler = getHandler();
58
+ if (handler?.message) handler.message(ws, message, isBinary);
59
+ }, "message"),
60
+ drain: /* @__PURE__ */ __name((ws) => {
61
+ const handler = getHandler();
62
+ if (handler?.drain) handler.drain(ws);
63
+ }, "drain"),
64
+ close: /* @__PURE__ */ __name((ws, code, message) => {
65
+ const handler = getHandler();
66
+ if (handler?.close) handler.close(ws, code, message);
67
+ }, "close"),
68
+ ping: /* @__PURE__ */ __name((ws, message) => {
69
+ const handler = getHandler();
70
+ if (handler?.ping) handler.ping(ws, message);
71
+ }, "ping"),
72
+ pong: /* @__PURE__ */ __name((ws, message) => {
73
+ const handler = getHandler();
74
+ if (handler?.pong) handler.pong(ws, message);
75
+ }, "pong")
76
+ };
77
+ }
78
+ __name(createWebSocketProxy, "createWebSocketProxy");
40
79
  const applyMatcherHMR = /* @__PURE__ */ __name(async (oweb, op, workingDir, fallbackDir, filePath, content) => {
41
80
  let def;
42
81
  const fileName = path.basename(filePath);
@@ -80,6 +119,12 @@ const applyRouteHMR = /* @__PURE__ */ __name(async (oweb, op, workingDir, fallba
80
119
  const routes = await generateRoutes(files, path2);
81
120
  routesCache = routes;
82
121
  const f = routes.find((x) => x.fileInfo.filePath == path2);
122
+ if (f?.fn?.prototype instanceof WebSocketRoute) {
123
+ assignSpecificRoute(oweb, f);
124
+ const end2 = Date.now() - start;
125
+ success(`WebSocket Route ${f.url} created in ${end2}ms`, "HMR");
126
+ return;
127
+ }
83
128
  temporaryRequests[f.method.toLowerCase()][f.url] = inner(oweb, f);
84
129
  const end = Date.now() - start;
85
130
  success(`Route ${f.method.toUpperCase()}:${f.url} created in ${end}ms`, "HMR");
@@ -89,6 +134,12 @@ const applyRouteHMR = /* @__PURE__ */ __name(async (oweb, op, workingDir, fallba
89
134
  const routes = await generateRoutes(files, path2);
90
135
  routesCache = routes;
91
136
  const f = routes.find((x) => x.fileInfo.filePath == path2);
137
+ if (f?.fn?.prototype instanceof WebSocketRoute) {
138
+ websocketRoutes[f.url] = new f.fn();
139
+ const end2 = Date.now() - start;
140
+ success(`WebSocket Route ${f.url} reloaded in ${end2}ms`, "HMR");
141
+ return;
142
+ }
92
143
  if (f.url in temporaryRequests[f.method.toLowerCase()]) {
93
144
  temporaryRequests[f.method.toLowerCase()][f.url] = inner(oweb, f);
94
145
  } else {
@@ -103,14 +154,22 @@ const applyRouteHMR = /* @__PURE__ */ __name(async (oweb, op, workingDir, fallba
103
154
  if (builded.url.endsWith("/index")) {
104
155
  builded.url = builded.url.slice(0, -"/index".length);
105
156
  }
157
+ if (websocketRoutes[builded.url]) {
158
+ delete websocketRoutes[builded.url];
159
+ const end = Date.now() - start;
160
+ success(`WebSocket Route ${builded.url} removed (shimmed) in ${end}ms`, "HMR");
161
+ return;
162
+ }
106
163
  const f = routesCache.find((x) => x.method == builded.method && x.url == builded.url);
107
- if (f.url in temporaryRequests[f.method.toLowerCase()]) {
108
- delete temporaryRequests[f.method.toLowerCase()][f.url];
109
- } else {
110
- delete routeFunctions[f.method.toLowerCase()][f.url];
164
+ if (f) {
165
+ if (f.url in temporaryRequests[f.method.toLowerCase()]) {
166
+ delete temporaryRequests[f.method.toLowerCase()][f.url];
167
+ } else {
168
+ delete routeFunctions[f.method.toLowerCase()][f.url];
169
+ }
170
+ const end = Date.now() - start;
171
+ success(`Route ${f.method.toUpperCase()}:${f.url} removed in ${end}ms`, "HMR");
111
172
  }
112
- const end = Date.now() - start;
113
- success(`Route ${f.method.toUpperCase()}:${f.url} removed in ${end}ms`, "HMR");
114
173
  }
115
174
  }, "applyRouteHMR");
116
175
  const generateRoutes = /* @__PURE__ */ __name(async (files, onlyGenerateFn) => {
@@ -153,7 +212,16 @@ function inner(oweb, route) {
153
212
  }
154
213
  const routeFunc = new route.fn();
155
214
  const matchers = route.matchers;
215
+ const isParametric = route.url.includes(":") || route.url.includes("*");
156
216
  return function(req, res) {
217
+ if (oweb._internalKV.get("hmr") && isParametric) {
218
+ const currentPath = req.raw.url.split("?")[0];
219
+ const method = req.method.toLowerCase();
220
+ const specificHandler = temporaryRequests[method]?.[currentPath];
221
+ if (specificHandler && specificHandler !== temporaryRequests[route.method][route.url]) {
222
+ return specificHandler(req, res);
223
+ }
224
+ }
157
225
  const checkMatchers = /* @__PURE__ */ __name(() => {
158
226
  for (const matcher of matchers) {
159
227
  const param = req.params[matcher.paramName];
@@ -164,31 +232,98 @@ function inner(oweb, route) {
164
232
  }
165
233
  return true;
166
234
  }, "checkMatchers");
167
- const handle = /* @__PURE__ */ __name(() => {
168
- if (routeFunc.handle.constructor.name == "AsyncFunction") {
169
- routeFunc.handle(req, res).catch((error) => {
170
- if (routeFunc?.handleError) {
171
- routeFunc.handleError(req, res, error);
235
+ const handle = /* @__PURE__ */ __name(async () => {
236
+ let result;
237
+ try {
238
+ if (routeFunc.handle.constructor.name === "AsyncFunction") {
239
+ result = await routeFunc.handle(req, res);
240
+ } else {
241
+ result = routeFunc.handle(req, res);
242
+ if (result instanceof Promise) {
243
+ result = await result;
244
+ }
245
+ }
246
+ } catch (error2) {
247
+ if (routeFunc?.handleError) {
248
+ routeFunc.handleError(req, res, error2);
249
+ } else {
250
+ oweb._options.OWEB_INTERNAL_ERROR_HANDLER(req, res, error2);
251
+ }
252
+ return;
253
+ }
254
+ const isIterable = result && typeof result[Symbol.iterator] === "function";
255
+ const isAsyncIterable = result && typeof result[Symbol.asyncIterator] === "function";
256
+ if ((isIterable || isAsyncIterable) && !res.sent) {
257
+ const rawObj = res.raw;
258
+ const uwsRes = rawObj.res && typeof rawObj.res.cork === "function" ? rawObj.res : null;
259
+ const corkedOp = /* @__PURE__ */ __name((op) => {
260
+ if (uwsRes) {
261
+ uwsRes.cork(op);
172
262
  } else {
173
- oweb._options.OWEB_INTERNAL_ERROR_HANDLER(req, res, error);
263
+ op();
264
+ }
265
+ }, "corkedOp");
266
+ corkedOp(() => {
267
+ res.raw.writeHead(200, {
268
+ "Content-Type": "text/event-stream",
269
+ "Cache-Control": "no-cache",
270
+ Connection: "keep-alive",
271
+ "Access-Control-Allow-Origin": "*"
272
+ });
273
+ if (res.raw.flushHeaders) {
274
+ res.raw.flushHeaders();
174
275
  }
175
276
  });
176
- } else {
277
+ let aborted = false;
278
+ const onAborted = /* @__PURE__ */ __name(() => {
279
+ aborted = true;
280
+ }, "onAborted");
281
+ if (res.raw.on) {
282
+ res.raw.on("close", onAborted);
283
+ res.raw.on("aborted", onAborted);
284
+ } else if (rawObj["onAborted"]) {
285
+ rawObj["onAborted"](onAborted);
286
+ }
177
287
  try {
178
- routeFunc.handle(req, res);
179
- } catch (error) {
180
- if (routeFunc?.handleError) {
181
- routeFunc.handleError(req, res, error);
288
+ if (isAsyncIterable) {
289
+ for await (const chunk of result) {
290
+ if (aborted || res.raw.destroyed) break;
291
+ corkedOp(() => {
292
+ res.raw.write(formatSSE(chunk));
293
+ });
294
+ }
182
295
  } else {
183
- oweb._options.OWEB_INTERNAL_ERROR_HANDLER(req, res, error);
296
+ for (const chunk of result) {
297
+ if (aborted || res.raw.destroyed) break;
298
+ corkedOp(() => {
299
+ res.raw.write(formatSSE(chunk));
300
+ });
301
+ }
302
+ }
303
+ } catch (err) {
304
+ error(`Error while streaming response for ${route.method.toUpperCase()}:${route.url} - ${err.message}`, "SSE");
305
+ } finally {
306
+ if (res.raw.off) {
307
+ res.raw.off("close", onAborted);
308
+ res.raw.off("aborted", onAborted);
309
+ }
310
+ if (!aborted && !res.raw.destroyed) {
311
+ corkedOp(() => {
312
+ res.raw.end();
313
+ });
184
314
  }
185
315
  }
316
+ return;
317
+ }
318
+ if (!res.sent && result !== void 0) {
319
+ res.send(result);
186
320
  }
187
321
  }, "handle");
188
322
  if (route.fileInfo.hooks.length) {
189
323
  for (let index = 0; index < route.fileInfo.hooks.length; index++) {
190
324
  const hookFun = route.fileInfo.hooks[index];
191
- hookFun.prototype.handle(req, res, () => {
325
+ const hookInstance = typeof hookFun === "function" ? new hookFun() : hookFun;
326
+ hookInstance.handle(req, res, () => {
192
327
  if (index + 1 == route.fileInfo.hooks.length) {
193
328
  if (!checkMatchers()) {
194
329
  send404(req, res);
@@ -217,7 +352,107 @@ function send404(req, res) {
217
352
  }
218
353
  __name(send404, "send404");
219
354
  function assignSpecificRoute(oweb, route) {
220
- if (!route.fn) return;
355
+ if (!route?.fn) return;
356
+ if (route?.fn?.prototype instanceof WebSocketRoute) {
357
+ const wsInstance = new route.fn();
358
+ websocketRoutes[route.url] = wsInstance;
359
+ if (!registeredWebSockets.has(route.url)) {
360
+ registeredWebSockets.add(route.url);
361
+ if (oweb._options.uWebSocketsEnabled && oweb.uServer) {
362
+ const proxy = createWebSocketProxy(route.url);
363
+ oweb.ws(route.url, proxy, route.fileInfo.hooks);
364
+ } else {
365
+ oweb.get(route.url, {
366
+ websocket: true
367
+ }, (arg1, arg2) => {
368
+ (async () => {
369
+ let socket;
370
+ let req;
371
+ if (arg1 && arg1.socket) {
372
+ socket = arg1.socket;
373
+ req = arg2;
374
+ } else if (arg1 && arg1.raw && arg1.raw.socket) {
375
+ socket = arg1.raw.socket;
376
+ req = arg1;
377
+ } else {
378
+ socket = arg1;
379
+ req = arg2 || {};
380
+ }
381
+ if (!socket || typeof socket.on !== "function") {
382
+ error(`Could not find underlying socket for route ${route.url}. Arg1 type: ${typeof arg1}`, "WS");
383
+ return;
384
+ }
385
+ const adapter = new FastifyWebSocketAdapter(socket, req.raw || req);
386
+ socket.on("error", (err) => {
387
+ error(`${route.url}: ${err.message}`, "WS");
388
+ });
389
+ const hooks = route.fileInfo.hooks || [];
390
+ try {
391
+ for (const HookClass of hooks) {
392
+ await new Promise((resolve, reject) => {
393
+ const hookInstance = typeof HookClass === "function" ? new HookClass() : HookClass;
394
+ hookInstance.handle(req, {
395
+ status: /* @__PURE__ */ __name((c) => ({
396
+ send: /* @__PURE__ */ __name((m) => {
397
+ socket.close(c, m);
398
+ reject("closed");
399
+ }, "send")
400
+ }), "status"),
401
+ header: /* @__PURE__ */ __name(() => {
402
+ }, "header"),
403
+ send: /* @__PURE__ */ __name((m) => {
404
+ socket.close(1e3, m);
405
+ reject("closed");
406
+ }, "send")
407
+ }, (err) => {
408
+ if (err) reject(err);
409
+ else resolve();
410
+ });
411
+ });
412
+ }
413
+ } catch (e) {
414
+ if (e !== "closed") {
415
+ error(`WebSocket Hook Error: ${e}`, "WS");
416
+ socket.close(1011);
417
+ }
418
+ return;
419
+ }
420
+ const getHandler = /* @__PURE__ */ __name(() => websocketRoutes[route.url], "getHandler");
421
+ socket.on("message", (message, isBinary) => {
422
+ const h = getHandler();
423
+ if (h?.message) h.message(adapter, message, isBinary);
424
+ });
425
+ socket.on("close", (code, reason) => {
426
+ const h = getHandler();
427
+ adapter.cleanup();
428
+ if (h?.close) h.close(adapter, code, reason);
429
+ });
430
+ socket.on("ping", (data) => {
431
+ const h = getHandler();
432
+ if (h?.ping) h.ping(adapter, data);
433
+ });
434
+ socket.on("pong", (data) => {
435
+ const h = getHandler();
436
+ if (h?.pong) h.pong(adapter, data);
437
+ });
438
+ const handler = getHandler();
439
+ if (handler?.open) {
440
+ await handler.open(adapter, req);
441
+ }
442
+ })().catch((err) => {
443
+ error(`Internaal Error on ${route.url}: ${err.message}`, "WS");
444
+ if (arg1 && arg1.socket) {
445
+ try {
446
+ arg1.socket.close(1011);
447
+ } catch {
448
+ }
449
+ }
450
+ });
451
+ });
452
+ }
453
+ }
454
+ return;
455
+ }
221
456
  const routeFunc = new route.fn();
222
457
  routeFunctions[route.method][route.url] = inner(oweb, route);
223
458
  oweb[route.method](route.url, routeFunc._options || {}, function(req, res) {
@@ -12,6 +12,18 @@ const convertParamSyntax = /* @__PURE__ */ __name((path) => {
12
12
  return mergePaths(...subpaths);
13
13
  }, "convertParamSyntax");
14
14
  const convertCatchallSyntax = /* @__PURE__ */ __name((url) => url.replace(/:\.\.\.\w+/g, "*"), "convertCatchallSyntax");
15
+ const formatSSE = /* @__PURE__ */ __name((chunk) => {
16
+ if (typeof chunk === "object") {
17
+ try {
18
+ chunk = JSON.stringify(chunk);
19
+ } catch (e) {
20
+ chunk = String(chunk);
21
+ }
22
+ }
23
+ return `data: ${chunk}
24
+
25
+ `;
26
+ }, "formatSSE");
15
27
  const buildRoutePath = /* @__PURE__ */ __name((parsedFile) => {
16
28
  const directory = parsedFile.dir === parsedFile.root ? "" : parsedFile.dir;
17
29
  const name = parsedFile.name.startsWith("index") ? parsedFile.name.replace("index", "") : `/${parsedFile.name}`;
@@ -64,5 +76,6 @@ export {
64
76
  buildRouteURL,
65
77
  convertCatchallSyntax,
66
78
  convertParamSyntax,
79
+ formatSSE,
67
80
  mergePaths
68
81
  };
@@ -69,9 +69,14 @@ const walk = /* @__PURE__ */ __name(async (directory, tree = [], fallbackDir) =>
69
69
  }
70
70
  const hooksImport = useHook.map((hookPath) => {
71
71
  if (fallbackDir) {
72
- const findLastPath = hookPath.replace(process.cwd(), "").split("\\").at(-1);
73
- const additionNeeded = !fallbackDir.endsWith(`/${findLastPath}`);
74
- return new URL(path.join(process.cwd(), fallbackDir, additionNeeded ? `/${findLastPath}` : "")).pathname.replaceAll("\\", "/") + "/_hooks.js";
72
+ const relativeToRoot = path.relative(process.cwd(), hookPath);
73
+ const normalizedFallback = fallbackDir.replace(/\\/g, "/").replace(/\/$/, "");
74
+ const normalizedRel = relativeToRoot.replace(/\\/g, "/");
75
+ let finalPath = normalizedRel;
76
+ if (!normalizedRel.startsWith(normalizedFallback)) {
77
+ finalPath = path.posix.join(normalizedFallback, normalizedRel);
78
+ }
79
+ return new URL(`file://${path.join(process.cwd(), finalPath)}`).pathname + "/_hooks.js";
75
80
  } else {
76
81
  return new URL(hookPath, `file://${__dirname}`).pathname.replaceAll("\\", "/") + "/_hooks.js";
77
82
  }
@@ -9,6 +9,7 @@ class HttpRequest extends Readable {
9
9
  }
10
10
  req;
11
11
  res;
12
+ uResponse;
12
13
  url;
13
14
  method;
14
15
  statusCode;
@@ -16,19 +17,31 @@ class HttpRequest extends Readable {
16
17
  body;
17
18
  headers;
18
19
  socket;
19
- constructor(uRequest) {
20
- super();
21
- const q = uRequest.getQuery();
20
+ // https://nodejs.org/api/http.html#class-httpincomingmessage
21
+ complete = false;
22
+ connection;
23
+ constructor(uRequest, uResponse) {
24
+ super({
25
+ highWaterMark: 64 * 1024
26
+ });
27
+ this.uResponse = uResponse;
22
28
  this.req = uRequest;
29
+ const q = uRequest.getQuery();
23
30
  this.url = uRequest.getUrl() + (q ? "?" + q : "");
24
31
  this.method = uRequest.getMethod().toUpperCase();
25
- this.statusCode = null;
26
- this.statusMessage = null;
27
32
  this.body = {};
28
33
  this.headers = {};
29
- this.socket = {};
34
+ this.socket = {
35
+ destroy: /* @__PURE__ */ __name(() => {
36
+ }, "destroy"),
37
+ on: /* @__PURE__ */ __name(() => {
38
+ }, "on"),
39
+ removeListener: /* @__PURE__ */ __name(() => {
40
+ }, "removeListener")
41
+ };
42
+ this.connection = this.socket;
30
43
  uRequest.forEach((header, value) => {
31
- this.headers[header] = value;
44
+ this.headers[header.toLowerCase()] = value;
32
45
  });
33
46
  }
34
47
  getRawHeaders() {
@@ -41,8 +54,14 @@ class HttpRequest extends Readable {
41
54
  getRaw() {
42
55
  return this.req;
43
56
  }
44
- _read(size) {
45
- return this.slice(0, size);
57
+ _read(_) {
58
+ if (this.uResponse) {
59
+ setImmediate(() => {
60
+ if (this.uResponse && !this.uResponse.aborted) {
61
+ this.uResponse.resume();
62
+ }
63
+ });
64
+ }
46
65
  }
47
66
  }
48
67
  export {
@@ -54,6 +54,9 @@ class HttpResponse extends Writable {
54
54
  const keys = Object.keys(this.__headers);
55
55
  for (let i = 0; i < keys.length; i++) {
56
56
  const key = keys[i];
57
+ if (key === "content-length" || key === "transfer-encoding") {
58
+ continue;
59
+ }
57
60
  const value = this.__headers[key];
58
61
  if (Array.isArray(value)) {
59
62
  for (let j = 0; j < value.length; j++) {
@@ -23,33 +23,51 @@ async function server_default({ cert_file_name, key_file_name }) {
23
23
  };
24
24
  const uServer = uWS[appType](config).any("/*", (res, req) => {
25
25
  res.finished = false;
26
- const reqWrapper = new HttpRequest(req);
26
+ res.aborted = false;
27
+ res.isPaused = false;
28
+ res.onAborted(() => {
29
+ res.aborted = true;
30
+ res.finished = true;
31
+ });
32
+ const reqWrapper = new HttpRequest(req, res);
27
33
  const resWrapper = new HttpResponse(res, uServer);
28
34
  reqWrapper.res = resWrapper;
29
35
  resWrapper.req = reqWrapper;
30
36
  reqWrapper.socket = resWrapper.socket;
37
+ const originalResume = res.resume;
38
+ res.resume = function() {
39
+ if (res.isPaused && !res.finished && !res.aborted) {
40
+ res.isPaused = false;
41
+ originalResume.call(res);
42
+ }
43
+ };
44
+ handler(reqWrapper, resWrapper);
31
45
  const method = reqWrapper.method;
32
- if (method !== "HEAD") {
46
+ if (method !== "HEAD" && method !== "GET" && !resWrapper.finished) {
33
47
  res.onData((bytes, isLast) => {
34
- const chunk = Buffer.from(bytes);
48
+ if (res.finished || res.aborted) return;
49
+ const chunk = Buffer.from(bytes.slice(0));
50
+ const streamReady = reqWrapper.push(chunk);
35
51
  if (isLast) {
36
- reqWrapper.push(chunk);
52
+ reqWrapper.complete = true;
37
53
  reqWrapper.push(null);
38
- if (!res.finished) {
39
- return handler(reqWrapper, resWrapper);
54
+ } else if (!streamReady) {
55
+ if (!res.isPaused) {
56
+ res.isPaused = true;
57
+ res.pause();
40
58
  }
41
- return;
42
59
  }
43
- return reqWrapper.push(chunk);
44
60
  });
45
- } else if (!res.finished) {
46
- handler(reqWrapper, resWrapper);
61
+ } else if (!resWrapper.finished) {
62
+ reqWrapper.complete = true;
63
+ reqWrapper.push(null);
47
64
  }
48
65
  });
49
66
  let uServerClass = class uServerClass extends EventEmitter {
50
67
  static {
51
68
  __name(this, "uServerClass");
52
69
  }
70
+ _socket;
53
71
  constructor() {
54
72
  super();
55
73
  const oldThisOn = this.on.bind(this);
@@ -113,6 +131,126 @@ async function server_default({ cert_file_name, key_file_name }) {
113
131
  }
114
132
  }
115
133
  }
134
+ ws(pattern, behaviors, hooks = []) {
135
+ uServer.ws(pattern, {
136
+ compression: behaviors.compression,
137
+ maxPayloadLength: behaviors.maxPayloadLength,
138
+ idleTimeout: behaviors.idleTimeout,
139
+ sendPingsAutomatically: behaviors.sendPingsAutomatically,
140
+ upgrade: /* @__PURE__ */ __name(async (res, req, context) => {
141
+ const url = req.getUrl();
142
+ const query = req.getQuery();
143
+ const method = req.getMethod().toUpperCase();
144
+ const headers = {};
145
+ req.forEach((key, value) => {
146
+ headers[key] = value;
147
+ });
148
+ const params = {};
149
+ if (pattern.includes(":") || pattern.includes("*")) {
150
+ const parts = pattern.split("/");
151
+ let paramIndex = 0;
152
+ for (const part of parts) {
153
+ if (part.startsWith(":")) {
154
+ const name = part.slice(1);
155
+ params[name] = req.getParameter(paramIndex);
156
+ paramIndex++;
157
+ } else if (part === "*") {
158
+ params["*"] = req.getParameter(paramIndex);
159
+ paramIndex++;
160
+ }
161
+ }
162
+ }
163
+ const secKey = headers["sec-websocket-key"];
164
+ const secProtocol = headers["sec-websocket-protocol"];
165
+ const secExtensions = headers["sec-websocket-extensions"];
166
+ let aborted = false;
167
+ res.onAborted(() => {
168
+ aborted = true;
169
+ });
170
+ const reqWrapper = {
171
+ url: url + (query ? "?" + query : ""),
172
+ routerPath: pattern,
173
+ query: new URLSearchParams(query),
174
+ headers,
175
+ method,
176
+ params,
177
+ raw: {
178
+ url,
179
+ method,
180
+ headers
181
+ }
182
+ };
183
+ const resWrapper = {
184
+ statusCode: 200,
185
+ _headers: {},
186
+ finished: false,
187
+ header(key, value) {
188
+ this._headers[key.toLowerCase()] = value;
189
+ return this;
190
+ },
191
+ status(code) {
192
+ this.statusCode = code;
193
+ return this;
194
+ },
195
+ send(payload) {
196
+ if (aborted || this.finished) return;
197
+ this.finished = true;
198
+ res.writeStatus(`${this.statusCode} Response`);
199
+ for (const [k, v] of Object.entries(this._headers)) {
200
+ res.writeHeader(k, String(v));
201
+ }
202
+ res.end(typeof payload === "object" ? JSON.stringify(payload) : String(payload));
203
+ return this;
204
+ }
205
+ };
206
+ try {
207
+ for (const HookClass of hooks) {
208
+ if (aborted || resWrapper.finished) return;
209
+ await new Promise((resolve, reject) => {
210
+ try {
211
+ const hookInstance = typeof HookClass === "function" ? new HookClass() : HookClass;
212
+ hookInstance.handle(reqWrapper, resWrapper, (err) => {
213
+ if (err) reject(err);
214
+ else resolve(true);
215
+ });
216
+ } catch (err) {
217
+ reject(err);
218
+ }
219
+ });
220
+ }
221
+ } catch (err) {
222
+ if (!aborted && !resWrapper.finished) {
223
+ console.error("WebSocket Hook Error:", err);
224
+ res.writeStatus("500 Internal Server Error");
225
+ res.end(JSON.stringify({
226
+ error: "Internal Server Error",
227
+ message: err.message
228
+ }));
229
+ }
230
+ return;
231
+ }
232
+ if (aborted || resWrapper.finished) return;
233
+ const reqData = {
234
+ ...reqWrapper,
235
+ query
236
+ };
237
+ res.upgrade({
238
+ req: reqData
239
+ }, secKey, secProtocol, secExtensions, context);
240
+ }, "upgrade"),
241
+ open: /* @__PURE__ */ __name((ws) => {
242
+ if (behaviors.open) {
243
+ const data = ws.getUserData();
244
+ behaviors.open(ws, data.req);
245
+ }
246
+ }, "open"),
247
+ message: behaviors.message,
248
+ drain: behaviors.drain,
249
+ close: behaviors.close,
250
+ ping: behaviors.ping,
251
+ pong: behaviors.pong
252
+ });
253
+ }
116
254
  get uwsApp() {
117
255
  return uServer;
118
256
  }
package/package.json CHANGED
@@ -1,68 +1,70 @@
1
- {
2
- "name": "owebjs",
3
- "version": "1.4.7",
4
- "description": "A flexible and modern web framework built on top of Fastify",
5
- "main": "dist/index.js",
6
- "types": "dist/index.d.ts",
7
- "publishConfig": {
8
- "access": "public"
9
- },
10
- "exports": {
11
- ".": {
12
- "types": "./dist/index.d.ts",
13
- "import": "./dist/index.js",
14
- "default": "./dist/index.js"
15
- },
16
- "./dist/plugins": {
17
- "types": "./dist/plugins/index.d.ts",
18
- "import": "./dist/plugins/index.js",
19
- "default": "./dist/plugins/index.js"
20
- }
21
- },
22
- "scripts": {
23
- "start": "node .",
24
- "build": "tsup",
25
- "dev": "tsup && node .",
26
- "test": "tsup && node test/index.js",
27
- "format": "prettier --write . --ignore-path .gitignore"
28
- },
29
- "homepage": "https://github.com/owebjs/oweb",
30
- "repository": {
31
- "type": "git",
32
- "url": "https://github.com/owebjs/oweb"
33
- },
34
- "keywords": [],
35
- "author": "owebjs",
36
- "license": "MIT",
37
- "dependencies": {
38
- "@babel/core": "^7.28.0",
39
- "@babel/generator": "^7.28.0",
40
- "@babel/parser": "^7.28.0",
41
- "@babel/preset-typescript": "^7.27.1",
42
- "@babel/traverse": "^7.28.0",
43
- "@babel/types": "^7.28.2",
44
- "@fastify/websocket": "^11.2.0",
45
- "chalk": "^5.4.1",
46
- "fastify": "4.23.2",
47
- "path-to-regexp": "^8.2.0",
48
- "uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.52.0"
49
- },
50
- "devDependencies": {
51
- "@fastify/multipart": "^8.1.0",
52
- "@swc/core": "^1.3.85",
53
- "@types/chokidar": "^2.1.3",
54
- "@types/node": "^24.1.0",
55
- "chokidar": "^3.5.3",
56
- "prettier": "^3.0.3",
57
- "tslib": "^2.6.2",
58
- "tsup": "^8.5.0",
59
- "@fastify/cors": "^9.0.1",
60
- "typescript": "^5.2.2"
61
- },
62
- "type": "module",
63
- "pnpm": {
64
- "onlyBuiltDependencies": [
65
- "esbuild"
66
- ]
67
- }
68
- }
1
+ {
2
+ "name": "owebjs",
3
+ "version": "1.4.9-dev",
4
+ "description": "A flexible and modern web framework built on top of Fastify",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js",
14
+ "default": "./dist/index.js"
15
+ },
16
+ "./dist/plugins": {
17
+ "types": "./dist/plugins/index.d.ts",
18
+ "import": "./dist/plugins/index.js",
19
+ "default": "./dist/plugins/index.js"
20
+ }
21
+ },
22
+ "scripts": {
23
+ "start": "node .",
24
+ "build": "tsup",
25
+ "dev": "tsup && node .",
26
+ "test": "tsup && node test/index.js",
27
+ "format": "prettier --write . --ignore-path .gitignore"
28
+ },
29
+ "homepage": "https://github.com/owebjs/oweb",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/owebjs/oweb"
33
+ },
34
+ "keywords": [],
35
+ "author": "owebjs",
36
+ "license": "MIT",
37
+ "dependencies": {
38
+ "@babel/core": "^7.28.0",
39
+ "@babel/generator": "^7.28.0",
40
+ "@babel/parser": "^7.28.0",
41
+ "@babel/preset-typescript": "^7.27.1",
42
+ "@babel/traverse": "^7.28.0",
43
+ "@babel/types": "^7.28.2",
44
+ "@fastify/websocket": "^10.0.1",
45
+ "chalk": "^5.4.1",
46
+ "fastify": "4.23.2",
47
+ "path-to-regexp": "^8.2.0",
48
+ "uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.52.0",
49
+ "ws": "^8.19.0"
50
+ },
51
+ "devDependencies": {
52
+ "@fastify/cors": "^9.0.1",
53
+ "@fastify/multipart": "^8.1.0",
54
+ "@swc/core": "^1.3.85",
55
+ "@types/chokidar": "^2.1.3",
56
+ "@types/node": "^24.1.0",
57
+ "@types/ws": "^8.18.1",
58
+ "chokidar": "^3.5.3",
59
+ "prettier": "^3.0.3",
60
+ "tslib": "^2.6.2",
61
+ "tsup": "^8.5.0",
62
+ "typescript": "^5.2.2"
63
+ },
64
+ "type": "module",
65
+ "pnpm": {
66
+ "onlyBuiltDependencies": [
67
+ "esbuild"
68
+ ]
69
+ }
70
+ }