@cloudnux/local-cloud-provider 0.7.0 → 0.10.0

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.
@@ -0,0 +1,43 @@
1
+ import { FastifyPluginOptions, FastifyPluginAsync } from 'fastify';
2
+ import { WebSocket } from 'ws';
3
+
4
+ type WebSocketEvent = "connect" | "disconnect" | "message";
5
+ interface WebSocketConnection {
6
+ connectionId: string;
7
+ socket: WebSocket;
8
+ path: string;
9
+ connectedAt: Date;
10
+ }
11
+ type WebSocketEventHandler = (connectionId: string, event: WebSocketEvent, data?: any) => Promise<any>;
12
+ interface WebSocketRouteHandler {
13
+ path: string;
14
+ event: WebSocketEvent;
15
+ route?: string;
16
+ handler: WebSocketEventHandler;
17
+ }
18
+ interface WebSocketConfig {
19
+ routeKeyField: string;
20
+ }
21
+ interface WebSocketPluginOptions extends FastifyPluginOptions {
22
+ prefix?: string;
23
+ config?: Partial<WebSocketConfig>;
24
+ }
25
+ interface WebSocketManager {
26
+ registerHandler(handler: WebSocketRouteHandler): void;
27
+ getConnections(path?: string): WebSocketConnection[];
28
+ sendToClient(connectionId: string, data: any): Promise<void>;
29
+ }
30
+ interface WebSocketDecoratorOptions {
31
+ connections: Map<string, WebSocketConnection>;
32
+ handlers: WebSocketRouteHandler[];
33
+ config: WebSocketConfig;
34
+ }
35
+ declare module 'fastify' {
36
+ interface FastifyInstance {
37
+ websockets: WebSocketManager;
38
+ }
39
+ }
40
+
41
+ declare const websocketsPlugin: FastifyPluginAsync<WebSocketPluginOptions>;
42
+
43
+ export { type WebSocketConfig, type WebSocketConnection, type WebSocketDecoratorOptions, type WebSocketEvent, type WebSocketEventHandler, type WebSocketManager, type WebSocketPluginOptions, type WebSocketRouteHandler, websocketsPlugin };
@@ -0,0 +1,96 @@
1
+ // src/websocket-plugin/plugin.ts
2
+ import crypto from "crypto";
3
+ import fsPlugin from "fastify-plugin";
4
+ import websocketPlugin from "@fastify/websocket";
5
+ var DEFAULT_CONFIG = {
6
+ routeKeyField: "action"
7
+ };
8
+ function mergeConfig(defaults, overrides) {
9
+ return { ...defaults, ...overrides };
10
+ }
11
+ var websocketsPlugin = fsPlugin(async (app, options) => {
12
+ const config = mergeConfig(DEFAULT_CONFIG, options.config);
13
+ const connections = /* @__PURE__ */ new Map();
14
+ const handlers = [];
15
+ const registeredPaths = /* @__PURE__ */ new Set();
16
+ await app.register(websocketPlugin);
17
+ function ensureRouteForPath(path) {
18
+ if (registeredPaths.has(path)) return;
19
+ registeredPaths.add(path);
20
+ app.get(path, { websocket: true }, (socket) => {
21
+ const connectionId = crypto.randomUUID();
22
+ const connection = {
23
+ connectionId,
24
+ socket,
25
+ path,
26
+ connectedAt: /* @__PURE__ */ new Date()
27
+ };
28
+ connections.set(connectionId, connection);
29
+ const pathHandlers = () => handlers.filter((h) => h.path === path);
30
+ const connectHandlers = pathHandlers().filter((h) => h.event === "connect");
31
+ for (const h of connectHandlers) {
32
+ h.handler(connectionId, "connect").catch(() => {
33
+ });
34
+ }
35
+ socket.on("message", (raw) => {
36
+ const data = raw.toString();
37
+ const currentHandlers = pathHandlers();
38
+ const routeKey = typeof data === "object" && data !== null ? data[config.routeKeyField] : void 0;
39
+ let matched = false;
40
+ if (routeKey) {
41
+ const routeHandlers = currentHandlers.filter(
42
+ (h) => h.event === "message" && h.route === routeKey
43
+ );
44
+ for (const h of routeHandlers) {
45
+ matched = true;
46
+ h.handler(connectionId, "message", data).catch(() => {
47
+ });
48
+ }
49
+ }
50
+ if (!matched) {
51
+ const defaultHandlers = currentHandlers.filter(
52
+ (h) => h.event === "message" && !h.route
53
+ );
54
+ for (const h of defaultHandlers) {
55
+ h.handler(connectionId, "message", data).catch(() => {
56
+ });
57
+ }
58
+ }
59
+ });
60
+ socket.on("close", () => {
61
+ const disconnectHandlers = pathHandlers().filter((h) => h.event === "disconnect");
62
+ for (const h of disconnectHandlers) {
63
+ h.handler(connectionId, "disconnect").catch(() => {
64
+ });
65
+ }
66
+ connections.delete(connectionId);
67
+ });
68
+ });
69
+ }
70
+ const manager = {
71
+ registerHandler(handler) {
72
+ handlers.push(handler);
73
+ ensureRouteForPath(handler.path);
74
+ },
75
+ getConnections(path) {
76
+ const allConnections = Array.from(connections.values());
77
+ if (path) {
78
+ return allConnections.filter((c) => c.path === path);
79
+ }
80
+ return allConnections;
81
+ },
82
+ async sendToClient(connectionId, data) {
83
+ const connection = connections.get(connectionId);
84
+ if (!connection) {
85
+ throw new Error(`WebSocket connection ${connectionId} not found`);
86
+ }
87
+ const payload = typeof data === "string" ? data : JSON.stringify(data);
88
+ connection.socket.send(payload);
89
+ }
90
+ };
91
+ app.decorate("websockets", manager);
92
+ });
93
+ export {
94
+ websocketsPlugin
95
+ };
96
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/websocket-plugin/plugin.ts"],"sourcesContent":["import crypto from \"node:crypto\";\n\nimport { FastifyInstance, FastifyPluginAsync } from \"fastify\";\nimport fsPlugin from \"fastify-plugin\";\nimport websocketPlugin from \"@fastify/websocket\";\n\nimport {\n WebSocketPluginOptions,\n WebSocketConfig,\n WebSocketConnection,\n WebSocketRouteHandler,\n} from \"./types\";\n\nconst DEFAULT_CONFIG: WebSocketConfig = {\n routeKeyField: \"action\",\n};\n\nfunction mergeConfig(\n defaults: WebSocketConfig,\n overrides?: Partial<WebSocketConfig>\n): WebSocketConfig {\n return { ...defaults, ...overrides };\n}\n\nexport const websocketsPlugin: FastifyPluginAsync<WebSocketPluginOptions> =\n fsPlugin(async (\n app: FastifyInstance,\n options: WebSocketPluginOptions\n ) => {\n const config = mergeConfig(DEFAULT_CONFIG, options.config);\n const connections = new Map<string, WebSocketConnection>();\n const handlers: WebSocketRouteHandler[] = [];\n const registeredPaths = new Set<string>();\n\n // Register @fastify/websocket\n await app.register(websocketPlugin);\n\n // Register WebSocket route for a path when first handler uses it\n function ensureRouteForPath(path: string) {\n if (registeredPaths.has(path)) return;\n registeredPaths.add(path);\n\n app.get(path, { websocket: true }, (socket) => {\n const connectionId = crypto.randomUUID();\n const connection: WebSocketConnection = {\n connectionId,\n socket,\n path,\n connectedAt: new Date(),\n };\n connections.set(connectionId, connection);\n\n // Filter handlers for this path at runtime so late-registered handlers are included\n const pathHandlers = () => handlers.filter(h => h.path === path);\n\n // Fire connect handlers\n const connectHandlers = pathHandlers().filter(h => h.event === \"connect\");\n for (const h of connectHandlers) {\n h.handler(connectionId, \"connect\").catch(() => { });\n }\n\n // Handle incoming messages\n socket.on(\"message\", (raw) => {\n const data = raw.toString();\n\n const currentHandlers = pathHandlers();\n\n // Try to match a specific route handler\n const routeKey = typeof data === \"object\" && data !== null\n ? data[config.routeKeyField]\n : undefined;\n\n let matched = false;\n if (routeKey) {\n const routeHandlers = currentHandlers.filter(\n h => h.event === \"message\" && h.route === routeKey\n );\n for (const h of routeHandlers) {\n matched = true;\n h.handler(connectionId, \"message\", data).catch(() => { });\n }\n }\n\n // Fall back to default message handlers (no route)\n if (!matched) {\n const defaultHandlers = currentHandlers.filter(\n h => h.event === \"message\" && !h.route\n );\n for (const h of defaultHandlers) {\n h.handler(connectionId, \"message\", data).catch(() => { });\n }\n }\n });\n\n // Handle disconnect\n socket.on(\"close\", () => {\n const disconnectHandlers = pathHandlers().filter(h => h.event === \"disconnect\");\n for (const h of disconnectHandlers) {\n h.handler(connectionId, \"disconnect\").catch(() => { });\n }\n connections.delete(connectionId);\n });\n });\n }\n\n // Create manager with eager route registration\n const manager = {\n registerHandler(handler: WebSocketRouteHandler) {\n handlers.push(handler);\n ensureRouteForPath(handler.path);\n },\n getConnections(path?: string): WebSocketConnection[] {\n const allConnections = Array.from(connections.values());\n if (path) {\n return allConnections.filter(c => c.path === path);\n }\n return allConnections;\n },\n async sendToClient(connectionId: string, data: any): Promise<void> {\n const connection = connections.get(connectionId);\n if (!connection) {\n throw new Error(`WebSocket connection ${connectionId} not found`);\n }\n const payload = typeof data === \"string\" ? data : JSON.stringify(data);\n connection.socket.send(payload);\n },\n };\n\n app.decorate('websockets', manager);\n });\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudnux/local-cloud-provider",
3
- "version": "0.7.0",
3
+ "version": "0.10.0",
4
4
  "description": "CloudNux local development cloud provider implementation",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -22,6 +22,10 @@
22
22
  "./dev-console-plugin": {
23
23
  "types": "./dist/dev-console-plugin/index.d.ts",
24
24
  "import": "./dist/dev-console-plugin/index.js"
25
+ },
26
+ "./websocket-plugin": {
27
+ "types": "./dist/websocket-plugin/index.d.ts",
28
+ "import": "./dist/websocket-plugin/index.js"
25
29
  }
26
30
  },
27
31
  "files": [
@@ -47,19 +51,22 @@
47
51
  "clean": "rm -rf dist"
48
52
  },
49
53
  "dependencies": {
50
- "@cloudnux/core-cloud-provider": "0.5.0",
54
+ "@cloudnux/core-cloud-provider": "0.10.0",
51
55
  "@fastify/cors": "^11.1.0",
52
56
  "@fastify/static": "^8.0.0",
57
+ "@fastify/websocket": "^11.0.1",
53
58
  "axios": "1.9.0",
54
59
  "chalk": "^5.4.1",
55
60
  "cron-parser": "^4.9.0",
56
61
  "fastify": "^5.2.1",
57
62
  "fastify-plugin": "^5.0.1",
63
+ "fastify-print-routes": "^5.0.1",
58
64
  "fastify-raw-body": "^5.0.0",
59
65
  "log-symbols": "^7.0.1"
60
66
  },
61
67
  "devDependencies": {
62
68
  "@types/node": "^24.10.1",
69
+ "@types/ws": "^8.18.1",
63
70
  "@vitest/coverage-v8": "^4.0.14",
64
71
  "vitest": "^4.0.14"
65
72
  },
@@ -67,4 +74,4 @@
67
74
  "access": "public",
68
75
  "registry": "https://registry.npmjs.org/"
69
76
  }
70
- }
77
+ }