feat: add ws server request pool handlers example
authorJérôme Benoit <jerome.benoit@sap.com>
Sat, 12 Aug 2023 11:23:27 +0000 (13:23 +0200)
committerJérôme Benoit <jerome.benoit@sap.com>
Sat, 12 Aug 2023 11:23:27 +0000 (13:23 +0200)
reference: #790

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
18 files changed:
.eslintrc.js
.github/dependabot.yml
CHANGELOG.md
README.md
examples/typescript/http-client-pool/tsconfig.json
examples/typescript/http-server-pool/express/src/main.ts
examples/typescript/http-server-pool/express/src/pool.ts
examples/typescript/http-server-pool/express/tsconfig.json
examples/typescript/http-server-pool/fastify/package.json
examples/typescript/http-server-pool/fastify/tsconfig.json
examples/typescript/websocket-server-pool/ws/package.json [new file with mode: 0644]
examples/typescript/websocket-server-pool/ws/pnpm-lock.yaml [new file with mode: 0644]
examples/typescript/websocket-server-pool/ws/requests.js [new file with mode: 0644]
examples/typescript/websocket-server-pool/ws/src/main.ts [new file with mode: 0644]
examples/typescript/websocket-server-pool/ws/src/pool.ts [new file with mode: 0644]
examples/typescript/websocket-server-pool/ws/src/types.ts [new file with mode: 0644]
examples/typescript/websocket-server-pool/ws/src/worker.ts [new file with mode: 0644]
examples/typescript/websocket-server-pool/ws/tsconfig.json [new file with mode: 0644]

index 0188819b8ba2b84d174ae01561cfbc6e8340724a..d640b896f75606df37aff753e6b9cf833f41fb2c 100644 (file)
@@ -120,16 +120,16 @@ module.exports = defineConfig({
     {
       files: ['examples/typescript/**/*.ts'],
       rules: {
-        'import/no-unresolved': 'off',
-        '@typescript-eslint/no-unsafe-argument': 'off',
+        // 'import/no-unresolved': 'off',
+        // '@typescript-eslint/no-unsafe-argument': 'off',
         '@typescript-eslint/no-unsafe-call': 'off',
-        '@typescript-eslint/no-unsafe-return': 'off',
+        // '@typescript-eslint/no-unsafe-return': 'off',
         '@typescript-eslint/no-unsafe-assignment': 'off',
-        '@typescript-eslint/no-unsafe-member-access': 'off',
-        '@typescript-eslint/no-unnecessary-type-assertion': 'off',
-        '@typescript-eslint/strict-boolean-expressions': 'off',
-        '@typescript-eslint/restrict-template-expressions': 'off',
-        '@typescript-eslint/return-await': 'off'
+        '@typescript-eslint/no-unsafe-member-access': 'off'
+        // '@typescript-eslint/no-unnecessary-type-assertion': 'off',
+        // '@typescript-eslint/strict-boolean-expressions': 'off',
+        // '@typescript-eslint/restrict-template-expressions': 'off',
+        // '@typescript-eslint/return-await': 'off'
       }
     },
     {
index 34d0ba137c1e386629a3cc0e034063892cf71946..bc1d7c6338733faf27078f22eae0ca97292fd4b1 100644 (file)
@@ -68,3 +68,15 @@ updates:
       - 'pioardi'
       - 'jerome-benoit'
     versioning-strategy: increase
+  - package-ecosystem: 'npm'
+    directory: '/examples/typescript/websocket-server-pool/ws'
+    schedule:
+      interval: 'daily'
+    labels:
+      - 'dependencies'
+      - 'examples'
+      - 'nocombine'
+    reviewers:
+      - 'pioardi'
+      - 'jerome-benoit'
+    versioning-strategy: increase
index 95fbef8b5516e1c536fd4fd2e4db5b4fc87c9d63..80df7b6ca94f3daf60b0c179773453d495edffa1 100644 (file)
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ### Added
 
 - Add array of transferable objects to the `execute()` method arguments.
+- WebSocket server pool examples: ws
 
 ## [2.6.23] - 2023-08-11
 
index d56bb1878e6dec25e35b37de7b1dbe59251028ba..570f61b3d35f0f24241d2160c28ae1e09661af99 100644 (file)
--- a/README.md
+++ b/README.md
@@ -149,6 +149,8 @@ You can do the same with the classes _ClusterWorker_, _FixedClusterPool_ and _Dy
   - [HTTP server pool](./examples/typescript/http-server-pool/)
     - [Express](./examples/typescript/http-server-pool/express/)
     - [Fastify](./examples/typescript/http-server-pool/fastify/)
+  - [Websocket server pool](./examples/typescript/websocket-server-pool/)
+    - [ws](./examples/typescript/websocket-server-pool/ws/)
 
 Remember that workers can only send and receive structured-cloneable data.
 
index 079bc9e1dff2fe47db7d851d61731b3ab0d7f39d..c070ed24e50c67b81a44c644272d4087d544fc5c 100644 (file)
@@ -4,6 +4,7 @@
     "target": "es2022",
     "module": "es2022",
     "moduleResolution": "Node16",
+    "verbatimModuleSyntax": true,
     "rootDir": "./src",
     "outDir": "./dist",
     "esModuleInterop": true,
index b7a713e03ae4798f5aa48b9bf545ab258c49d6e2..4f1a4d957a75d3274052f56237bd2bdcea243054 100644 (file)
@@ -30,7 +30,7 @@ expressApp.all('/api/echo', (req: Request, res: Response) => {
 expressApp.get('/api/factorial/:number', (req: Request, res: Response) => {
   const { number } = req.params
   requestHandlerPool
-    .execute({ body: { number } }, 'factorial')
+    .execute({ body: { number: parseInt(number) } }, 'factorial')
     .then(response => {
       return res.send(response.body).end()
     })
index 46b673a54e6026c75506323a769d722b0ed812e9..af2a216de5e27ff36f39aacecc38a8b367c8e71b 100644 (file)
@@ -1,7 +1,11 @@
 import { dirname, extname, join } from 'node:path'
 import { fileURLToPath } from 'node:url'
 import { DynamicThreadPool, availableParallelism } from 'poolifier'
-import { type WorkerData, type WorkerResponse } from './types.js'
+import {
+  type BodyPayload,
+  type WorkerData,
+  type WorkerResponse
+} from './types.js'
 
 const workerFile = join(
   dirname(fileURLToPath(import.meta.url)),
@@ -9,8 +13,8 @@ const workerFile = join(
 )
 
 export const requestHandlerPool = new DynamicThreadPool<
-WorkerData,
-WorkerResponse
+WorkerData<BodyPayload>,
+WorkerResponse<BodyPayload>
 >(1, availableParallelism(), workerFile, {
   enableTasksQueue: true,
   tasksQueueOptions: {
index 82728d1c94e54f06f1b10af73aa45c06e2d71eec..57e49a1c4adf10b70b32bac5d52516025336d16b 100644 (file)
@@ -4,6 +4,7 @@
     "target": "ES2022",
     "module": "ES2022",
     "moduleResolution": "Node16",
+    "verbatimModuleSyntax": true,
     "rootDir": "./src",
     "outDir": "./dist",
     "esModuleInterop": true,
index 1f4f11728d94157e358d60541db151a998cf5a1e..ffc88cd7a0c0da5b11410b62024e41bd396ad81b 100644 (file)
@@ -1,8 +1,8 @@
 {
   "$schema": "https://json.schemastore.org/package",
-  "name": "express-request-pool",
+  "name": "fastify-plugin-request-pool",
   "version": "1.0.0",
-  "description": "Express request pool",
+  "description": "fastify plugin request pool",
   "main": "dist/main.js",
   "type": "module",
   "volta": {
index 82728d1c94e54f06f1b10af73aa45c06e2d71eec..57e49a1c4adf10b70b32bac5d52516025336d16b 100644 (file)
@@ -4,6 +4,7 @@
     "target": "ES2022",
     "module": "ES2022",
     "moduleResolution": "Node16",
+    "verbatimModuleSyntax": true,
     "rootDir": "./src",
     "outDir": "./dist",
     "esModuleInterop": true,
diff --git a/examples/typescript/websocket-server-pool/ws/package.json b/examples/typescript/websocket-server-pool/ws/package.json
new file mode 100644 (file)
index 0000000..7ef6bd6
--- /dev/null
@@ -0,0 +1,34 @@
+{
+  "$schema": "https://json.schemastore.org/package",
+  "name": "ws-request-pool",
+  "version": "1.0.0",
+  "description": "ws request pool",
+  "main": "dist/main.js",
+  "type": "module",
+  "volta": {
+    "node": "20.5.1",
+    "pnpm": "8.6.12"
+  },
+  "scripts": {
+    "build": "pnpm build:clean && tsc",
+    "build:clean": "tsc --build --clean",
+    "start": "node dist/main.js",
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "keywords": [],
+  "author": "",
+  "license": "ISC",
+  "dependencies": {
+    "poolifier": "^2.6.23",
+    "ws": "^8.13.0"
+  },
+  "devDependencies": {
+    "@types/node": "^20.4.9",
+    "@types/ws": "^8.5.5",
+    "typescript": "^5.1.6"
+  },
+  "optionalDependencies": {
+    "bufferutil": "^4.0.7",
+    "utf-8-validate": "^6.0.3"
+  }
+}
diff --git a/examples/typescript/websocket-server-pool/ws/pnpm-lock.yaml b/examples/typescript/websocket-server-pool/ws/pnpm-lock.yaml
new file mode 100644 (file)
index 0000000..78402dc
--- /dev/null
@@ -0,0 +1,94 @@
+lockfileVersion: '6.0'
+
+settings:
+  autoInstallPeers: true
+  excludeLinksFromLockfile: false
+
+dependencies:
+  poolifier:
+    specifier: ^2.6.23
+    version: 2.6.23
+  ws:
+    specifier: ^8.13.0
+    version: 8.13.0(bufferutil@4.0.7)(utf-8-validate@6.0.3)
+
+optionalDependencies:
+  bufferutil:
+    specifier: ^4.0.7
+    version: 4.0.7
+  utf-8-validate:
+    specifier: ^6.0.3
+    version: 6.0.3
+
+devDependencies:
+  '@types/node':
+    specifier: ^20.4.9
+    version: 20.4.10
+  '@types/ws':
+    specifier: ^8.5.5
+    version: 8.5.5
+  typescript:
+    specifier: ^5.1.6
+    version: 5.1.6
+
+packages:
+
+  /@types/node@20.4.10:
+    resolution: {integrity: sha512-vwzFiiy8Rn6E0MtA13/Cxxgpan/N6UeNYR9oUu6kuJWxu6zCk98trcDp8CBhbtaeuq9SykCmXkFr2lWLoPcvLg==}
+    dev: true
+
+  /@types/ws@8.5.5:
+    resolution: {integrity: sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==}
+    dependencies:
+      '@types/node': 20.4.10
+    dev: true
+
+  /bufferutil@4.0.7:
+    resolution: {integrity: sha512-kukuqc39WOHtdxtw4UScxF/WVnMFVSQVKhtx3AjZJzhd0RGZZldcrfSEbVsWWe6KNH253574cq5F+wpv0G9pJw==}
+    engines: {node: '>=6.14.2'}
+    requiresBuild: true
+    dependencies:
+      node-gyp-build: 4.6.0
+    dev: false
+
+  /node-gyp-build@4.6.0:
+    resolution: {integrity: sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==}
+    hasBin: true
+    requiresBuild: true
+    dev: false
+
+  /poolifier@2.6.23:
+    resolution: {integrity: sha512-QQagtUD4DCzeEEq2SjoxDRsPLqr5qzJm3Wur29rWDepG935BXlGHVZLFlqUoaHXkcILIBKvLxEFs+SB2Op4TIw==}
+    engines: {node: '>=16.14.0', pnpm: '>=8.6.0'}
+    requiresBuild: true
+    dev: false
+
+  /typescript@5.1.6:
+    resolution: {integrity: sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==}
+    engines: {node: '>=14.17'}
+    hasBin: true
+    dev: true
+
+  /utf-8-validate@6.0.3:
+    resolution: {integrity: sha512-uIuGf9TWQ/y+0Lp+KGZCMuJWc3N9BHA+l/UmHd/oUHwJJDeysyTRxNQVkbzsIWfGFbRe3OcgML/i0mvVRPOyDA==}
+    engines: {node: '>=6.14.2'}
+    requiresBuild: true
+    dependencies:
+      node-gyp-build: 4.6.0
+    dev: false
+
+  /ws@8.13.0(bufferutil@4.0.7)(utf-8-validate@6.0.3):
+    resolution: {integrity: sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==}
+    engines: {node: '>=10.0.0'}
+    peerDependencies:
+      bufferutil: ^4.0.1
+      utf-8-validate: '>=5.0.2'
+    peerDependenciesMeta:
+      bufferutil:
+        optional: true
+      utf-8-validate:
+        optional: true
+    dependencies:
+      bufferutil: 4.0.7
+      utf-8-validate: 6.0.3
+    dev: false
diff --git a/examples/typescript/websocket-server-pool/ws/requests.js b/examples/typescript/websocket-server-pool/ws/requests.js
new file mode 100644 (file)
index 0000000..e00bcc7
--- /dev/null
@@ -0,0 +1,20 @@
+import { WebSocket } from 'ws'
+
+const ws = new WebSocket('ws://localhost:8080')
+
+ws.on('error', console.error)
+
+ws.on('open', () => {
+  for (let i = 0; i < 60; i++) {
+    ws.send(
+      JSON.stringify({ type: 'echo', data: { key1: 'value1', key2: 'value2' } })
+    )
+  }
+  for (let i = 0; i < 60; i++) {
+    ws.send(JSON.stringify({ type: 'factorial', data: { number: 30 } }))
+  }
+})
+
+ws.on('message', message => {
+  console.info('message received: %s', message)
+})
diff --git a/examples/typescript/websocket-server-pool/ws/src/main.ts b/examples/typescript/websocket-server-pool/ws/src/main.ts
new file mode 100644 (file)
index 0000000..37f31f3
--- /dev/null
@@ -0,0 +1,55 @@
+import { type RawData, WebSocketServer } from 'ws'
+import { type DataPayload, type MessagePayload, MessageType } from './types.js'
+import { requestHandlerPool } from './pool.js'
+
+const port = 8080
+const wss = new WebSocketServer({ port }, () => {
+  console.info(
+    `⚡️[ws server]: WebSocket server is started at http://localhost:${port}/`
+  )
+})
+
+const emptyFunction = (): void => {
+  /** Intentional */
+}
+
+wss.on('connection', ws => {
+  ws.on('error', console.error)
+
+  ws.on('message', (message: RawData) => {
+    const { type, data } = JSON.parse(
+      // eslint-disable-next-line @typescript-eslint/no-base-to-string
+      message.toString()
+    ) as MessagePayload<DataPayload>
+    switch (type) {
+      case MessageType.echo:
+        requestHandlerPool
+          .execute({ data }, 'echo')
+          .then(response => {
+            ws.send(
+              JSON.stringify({
+                type: MessageType.echo,
+                data: response.data
+              })
+            )
+            return null
+          })
+          .catch(emptyFunction)
+        break
+      case MessageType.factorial:
+        requestHandlerPool
+          .execute({ data }, 'factorial')
+          .then(response => {
+            ws.send(
+              JSON.stringify({
+                type: MessageType.factorial,
+                data: response.data
+              })
+            )
+            return null
+          })
+          .catch(emptyFunction)
+        break
+    }
+  })
+})
diff --git a/examples/typescript/websocket-server-pool/ws/src/pool.ts b/examples/typescript/websocket-server-pool/ws/src/pool.ts
new file mode 100644 (file)
index 0000000..b29c6d1
--- /dev/null
@@ -0,0 +1,26 @@
+import { dirname, extname, join } from 'node:path'
+import { fileURLToPath } from 'node:url'
+import { DynamicThreadPool, availableParallelism } from 'poolifier'
+import {
+  type DataPayload,
+  type WorkerData,
+  type WorkerResponse
+} from './types.js'
+
+const workerFile = join(
+  dirname(fileURLToPath(import.meta.url)),
+  `worker${extname(fileURLToPath(import.meta.url))}`
+)
+
+export const requestHandlerPool = new DynamicThreadPool<
+WorkerData<DataPayload>,
+WorkerResponse<DataPayload>
+>(1, availableParallelism(), workerFile, {
+  enableTasksQueue: true,
+  tasksQueueOptions: {
+    concurrency: 8
+  },
+  errorHandler: (e: Error) => {
+    console.error(e)
+  }
+})
diff --git a/examples/typescript/websocket-server-pool/ws/src/types.ts b/examples/typescript/websocket-server-pool/ws/src/types.ts
new file mode 100644 (file)
index 0000000..4f821e6
--- /dev/null
@@ -0,0 +1,21 @@
+export enum MessageType {
+  echo = 'echo',
+  factorial = 'factorial'
+}
+
+export interface MessagePayload<T = unknown> {
+  type: MessageType
+  data: T
+}
+
+export interface DataPayload {
+  number?: number
+}
+
+export interface WorkerData<T = unknown> {
+  data: T
+}
+
+export interface WorkerResponse<T = unknown> {
+  data: T
+}
diff --git a/examples/typescript/websocket-server-pool/ws/src/worker.ts b/examples/typescript/websocket-server-pool/ws/src/worker.ts
new file mode 100644 (file)
index 0000000..92b9d64
--- /dev/null
@@ -0,0 +1,36 @@
+import { ThreadWorker } from 'poolifier'
+import {
+  type DataPayload,
+  type WorkerData,
+  type WorkerResponse
+} from './types.js'
+
+const factorial: (n: number) => number = n => {
+  if (n === 0) {
+    return 1
+  }
+  return factorial(n - 1) * n
+}
+
+class RequestHandlerWorker<
+  Data extends WorkerData<DataPayload>,
+  Response extends WorkerResponse<DataPayload>
+> extends ThreadWorker<Data, Response> {
+  public constructor () {
+    super({
+      echo: (workerData?: Data) => {
+        return workerData as unknown as Response
+      },
+      factorial: (workerData?: Data) => {
+        return {
+          data: { number: factorial(workerData?.data?.number as number) }
+        } as unknown as Response
+      }
+    })
+  }
+}
+
+export const requestHandlerWorker = new RequestHandlerWorker<
+WorkerData<DataPayload>,
+WorkerResponse<DataPayload>
+>()
diff --git a/examples/typescript/websocket-server-pool/ws/tsconfig.json b/examples/typescript/websocket-server-pool/ws/tsconfig.json
new file mode 100644 (file)
index 0000000..57e49a1
--- /dev/null
@@ -0,0 +1,15 @@
+{
+  "$schema": "https://json.schemastore.org/tsconfig",
+  "compilerOptions": {
+    "target": "ES2022",
+    "module": "ES2022",
+    "moduleResolution": "Node16",
+    "verbatimModuleSyntax": true,
+    "rootDir": "./src",
+    "outDir": "./dist",
+    "esModuleInterop": true,
+    "forceConsistentCasingInFileNames": true,
+    "strict": true,
+    "skipLibCheck": true
+  }
+}