├── .nvmrc ├── .vscode └── settings.json ├── packages ├── rsocket-composite-metadata │ ├── jest.setup.ts │ ├── tsconfig.json │ ├── README.md │ ├── tsconfig.build.json │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ └── encodeWellKnownMetadataHeader.ts.snap │ │ ├── test-utils │ │ │ └── hex.ts │ │ ├── encodeWellKnownMetadataHeader.ts │ │ ├── encodeCustomMetadataHeader.ts │ │ ├── encodeAndAddWellKnownMetadata.ts │ │ └── encodeAndAddCustomMetadata.spec.ts │ ├── jest.config.ts │ ├── package.json │ └── src │ │ ├── index.ts │ │ ├── RoutingMetadata.ts │ │ └── WellKnownAuthType.ts ├── rsocket-messaging │ ├── tsconfig.json │ ├── tsconfig.build.json │ ├── README.md │ └── package.json ├── rsocket-tcp-client │ ├── tsconfig.json │ ├── tsconfig.build.json │ ├── README.md │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── __mocks__ │ │ │ └── net.ts │ │ ├── __tests__ │ │ │ ├── __snapshots__ │ │ │ │ └── TcpDuplexConnection.spec.ts.snap │ │ │ └── TcpClientTransport.spec.ts │ │ ├── TcpClientTransport.ts │ │ └── TcpDuplexConnection.ts │ └── jest.config.ts ├── rsocket-tcp-server │ ├── tsconfig.json │ ├── tsconfig.build.json │ ├── README.md │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── __tests__ │ │ │ └── TcpServerTransport.spec.ts │ │ ├── TcpServerTransport.ts │ │ └── TcpDuplexConnection.ts │ └── jest.config.ts ├── rsocket-adapter-rxjs │ ├── tsconfig.json │ ├── src │ │ ├── index.ts │ │ ├── Utils.ts │ │ ├── ObserverToRSocketSubscriber.ts │ │ ├── RSocketPublisherToObservable.ts │ │ ├── RSocketPublisherToPrefetchingObservable.ts │ │ ├── RSocketPublisher2PrefetchingObservableToObserver2BufferingRSocketSubscriber.ts │ │ ├── Observer2BufferingSubscriberToPublisher2PrefetchingObservable.ts │ │ └── ObserverToBufferingRSocketSubscriber.ts │ ├── tsconfig.build.json │ ├── README.md │ └── package.json ├── rsocket-websocket-server │ ├── tsconfig.json │ ├── tsconfig.build.json │ ├── README.md │ ├── src │ │ ├── index.ts │ │ ├── WebsocketServerTransport.ts │ │ └── WebsocketDuplexConnection.ts │ └── package.json ├── rsocket-graphql-apollo-link │ ├── tsconfig.json │ ├── tsconfig.build.json │ ├── README.md │ ├── package.json │ └── src │ │ ├── index.ts │ │ ├── RSocketLink.ts │ │ ├── RSocketQueryLink.ts │ │ └── RSocketSubscriptionLink.ts ├── rsocket-graphql-apollo-server │ ├── tsconfig.json │ ├── src │ │ ├── index.ts │ │ ├── RSocketApolloGraphlQLPlugin.ts │ │ └── utilities.ts │ ├── README.md │ ├── tsconfig.build.json │ └── package.json ├── rsocket-core │ ├── jest.d.ts │ ├── jest.setup.ts │ ├── tsconfig.json │ ├── README.md │ ├── tsconfig.build.json │ ├── package.json │ ├── jest.config.ts │ ├── __tests__ │ │ ├── KeepAliveSender.spec.ts │ │ ├── test-utils │ │ │ ├── MockStream.ts │ │ │ └── toMatchYields.ts │ │ ├── Codec.spec.ts │ │ ├── KeepAliveHandler.spec.ts │ │ ├── __snapshots__ │ │ │ └── ClientServerMultiplexerDemultiplexer.spec.ts.snap │ │ ├── RSocketServer.spec.ts │ │ └── ClientServerMultiplexerDemultiplexer.spec.ts │ └── src │ │ ├── Lease.ts │ │ ├── index.ts │ │ ├── Errors.ts │ │ ├── Common.ts │ │ ├── Deferred.ts │ │ ├── Reassembler.ts │ │ ├── Resume.ts │ │ ├── RSocket.ts │ │ └── Transport.ts ├── rsocket-websocket-client │ ├── tsconfig.json │ ├── tsconfig.build.json │ ├── README.md │ ├── package.json │ ├── jest.config.ts │ └── src │ │ ├── index.ts │ │ ├── __mocks__ │ │ └── ws.ts │ │ ├── __tests__ │ │ └── __snapshots__ │ │ │ └── WebsocketDuplexConnection.spec.ts.snap │ │ ├── WebsocketClientTransport.ts │ │ └── WebsocketDuplexConnection.ts └── rsocket-examples │ ├── tsconfig.json │ ├── tsconfig.build.json │ ├── src │ ├── webpack │ │ ├── simple │ │ │ ├── client │ │ │ │ ├── index.html │ │ │ │ ├── package.json │ │ │ │ ├── webpack.config.js │ │ │ │ └── index.js │ │ │ └── server │ │ │ │ ├── package.json │ │ │ │ ├── server.js │ │ │ │ └── yarn.lock │ │ └── browser-bundle │ │ │ ├── src │ │ │ ├── index.html │ │ │ ├── rsocket.js │ │ │ └── app.js │ │ │ ├── package.json │ │ │ ├── webpack.config.js │ │ │ └── README.md │ ├── graphql │ │ └── apollo │ │ │ ├── client-server │ │ │ ├── schema.graphql │ │ │ └── resolvers.ts │ │ │ └── client │ │ │ └── example.ts │ ├── shared │ │ └── logger.ts │ ├── tcp │ │ ├── ClienRequestFnfnWithLeaseExampleTcp.ts │ │ └── ClientServerRequestResponseExampleTcp.ts │ ├── ClientRequestChannelExample.ts │ ├── websocket │ │ └── ClientServerRequestResponseExampleWebSocket.ts │ └── ClientCompositeMetadataRouteExample.ts │ └── package.json ├── .eslintignore ├── .prettierrc ├── tsconfig.json ├── .editorconfig ├── .eslintrc.js ├── lerna.json ├── tsconfig.build.json ├── package.json ├── RELEASE.md ├── .github └── workflows │ ├── build.yml │ └── release.yml ├── README.md └── .gitignore /.nvmrc: -------------------------------------------------------------------------------- 1 | v18.12.1 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.autoSave": "afterDelay" 3 | } -------------------------------------------------------------------------------- /packages/rsocket-composite-metadata/jest.setup.ts: -------------------------------------------------------------------------------- 1 | expect.extend({}); 2 | -------------------------------------------------------------------------------- /packages/rsocket-messaging/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/rsocket-tcp-client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/rsocket-tcp-server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/rsocket-adapter-rxjs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/rsocket-websocket-server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/coverage/** 2 | **/node_modules/** 3 | dist 4 | build 5 | scripts/ 6 | examples/ 7 | -------------------------------------------------------------------------------- /packages/rsocket-graphql-apollo-link/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/rsocket-graphql-apollo-server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/rsocket-graphql-apollo-server/src/index.ts: -------------------------------------------------------------------------------- 1 | export { RSocketApolloServer } from "./RSocketApolloServer"; 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": false 6 | } 7 | -------------------------------------------------------------------------------- /packages/rsocket-adapter-rxjs/src/index.ts: -------------------------------------------------------------------------------- 1 | export * as RxRequestersFactory from "./Requesters"; 2 | export * as RxRespondersFactory from "./Responders"; 3 | -------------------------------------------------------------------------------- /packages/rsocket-core/jest.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace jest { 2 | interface Matchers { 3 | toMatchYields(expectedYieldValues: any[]): R; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/rsocket-websocket-client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["ws"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/rsocket-core/jest.setup.ts: -------------------------------------------------------------------------------- 1 | import toMatchYields from "./__tests__/test-utils/toMatchYields"; 2 | 3 | expect.extend({ 4 | toMatchYields: toMatchYields, 5 | }); 6 | -------------------------------------------------------------------------------- /packages/rsocket-core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "downlevelIteration": true, 5 | "types": ["node", "jest"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/rsocket-composite-metadata/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "downlevelIteration": true, 5 | "types": ["node", "jest"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/rsocket-examples/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | 4 | "compilerOptions": { 5 | "downlevelIteration": true, 6 | "sourceMap": true, 7 | "target": "ESNext" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/rsocket-adapter-rxjs/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.build.json", 3 | 4 | "compilerOptions": { 5 | "outDir": "./dist" 6 | }, 7 | 8 | "include": [ 9 | "src/**/*" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /packages/rsocket-core/README.md: -------------------------------------------------------------------------------- 1 | This package is published from the rsocket-js monorepo. 2 | 3 | Please refer to rsocket-js on [GitHub](https://github.com/rsocket/rsocket-js) or [rsocket.io](https://rsocket.io) for more information. 4 | -------------------------------------------------------------------------------- /packages/rsocket-messaging/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.build.json", 3 | 4 | "compilerOptions": { 5 | "outDir": "./dist" 6 | }, 7 | 8 | "include": [ 9 | "src/**/*" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /packages/rsocket-tcp-client/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.build.json", 3 | 4 | "compilerOptions": { 5 | "outDir": "./dist" 6 | }, 7 | 8 | "include": [ 9 | "src/**/*" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /packages/rsocket-tcp-server/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.build.json", 3 | 4 | "compilerOptions": { 5 | "outDir": "./dist" 6 | }, 7 | 8 | "include": [ 9 | "src/**/*" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /packages/rsocket-messaging/README.md: -------------------------------------------------------------------------------- 1 | This package is published from the rsocket-js monorepo. 2 | 3 | Please refer to rsocket-js on [GitHub](https://github.com/rsocket/rsocket-js) or [rsocket.io](https://rsocket.io) for more information. 4 | -------------------------------------------------------------------------------- /packages/rsocket-tcp-client/README.md: -------------------------------------------------------------------------------- 1 | This package is published from the rsocket-js monorepo. 2 | 3 | Please refer to rsocket-js on [GitHub](https://github.com/rsocket/rsocket-js) or [rsocket.io](https://rsocket.io) for more information. 4 | -------------------------------------------------------------------------------- /packages/rsocket-tcp-server/README.md: -------------------------------------------------------------------------------- 1 | This package is published from the rsocket-js monorepo. 2 | 3 | Please refer to rsocket-js on [GitHub](https://github.com/rsocket/rsocket-js) or [rsocket.io](https://rsocket.io) for more information. 4 | -------------------------------------------------------------------------------- /packages/rsocket-websocket-client/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.build.json", 3 | 4 | "compilerOptions": { 5 | "outDir": "./dist" 6 | }, 7 | 8 | "include": [ 9 | "src/**/*" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /packages/rsocket-websocket-server/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.build.json", 3 | 4 | "compilerOptions": { 5 | "outDir": "./dist" 6 | }, 7 | 8 | "include": [ 9 | "src/**/*" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.build.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "rsocket-*": [ 7 | "packages/rsocket-*/src" 8 | ] 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/rsocket-adapter-rxjs/README.md: -------------------------------------------------------------------------------- 1 | This package is published from the rsocket-js monorepo. 2 | 3 | Please refer to rsocket-js on [GitHub](https://github.com/rsocket/rsocket-js) or [rsocket.io](https://rsocket.io) for more information. 4 | -------------------------------------------------------------------------------- /packages/rsocket-graphql-apollo-link/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.build.json", 3 | 4 | "compilerOptions": { 5 | "outDir": "./dist" 6 | }, 7 | 8 | "include": [ 9 | "src/**/*" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /packages/rsocket-websocket-client/README.md: -------------------------------------------------------------------------------- 1 | This package is published from the rsocket-js monorepo. 2 | 3 | Please refer to rsocket-js on [GitHub](https://github.com/rsocket/rsocket-js) or [rsocket.io](https://rsocket.io) for more information. 4 | -------------------------------------------------------------------------------- /packages/rsocket-websocket-server/README.md: -------------------------------------------------------------------------------- 1 | This package is published from the rsocket-js monorepo. 2 | 3 | Please refer to rsocket-js on [GitHub](https://github.com/rsocket/rsocket-js) or [rsocket.io](https://rsocket.io) for more information. 4 | -------------------------------------------------------------------------------- /packages/rsocket-composite-metadata/README.md: -------------------------------------------------------------------------------- 1 | This package is published from the rsocket-js monorepo. 2 | 3 | Please refer to rsocket-js on [GitHub](https://github.com/rsocket/rsocket-js) or [rsocket.io](https://rsocket.io) for more information. 4 | -------------------------------------------------------------------------------- /packages/rsocket-graphql-apollo-link/README.md: -------------------------------------------------------------------------------- 1 | This package is published from the rsocket-js monorepo. 2 | 3 | Please refer to rsocket-js on [GitHub](https://github.com/rsocket/rsocket-js) or [rsocket.io](https://rsocket.io) for more information. 4 | -------------------------------------------------------------------------------- /packages/rsocket-graphql-apollo-server/README.md: -------------------------------------------------------------------------------- 1 | This package is published from the rsocket-js monorepo. 2 | 3 | Please refer to rsocket-js on [GitHub](https://github.com/rsocket/rsocket-js) or [rsocket.io](https://rsocket.io) for more information. 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | 2 | [*] 3 | indent_style = space 4 | indent_size = 2 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.md] 11 | indent_size = 4 12 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: "@typescript-eslint/parser", 3 | plugins: ["prettier"], 4 | rules: { 5 | "prettier/prettier": [ 6 | "error", 7 | { 8 | endOfLine: "auto", 9 | }, 10 | ], 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /packages/rsocket-core/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.build.json", 3 | 4 | "compilerOptions": { 5 | "outDir": "./dist", 6 | "downlevelIteration": true 7 | }, 8 | 9 | "include": [ 10 | "src/**/*" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /packages/rsocket-examples/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.build.json", 3 | 4 | "compilerOptions": { 5 | "outDir": "./dist", 6 | "downlevelIteration": true, 7 | }, 8 | 9 | "include": [ 10 | "src/**/*" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /packages/rsocket-graphql-apollo-server/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.build.json", 3 | 4 | "compilerOptions": { 5 | "target": "ES6", 6 | "outDir": "./dist" 7 | }, 8 | 9 | "include": [ 10 | "src/**/*" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /packages/rsocket-composite-metadata/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.build.json", 3 | 4 | "compilerOptions": { 5 | "outDir": "./dist", 6 | "downlevelIteration": true 7 | }, 8 | 9 | "include": [ 10 | "src/**/*" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /packages/rsocket-composite-metadata/__tests__/__snapshots__/encodeWellKnownMetadataHeader.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`encodeWellKnownMetadataHeader encodes the header as per spec 1`] = ` 4 | [ 5 | 133, 6 | 0, 7 | 0, 8 | 16, 9 | ] 10 | `; 11 | -------------------------------------------------------------------------------- /packages/rsocket-examples/src/webpack/simple/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | RSocket Webpack Example 6 | 7 | 8 |

RSocket Webpack Example

9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "version": "independent", 6 | "useWorkspaces": true, 7 | "npmClient": "yarn", 8 | "command": { 9 | "publish": { 10 | "message": "chore(release): release" 11 | }, 12 | "version": { 13 | "push": false, 14 | "allowBranch": ["main", "1.0.x-alpha"] 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES6", 5 | "sourceMap": true, 6 | "noEmitOnError": true, 7 | "skipLibCheck": true, 8 | "esModuleInterop": true, 9 | "types": ["node", "jest"], 10 | "declaration": true, 11 | "resolveJsonModule": true, 12 | "downlevelIteration": true 13 | }, 14 | "exclude": [ 15 | "node_modules", 16 | "dist", 17 | "**/__tests__" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /packages/rsocket-examples/src/webpack/browser-bundle/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | RSocket Webpack Example 6 | 7 | 8 |

RSocket Webpack Example

9 |
10 | 11 | 12 | 13 |
14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /packages/rsocket-composite-metadata/__tests__/test-utils/hex.ts: -------------------------------------------------------------------------------- 1 | function numHex(s) { 2 | let a = s.toString(16); 3 | if (a.length % 2 > 0) { 4 | a = "0" + a; 5 | } 6 | return a; 7 | } 8 | 9 | function strHex(s) { 10 | let a = ""; 11 | for (let i = 0; i < s.length; i++) { 12 | a = a + numHex(s.charCodeAt(i)); 13 | } 14 | 15 | return a; 16 | } 17 | 18 | const alphabetNumeric = "abcdefghijklmnopqrstuvqxyz0123456789"; 19 | 20 | export const hex: any = {}; 21 | 22 | alphabetNumeric.split("").forEach((c) => { 23 | hex[c] = strHex(c); 24 | }); 25 | -------------------------------------------------------------------------------- /packages/rsocket-examples/src/webpack/browser-bundle/src/rsocket.js: -------------------------------------------------------------------------------- 1 | import { RSocketConnector } from "rsocket-core"; 2 | import { WebsocketClientTransport } from "rsocket-websocket-client"; 3 | 4 | export async function connect(transportOptions) { 5 | const connector = new RSocketConnector({ 6 | transport: new WebsocketClientTransport({ 7 | wsCreator: (url) => new WebSocket(url), 8 | ...transportOptions, 9 | }), 10 | }); 11 | 12 | return connector.connect(); 13 | } 14 | 15 | export function createBuffer(value) { 16 | return Buffer.from(value); 17 | } 18 | -------------------------------------------------------------------------------- /packages/rsocket-composite-metadata/__tests__/encodeWellKnownMetadataHeader.ts: -------------------------------------------------------------------------------- 1 | import { 2 | encodeWellKnownMetadataHeader, 3 | WellKnownMimeType, 4 | } from "rsocket-composite-metadata"; 5 | 6 | describe("encodeWellKnownMetadataHeader", () => { 7 | it("encodes the header as per spec", () => { 8 | const header = encodeWellKnownMetadataHeader( 9 | WellKnownMimeType.APPLICATION_JSON.identifier, 10 | WellKnownMimeType.APPLICATION_JSON.toString().length 11 | ); 12 | const actual = header.toJSON().data; 13 | expect(actual).toMatchSnapshot(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /packages/rsocket-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rsocket-core", 3 | "version": "1.0.0-alpha.3", 4 | "license": "Apache-2.0", 5 | "main": "dist/index", 6 | "types": "dist/index", 7 | "files": [ 8 | "dist", 9 | "LICENSE" 10 | ], 11 | "publishConfig": { 12 | "access": "public" 13 | }, 14 | "scripts": { 15 | "build": "yarn run clean && yarn run compile", 16 | "clean": "rimraf -rf ./dist", 17 | "compile": "tsc -p tsconfig.build.json", 18 | "prepublishOnly": "yarn run build", 19 | "test": "yarn jest" 20 | }, 21 | "devDependencies": { 22 | "rimraf": "~3.0.2", 23 | "typescript": "~4.5.2" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/rsocket-core/jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "@jest/types"; 2 | import { pathsToModuleNameMapper } from "ts-jest"; 3 | import { compilerOptions } from "../../tsconfig.json"; 4 | 5 | const config: Config.InitialOptions = { 6 | preset: "ts-jest", 7 | moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { 8 | // This has to match the baseUrl defined in tsconfig.json. 9 | prefix: "/../../", 10 | }), 11 | modulePathIgnorePatterns: ["/__tests__/test-utils"], 12 | collectCoverage: true, 13 | collectCoverageFrom: ["/src/**/*.ts", "!**/node_modules/**"], 14 | setupFilesAfterEnv: ["/jest.setup.ts"], 15 | }; 16 | 17 | export default config; 18 | -------------------------------------------------------------------------------- /packages/rsocket-composite-metadata/jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "@jest/types"; 2 | import { pathsToModuleNameMapper } from "ts-jest"; 3 | import { compilerOptions } from "../../tsconfig.json"; 4 | 5 | const config: Config.InitialOptions = { 6 | preset: "ts-jest", 7 | moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { 8 | // This has to match the baseUrl defined in tsconfig.json. 9 | prefix: "/../../", 10 | }), 11 | modulePathIgnorePatterns: ["/__tests__/test-utils"], 12 | collectCoverage: true, 13 | collectCoverageFrom: ["/src/**/*.ts", "!**/node_modules/**"], 14 | setupFilesAfterEnv: ["/jest.setup.ts"], 15 | }; 16 | 17 | export default config; 18 | -------------------------------------------------------------------------------- /packages/rsocket-core/__tests__/KeepAliveSender.spec.ts: -------------------------------------------------------------------------------- 1 | import { KeepAliveSender } from "../src/RSocketSupport"; 2 | import { mock } from "jest-mock-extended"; 3 | import { Closeable, Outbound } from "../src"; 4 | 5 | jest.useFakeTimers(); 6 | 7 | describe("KeepAliveSender", () => { 8 | it("Sends a keep alive frame on the configured interval", () => { 9 | const expectedFrames = 3; 10 | const keepAlivePeriod = 100; 11 | const mockOutbound = mock(); 12 | const sender = new KeepAliveSender(mockOutbound, keepAlivePeriod); 13 | 14 | sender.start(); 15 | 16 | jest.advanceTimersByTime(expectedFrames * keepAlivePeriod); 17 | 18 | expect(mockOutbound.send).toBeCalledTimes(expectedFrames); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /packages/rsocket-composite-metadata/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rsocket-composite-metadata", 3 | "version": "1.0.0-alpha.3", 4 | "license": "Apache-2.0", 5 | "main": "dist/index", 6 | "types": "dist/index", 7 | "files": [ 8 | "dist", 9 | "LICENSE" 10 | ], 11 | "publishConfig": { 12 | "access": "public" 13 | }, 14 | "scripts": { 15 | "build": "yarn run clean && yarn run compile", 16 | "clean": "rimraf -rf ./dist", 17 | "compile": "tsc -p tsconfig.build.json", 18 | "prepublishOnly": "yarn run build", 19 | "test": "yarn jest" 20 | }, 21 | "dependencies": { 22 | "rsocket-core": "^1.0.0-alpha.3" 23 | }, 24 | "devDependencies": { 25 | "rimraf": "~3.0.2", 26 | "typescript": "~4.5.2" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/rsocket-tcp-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rsocket-tcp-client", 3 | "version": "1.0.0-alpha.3", 4 | "license": "Apache-2.0", 5 | "main": "dist/index", 6 | "types": "dist/index", 7 | "files": [ 8 | "dist", 9 | "LICENSE", 10 | "README.md" 11 | ], 12 | "publishConfig": { 13 | "access": "public" 14 | }, 15 | "scripts": { 16 | "build": "yarn run clean && yarn run compile", 17 | "clean": "rimraf -rf ./dist", 18 | "compile": "tsc -p tsconfig.build.json", 19 | "prepublishOnly": "yarn run build", 20 | "test": "jest" 21 | }, 22 | "dependencies": { 23 | "rsocket-core": "^1.0.0-alpha.3" 24 | }, 25 | "devDependencies": { 26 | "rimraf": "~3.0.2", 27 | "typescript": "~4.5.2" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/rsocket-tcp-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rsocket-tcp-server", 3 | "version": "1.0.0-alpha.3", 4 | "license": "Apache-2.0", 5 | "main": "dist/index", 6 | "types": "dist/index", 7 | "files": [ 8 | "dist", 9 | "LICENSE", 10 | "README.md" 11 | ], 12 | "publishConfig": { 13 | "access": "public" 14 | }, 15 | "scripts": { 16 | "build": "yarn run clean && yarn run compile", 17 | "clean": "rimraf -rf ./dist", 18 | "compile": "tsc -p tsconfig.build.json", 19 | "prepublishOnly": "yarn run build", 20 | "test": "jest" 21 | }, 22 | "dependencies": { 23 | "rsocket-core": "^1.0.0-alpha.3" 24 | }, 25 | "devDependencies": { 26 | "rimraf": "~3.0.2", 27 | "typescript": "~4.5.2" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/rsocket-websocket-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rsocket-websocket-client", 3 | "version": "1.0.0-alpha.3", 4 | "license": "Apache-2.0", 5 | "main": "dist/index", 6 | "types": "dist/index", 7 | "files": [ 8 | "dist", 9 | "LICENSE", 10 | "README.md" 11 | ], 12 | "publishConfig": { 13 | "access": "public" 14 | }, 15 | "scripts": { 16 | "build": "yarn run clean && yarn run compile", 17 | "clean": "rimraf -rf ./dist", 18 | "compile": "tsc -p tsconfig.build.json", 19 | "prepublishOnly": "yarn run build", 20 | "test": "jest" 21 | }, 22 | "dependencies": { 23 | "rsocket-core": "^1.0.0-alpha.3" 24 | }, 25 | "devDependencies": { 26 | "rimraf": "~3.0.2", 27 | "typescript": "~4.5.2 " 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/rsocket-tcp-client/src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | "use strict"; 18 | 19 | export * from "./TcpClientTransport"; 20 | -------------------------------------------------------------------------------- /packages/rsocket-tcp-server/src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | "use strict"; 18 | 19 | export * from "./TcpServerTransport"; 20 | -------------------------------------------------------------------------------- /packages/rsocket-tcp-client/jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "@jest/types"; 2 | import { pathsToModuleNameMapper } from "ts-jest"; 3 | import { compilerOptions } from "../../tsconfig.json"; 4 | 5 | const config: Config.InitialOptions = { 6 | preset: "ts-jest", 7 | testRegex: "(\\/__tests__\\/.*|\\.(test|spec))\\.(ts)$", 8 | moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { 9 | // This has to match the baseUrl defined in tsconfig.json. 10 | prefix: "/../../", 11 | }), 12 | modulePathIgnorePatterns: [ 13 | "/__tests__/test-utils", 14 | "/__tests__/*.d.ts", 15 | ], 16 | collectCoverage: true, 17 | collectCoverageFrom: ["/src/**/*.ts", "!**/node_modules/**"], 18 | }; 19 | 20 | export default config; 21 | -------------------------------------------------------------------------------- /packages/rsocket-tcp-server/jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "@jest/types"; 2 | import { pathsToModuleNameMapper } from "ts-jest"; 3 | import { compilerOptions } from "../../tsconfig.json"; 4 | 5 | const config: Config.InitialOptions = { 6 | preset: "ts-jest", 7 | testRegex: "(\\/__tests__\\/.*|\\.(test|spec))\\.(ts)$", 8 | moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { 9 | // This has to match the baseUrl defined in tsconfig.json. 10 | prefix: "/../../", 11 | }), 12 | modulePathIgnorePatterns: [ 13 | "/__tests__/test-utils", 14 | "/__tests__/*.d.ts", 15 | ], 16 | collectCoverage: true, 17 | collectCoverageFrom: ["/src/**/*.ts", "!**/node_modules/**"], 18 | }; 19 | 20 | export default config; 21 | -------------------------------------------------------------------------------- /packages/rsocket-websocket-client/jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "@jest/types"; 2 | import { pathsToModuleNameMapper } from "ts-jest"; 3 | import { compilerOptions } from "../../tsconfig.json"; 4 | 5 | const config: Config.InitialOptions = { 6 | preset: "ts-jest", 7 | testRegex: "(\\/__tests__\\/.*|\\.(test|spec))\\.(ts)$", 8 | moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { 9 | // This has to match the baseUrl defined in tsconfig.json. 10 | prefix: "/../../", 11 | }), 12 | modulePathIgnorePatterns: [ 13 | "/__tests__/test-utils", 14 | "/__tests__/*.d.ts", 15 | ], 16 | collectCoverage: true, 17 | collectCoverageFrom: ["/src/**/*.ts", "!**/node_modules/**"], 18 | }; 19 | 20 | export default config; 21 | -------------------------------------------------------------------------------- /packages/rsocket-websocket-client/src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | "use strict"; 18 | 19 | export * from "./WebsocketClientTransport"; 20 | -------------------------------------------------------------------------------- /packages/rsocket-websocket-server/src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | "use strict"; 18 | 19 | export * from "./WebsocketServerTransport"; 20 | -------------------------------------------------------------------------------- /packages/rsocket-messaging/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rsocket-messaging", 3 | "version": "1.0.0-alpha.3", 4 | "license": "Apache-2.0", 5 | "main": "dist/index", 6 | "types": "dist/index", 7 | "files": [ 8 | "dist", 9 | "LICENSE" 10 | ], 11 | "publishConfig": { 12 | "access": "public" 13 | }, 14 | "scripts": { 15 | "build": "yarn run clean && yarn run compile", 16 | "clean": "rimraf -rf ./dist", 17 | "compile": "tsc -p tsconfig.build.json", 18 | "prepublishOnly": "yarn run build", 19 | "test": "echo \"Error: no test specified\" && exit 0" 20 | }, 21 | "dependencies": { 22 | "rsocket-composite-metadata": "^1.0.0-alpha.3", 23 | "rsocket-core": "^1.0.0-alpha.3" 24 | }, 25 | "devDependencies": { 26 | "rimraf": "~3.0.2", 27 | "typescript": "~4.5.2" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/rsocket-adapter-rxjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rsocket-adapter-rxjs", 3 | "version": "1.0.0-alpha.4", 4 | "license": "Apache-2.0", 5 | "main": "dist/index", 6 | "types": "dist/index", 7 | "files": [ 8 | "dist", 9 | "LICENSE" 10 | ], 11 | "publishConfig": { 12 | "access": "public" 13 | }, 14 | "scripts": { 15 | "build": "yarn run clean && yarn run compile", 16 | "clean": "rimraf -rf ./dist", 17 | "compile": "tsc -p tsconfig.build.json", 18 | "prepublishOnly": "yarn run build", 19 | "test": "echo \"Error: no test specified\" && exit 0" 20 | }, 21 | "dependencies": { 22 | "rsocket-core": "^1.0.0-alpha.3", 23 | "rsocket-messaging": "^1.0.0-alpha.3", 24 | "rxjs": "^7.4.0" 25 | }, 26 | "devDependencies": { 27 | "rimraf": "~3.0.2", 28 | "typescript": "~4.5.2" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/rsocket-examples/src/webpack/browser-bundle/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rsocket-examples-websocket-simple-client", 3 | "version": "0.0.0", 4 | "license": "Apache-2.0", 5 | "private": true, 6 | "files": [ 7 | "dist", 8 | "LICENSE" 9 | ], 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1", 12 | "build": "webpack", 13 | "serve": "webpack serve" 14 | }, 15 | "engines": { 16 | "node": "^18" 17 | }, 18 | "devDependencies": { 19 | "buffer": "^6.0.3", 20 | "rsocket-adapter-rxjs": "^1.0.0-alpha.4", 21 | "rsocket-composite-metadata": "^1.0.0-alpha.3", 22 | "rsocket-core": "^1.0.0-alpha.3", 23 | "rsocket-websocket-client": "^1.0.0-alpha.3", 24 | "webpack": "^5", 25 | "webpack-cli": "^5", 26 | "webpack-dev-server": "^5", 27 | "html-webpack-plugin": "^5" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/rsocket-websocket-client/src/__mocks__/ws.ts: -------------------------------------------------------------------------------- 1 | import { CloseEvent, ErrorEvent, MessageEvent } from "ws"; 2 | 3 | const EventEmitter = require("events"); 4 | 5 | export class MockSocket extends EventEmitter { 6 | send = jest.fn(); 7 | 8 | close = jest.fn(); 9 | 10 | addEventListener = (name, handler) => { 11 | this.on(name, handler); 12 | }; 13 | 14 | removeEventListener = (name, handler) => { 15 | this.on(name, handler); 16 | }; 17 | 18 | mock = { 19 | close: (event: Partial) => { 20 | this.emit("close", event); 21 | }, 22 | open: () => { 23 | this.emit("connect"); 24 | }, 25 | message: (message: Partial) => { 26 | this.emit("message", message); 27 | }, 28 | error: (event: Partial) => { 29 | this.emit("error", event); 30 | }, 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /packages/rsocket-websocket-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rsocket-websocket-server", 3 | "version": "1.0.0-alpha.3", 4 | "license": "Apache-2.0", 5 | "main": "dist/index", 6 | "types": "dist/index", 7 | "files": [ 8 | "dist", 9 | "LICENSE", 10 | "README.md" 11 | ], 12 | "publishConfig": { 13 | "access": "public" 14 | }, 15 | "scripts": { 16 | "build": "yarn run clean && yarn run compile", 17 | "clean": "rimraf -rf ./dist", 18 | "compile": "tsc -p tsconfig.build.json", 19 | "prepublishOnly": "yarn run build", 20 | "test": "echo \"Error: no test specified\" && exit 0" 21 | }, 22 | "dependencies": { 23 | "rsocket-core": "^1.0.0-alpha.3", 24 | "ws": "~8.2.3" 25 | }, 26 | "devDependencies": { 27 | "@types/ws": "^8.2.0", 28 | "rimraf": "~3.0.2", 29 | "typescript": "~4.5.2" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/rsocket-examples/src/webpack/simple/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rsocket-examples-websocket-simple-client", 3 | "version": "0.0.0", 4 | "license": "Apache-2.0", 5 | "private": true, 6 | "files": [ 7 | "dist", 8 | "LICENSE" 9 | ], 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1", 12 | "build": "webpack", 13 | "serve": "webpack serve" 14 | }, 15 | "engines": { 16 | "node": "^18" 17 | }, 18 | "devDependencies": { 19 | "buffer": "^6.0.3", 20 | "rsocket-adapter-rxjs": "^1.0.0-alpha.4", 21 | "rsocket-composite-metadata": "^1.0.0-alpha.3", 22 | "rsocket-core": "^1.0.0-alpha.3", 23 | "rsocket-websocket-client": "^1.0.0-alpha.3", 24 | "webpack": "^5.74.0", 25 | "webpack-cli": "^5", 26 | "webpack-dev-server": "^5", 27 | "html-webpack-plugin": "^5" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/rsocket-examples/src/webpack/simple/client/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const webpack = require("webpack"); 3 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 4 | 5 | module.exports = { 6 | entry: "./index.js", 7 | mode: "development", 8 | output: { 9 | filename: "main.js", 10 | path: path.resolve(__dirname, "dist"), 11 | }, 12 | devtool: "source-map", 13 | devServer: { 14 | static: { 15 | directory: path.join(__dirname, "dist"), 16 | }, 17 | compress: false, 18 | port: 9000, 19 | }, 20 | resolve: { 21 | fallback: { 22 | buffer: require.resolve("buffer/"), 23 | }, 24 | }, 25 | plugins: [ 26 | new HtmlWebpackPlugin({ 27 | template: "./index.html", 28 | }), 29 | new webpack.ProvidePlugin({ 30 | Buffer: ["buffer", "Buffer"], 31 | }), 32 | ], 33 | }; 34 | -------------------------------------------------------------------------------- /packages/rsocket-graphql-apollo-link/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rsocket-graphql-apollo-link", 3 | "version": "1.0.0-alpha.1", 4 | "license": "Apache-2.0", 5 | "main": "dist/index", 6 | "types": "dist/index", 7 | "files": [ 8 | "dist", 9 | "LICENSE", 10 | "README.md" 11 | ], 12 | "scripts": { 13 | "build": "yarn run clean && yarn run compile", 14 | "clean": "rimraf -rf ./dist", 15 | "compile": "tsc -p tsconfig.build.json", 16 | "prepublishOnly": "yarn run build", 17 | "test": "echo \"Error: no test specified\" && exit 0" 18 | }, 19 | "dependencies": { 20 | "@apollo/client": "^3.5.10", 21 | "rsocket-composite-metadata": "^1.0.0-alpha.1", 22 | "rsocket-core": "^1.0.0-alpha.1", 23 | "graphql": "^16.3.0" 24 | }, 25 | "devDependencies": { 26 | "rimraf": "~3.0.2", 27 | "typescript": "~4.5.2" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/rsocket-tcp-client/src/__mocks__/net.ts: -------------------------------------------------------------------------------- 1 | const EventEmitter = require("events"); 2 | 3 | export class MockSocket extends EventEmitter { 4 | end = jest.fn(() => { 5 | // 'end' is only emitted when a FIN packet is received 6 | this.emit("close"); 7 | }); 8 | 9 | write = jest.fn(); 10 | 11 | mock = { 12 | close: () => { 13 | this.emit("close"); 14 | }, 15 | connect: () => { 16 | this.emit("connect"); 17 | }, 18 | data: (data) => { 19 | this.emit("data", data); 20 | }, 21 | error: (error) => { 22 | this.emit("error", error); 23 | }, 24 | }; 25 | 26 | destroy() {} 27 | } 28 | 29 | export const net = { 30 | connect: jest.fn(() => { 31 | const socket = new MockSocket(); 32 | net.socket = socket; // for easy accessibility in tests 33 | return socket; 34 | }), 35 | socket: null, 36 | }; 37 | -------------------------------------------------------------------------------- /packages/rsocket-examples/src/webpack/simple/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rsocket-examples-websocket-simple-server", 3 | "version": "0.0.0", 4 | "license": "Apache-2.0", 5 | "private": true, 6 | "files": [ 7 | "dist", 8 | "LICENSE" 9 | ], 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1", 12 | "start": "node server.js" 13 | }, 14 | "engines": { 15 | "node": "^18" 16 | }, 17 | "dependencies": { 18 | "rsocket-adapter-rxjs": "^1.0.0-alpha.4", 19 | "rsocket-composite-metadata": "^1.0.0-alpha.3", 20 | "rsocket-core": "^1.0.0-alpha.3", 21 | "rsocket-tcp-client": "^1.0.0-alpha.3", 22 | "rsocket-tcp-server": "^1.0.0-alpha.3", 23 | "rsocket-websocket-client": "^1.0.0-alpha.3", 24 | "rsocket-websocket-server": "^1.0.0-alpha.3", 25 | "ws": "^8.18" 26 | }, 27 | "devDependencies": { 28 | 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/rsocket-composite-metadata/src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export * from "./CompositeMetadata"; 18 | export * from "./WellKnownMimeType"; 19 | export * from "./AuthMetadata"; 20 | export * from "./RoutingMetadata"; 21 | export * from "./WellKnownAuthType"; 22 | -------------------------------------------------------------------------------- /packages/rsocket-graphql-apollo-link/src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export { makeRSocketLink, makeRSocketLinkConfig } from "./RSocketLink"; 18 | export { RSocketQueryLink } from "./RSocketQueryLink"; 19 | export { RSocketSubscriptionLink } from "./RSocketSubscriptionLink"; 20 | -------------------------------------------------------------------------------- /packages/rsocket-core/src/Lease.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { StreamFrameHandler, StreamLifecycleHandler } from "./Transport"; 18 | 19 | export interface LeaseManager { 20 | requestLease(handler: StreamFrameHandler & StreamLifecycleHandler): void; 21 | cancelRequest(handler: StreamFrameHandler & StreamLifecycleHandler): void; 22 | } 23 | -------------------------------------------------------------------------------- /packages/rsocket-core/src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export * from "./Codecs"; 18 | export * from "./Common"; 19 | export * from "./Deferred"; 20 | export * from "./Errors"; 21 | export * from "./Frames"; 22 | export * from "./RSocket"; 23 | export * from "./RSocketConnector"; 24 | export * from "./RSocketServer"; 25 | export * from "./Transport"; 26 | -------------------------------------------------------------------------------- /packages/rsocket-examples/src/graphql/apollo/client-server/schema.graphql: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2021-2022 the original author or authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | """ 16 | 17 | type ChatMessage { 18 | message: String 19 | } 20 | 21 | type Query { 22 | echo(message: String): ChatMessage 23 | } 24 | 25 | type Mutation { 26 | createMessage(message: String): ChatMessage 27 | } 28 | 29 | type Subscription { 30 | messageCreated: ChatMessage 31 | } 32 | -------------------------------------------------------------------------------- /packages/rsocket-graphql-apollo-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rsocket-graphql-apollo-server", 3 | "version": "1.0.0-alpha.1", 4 | "license": "Apache-2.0", 5 | "main": "dist/index", 6 | "types": "dist/index", 7 | "files": [ 8 | "dist", 9 | "LICENSE", 10 | "README.md" 11 | ], 12 | "scripts": { 13 | "build": "yarn run clean && yarn run compile", 14 | "clean": "rimraf -rf ./dist", 15 | "compile": "tsc -p tsconfig.build.json", 16 | "prepublishOnly": "yarn run build", 17 | "test": "echo \"Error: no test specified\" && exit 0" 18 | }, 19 | "dependencies": { 20 | "@apollo/client": "^3.5.10", 21 | "@graphql-tools/schema": "^8.3.5", 22 | "rsocket-composite-metadata": "^1.0.0-alpha.1", 23 | "rsocket-core": "^1.0.0-alpha.1", 24 | "apollo-server-plugin-base": "^3.6.2", 25 | "apollo-server-core": "^3.6.6", 26 | "apollo-server-env": "^4.2.1", 27 | "graphql": "^16.3.0", 28 | "rxjs": "^7.5" 29 | }, 30 | "devDependencies": { 31 | "rimraf": "~3.0.2", 32 | "typescript": "~4.5.2" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rsocket-js", 3 | "description": "A JavaScript implementation of the RSocket protocol (https://github.com/rsocket/rsocket).", 4 | "private": true, 5 | "license": "Apache-2.0", 6 | "workspaces": [ 7 | "packages/*" 8 | ], 9 | "scripts": { 10 | "build": "lerna run build", 11 | "clean": "lerna run clean && rimraf -rf ./coverage", 12 | "pub": "lerna publish", 13 | "pretest": "yarn clean", 14 | "test": "lerna run test", 15 | "lint": "eslint --ext js,ts .", 16 | "lint:fix": "npm run lint -- --fix" 17 | }, 18 | "devDependencies": { 19 | "@types/jest": "^27.0.3", 20 | "@types/node": "^17.0.5", 21 | "@typescript-eslint/parser": "^5.8.0", 22 | "eslint": "~8.5.0", 23 | "eslint-plugin-import": "~2.25.3", 24 | "eslint-plugin-prettier": "^4.0.0", 25 | "jest": "^29.7.0", 26 | "jest-config": "^29.7.0", 27 | "jest-mock-extended": "^3.0.7", 28 | "lerna": "^4.0.0", 29 | "prettier": "^2.5.1", 30 | "sinon": "^12.0.1", 31 | "ts-jest": "^29.2.5", 32 | "typescript": "~4.5.4" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/rsocket-websocket-client/src/__tests__/__snapshots__/WebsocketDuplexConnection.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`WebsocketDuplexConnection when receiving data when buffer contains a single frame deserializes received frames and calls the configured handler 1`] = ` 4 | { 5 | "data": { 6 | "data": [ 7 | 104, 8 | 101, 9 | 108, 10 | 108, 11 | 111, 12 | 32, 13 | 119, 14 | 111, 15 | 114, 16 | 108, 17 | 100, 18 | ], 19 | "type": "Buffer", 20 | }, 21 | "dataMimeType": "application/octet-stream", 22 | "flags": 256, 23 | "keepAlive": 60000, 24 | "lifetime": 300000, 25 | "majorVersion": 1, 26 | "metadata": { 27 | "data": [ 28 | 104, 29 | 101, 30 | 108, 31 | 108, 32 | 111, 33 | 32, 34 | 119, 35 | 111, 36 | 114, 37 | 108, 38 | 100, 39 | ], 40 | "type": "Buffer", 41 | }, 42 | "metadataMimeType": "application/octet-stream", 43 | "minorVersion": 0, 44 | "resumeToken": null, 45 | "streamId": 0, 46 | "type": 1, 47 | } 48 | `; 49 | -------------------------------------------------------------------------------- /packages/rsocket-tcp-server/src/__tests__/TcpServerTransport.spec.ts: -------------------------------------------------------------------------------- 1 | // import { TcpDuplexConnection } from "../TcpDuplexConnection"; 2 | // import sinon from "sinon"; 3 | // import EventEmitter from "events"; 4 | // import { DuplexConnection } from "rsocket-core/src"; 5 | 6 | describe("TcpClientTransport", function () { 7 | describe("connect", () => { 8 | it.skip("resolves to an instance of DuplexConnection on successful connection", async () => { 9 | // arrange 10 | // const netStub = new EventEmitter(); 11 | // const socketStub = sinon.createStubInstance(ws.Socket); 12 | // const transport = new TcpServerTransport({ 13 | // listenOptions: undefined, 14 | // serverOptions: undefined, 15 | // socketCreator(): ws.Server { 16 | // return undefined; 17 | // }, 18 | // }); 19 | // act 20 | // const connectionPromise = await transport.bind((connection) => {}); 21 | // netStub.emit("connect", socketStub); 22 | // 23 | // // assert 24 | // await expect(connectionPromise).resolves.toBeInstanceOf( 25 | // TcpDuplexConnection 26 | // ); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /packages/rsocket-examples/src/webpack/browser-bundle/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const webpack = require("webpack"); 3 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 4 | 5 | module.exports = { 6 | mode: "development", 7 | entry: { 8 | rsocket: { 9 | import: "./src/rsocket.js", 10 | library: { 11 | type: "window", 12 | name: "rsocket", 13 | }, 14 | }, 15 | app: "./src/app.js", 16 | }, 17 | output: { 18 | filename: "[name].js", // [name] will be replaced by 'app' or 'rsocket' 19 | path: path.resolve(__dirname, "dist"), 20 | }, 21 | devtool: "source-map", 22 | devServer: { 23 | static: { 24 | directory: path.join(__dirname, "dist"), 25 | }, 26 | compress: false, 27 | port: 9000, 28 | }, 29 | resolve: { 30 | fallback: { 31 | buffer: require.resolve("buffer/"), 32 | }, 33 | }, 34 | plugins: [ 35 | new HtmlWebpackPlugin({ 36 | template: "./src/index.html", 37 | inject: "footer", 38 | scriptLoading: "blocking", 39 | }), 40 | new webpack.ProvidePlugin({ 41 | Buffer: ["buffer", "Buffer"], 42 | }), 43 | ], 44 | }; 45 | -------------------------------------------------------------------------------- /packages/rsocket-adapter-rxjs/src/Utils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | "use strict"; 17 | // This can live anywhere in your codebase: 18 | export function applyMixins(derivedCtor: any, constructors: any[]) { 19 | constructors.forEach((baseCtor) => { 20 | Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => { 21 | Object.defineProperty( 22 | derivedCtor.prototype, 23 | name, 24 | Object.getOwnPropertyDescriptor(baseCtor.prototype, name) || 25 | Object.create(null) 26 | ); 27 | }); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release 2 | 3 | How to publish new releases for this project. 4 | 5 | ## Versioning 6 | 7 | [semver](https://semver.org/) should be followed when deciding new release versions. 8 | 9 | You can either set versions in the `package.json` files manually, or use the `lerna version` command to set them via the Lerna CLI. When setting versions manually, you will also need to set the git tags for each package and version. For this reason, it is recommended you use the `lerna version` command, which will create these tags automatically. 10 | 11 | ex: `rsocket-adapter-rxjs@1.0.0-alpha.1` 12 | 13 | Lerna will not push the git tags after creation. You should push the git tags once you are confident in your changes. 14 | 15 | ### Example 16 | 17 | ``` 18 | lerna version prerelease --sign-git-commit 19 | ``` 20 | 21 | ## Publishing 22 | 23 | The `Test, Build, Release` Workflow on GitHub can be run to [manually trigger](https://docs.github.com/en/actions/managing-workflow-runs/manually-running-a-workflow) publishing of packages to NPM. This workflow will only publish versions which do not already exist on NPM. 24 | 25 | The `Test, Build, Release` Workflow will: 26 | 27 | - Run automated linting & tests 28 | - Compile/build various packages 29 | - Publish built packages to NPM 30 | -------------------------------------------------------------------------------- /packages/rsocket-composite-metadata/__tests__/encodeCustomMetadataHeader.ts: -------------------------------------------------------------------------------- 1 | import { encodeCustomMetadataHeader } from "rsocket-composite-metadata"; 2 | import { hex } from "./test-utils/hex"; 3 | 4 | describe("encodeCustomMetadataHeader", () => { 5 | it("throws if length is less than 1", () => { 6 | expect(() => encodeCustomMetadataHeader("", 0)).toThrow( 7 | "Custom mime type must have a strictly positive length that fits on 7 unsigned bits, ie 1-128" 8 | ); 9 | }); 10 | 11 | it("throws if length is greater than 127", () => { 12 | let mime = ""; 13 | while (mime.length < 130) { 14 | mime += "a"; 15 | } 16 | expect(() => encodeCustomMetadataHeader(mime, mime.length)).toThrow( 17 | "Custom mime type must have a strictly positive length that fits on 7 unsigned bits, ie 1-128" 18 | ); 19 | }); 20 | 21 | it("encodes the header as per spec", () => { 22 | const { t, e, s } = hex; 23 | const mime = "test"; 24 | // length minus 1 (uint8) 25 | const expectedLength8 = "03"; 26 | // full length (uint24) 27 | const expectedLength24 = "000004"; 28 | const header = encodeCustomMetadataHeader(mime, mime.length); 29 | expect(header.toString("hex")).toBe( 30 | `${expectedLength8}${t}${e}${s}${t}${expectedLength24}` 31 | ); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /packages/rsocket-core/src/Errors.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export class RSocketError extends Error { 18 | constructor(readonly code: number | ErrorCodes, message?: string) { 19 | super(message); 20 | } 21 | } 22 | 23 | export enum ErrorCodes { 24 | RESERVED = 0x00000000, 25 | INVALID_SETUP = 0x00000001, 26 | UNSUPPORTED_SETUP = 0x00000002, 27 | REJECTED_SETUP = 0x00000003, 28 | REJECTED_RESUME = 0x00000004, 29 | CONNECTION_CLOSE = 0x00000102, 30 | CONNECTION_ERROR = 0x00000101, 31 | APPLICATION_ERROR = 0x00000201, 32 | REJECTED = 0x00000202, 33 | CANCELED = 0x00000203, 34 | INVALID = 0x00000204, 35 | RESERVED_EXTENSION = 0xffffffff, 36 | } 37 | -------------------------------------------------------------------------------- /packages/rsocket-examples/src/webpack/simple/server/server.js: -------------------------------------------------------------------------------- 1 | const { RSocketServer } = require("rsocket-core"); 2 | const { WebsocketServerTransport } = require("rsocket-websocket-server"); 3 | const WebSocket = require("ws"); 4 | 5 | const port = 9090; 6 | 7 | const server = new RSocketServer({ 8 | transport: new WebsocketServerTransport({ 9 | wsCreator: (options) => { 10 | return new WebSocket.Server({ 11 | port: port, 12 | }); 13 | }, 14 | }), 15 | acceptor: { 16 | accept: async () => ({ 17 | requestResponse: (payload, responderStream) => { 18 | const timeout = setTimeout( 19 | () => 20 | responderStream.onNext( 21 | { 22 | data: Buffer.concat([Buffer.from("ECHO: "), payload.data]), 23 | }, 24 | true 25 | ), 26 | 100 27 | ); 28 | return { 29 | cancel: () => { 30 | clearTimeout(timeout); 31 | console.log("cancelled"); 32 | }, 33 | onExtension: () => { 34 | console.log("Received Extension request"); 35 | }, 36 | }; 37 | }, 38 | }), 39 | }, 40 | }); 41 | 42 | (async () => { 43 | await server.bind(); 44 | console.log(`Server listening on port ${port}`); 45 | })(); 46 | -------------------------------------------------------------------------------- /packages/rsocket-core/src/Common.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export interface Closeable { 18 | /** 19 | * Close the underlying connection, emitting `onComplete` on the receive() 20 | * Publisher. 21 | */ 22 | close(error?: Error): void; 23 | 24 | /** 25 | * Registers a callback to be called when the Closeable is closed. optionally with an Error. 26 | */ 27 | onClose(callback: (error?: Error) => void): void; 28 | } 29 | 30 | export interface Availability { 31 | /** 32 | * Returns positive number representing the availability of RSocket requester. Higher is better, 0.0 33 | * means not available. 34 | */ 35 | readonly availability: number; 36 | } 37 | -------------------------------------------------------------------------------- /packages/rsocket-examples/src/shared/logger.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export default class Logger { 18 | static info(message, ...rest) { 19 | const date = new Date() 20 | .toISOString() 21 | .replace(/T/, " ") // replace T with a space 22 | .replace(/\..+/, ""); // delete the dot and everything after; 23 | return console.log(`[${date}] ${message}`, ...rest); 24 | } 25 | 26 | static error(message, ...rest) { 27 | const date = new Date() 28 | .toISOString() 29 | .replace(/T/, " ") // replace T with a space 30 | .replace(/\..+/, ""); // delete the dot and everything after; 31 | return console.error(`[${date}] ${message}`, ...rest); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/rsocket-core/__tests__/test-utils/MockStream.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CancelFrame, 3 | ErrorFrame, 4 | ExtFrame, 5 | Frame, 6 | PayloadFrame, 7 | RequestChannelFrame, 8 | RequestFnfFrame, 9 | RequestNFrame, 10 | RequestResponseFrame, 11 | RequestStreamFrame, 12 | } from "../../src/Frames"; 13 | import { Stream, StreamFrameHandler } from "../../src/Transport"; 14 | 15 | export class MockStream implements Stream { 16 | handler: StreamFrameHandler; 17 | frames: Frame[] = []; 18 | wasConnected: boolean = false; 19 | 20 | connect(handler: StreamFrameHandler): void { 21 | if (!this.handler) { 22 | this.wasConnected = true; 23 | this.handler = handler; 24 | } 25 | } 26 | 27 | disconnect(handler: StreamFrameHandler): void { 28 | if (this.handler == handler) { 29 | this.handler = undefined; 30 | } 31 | } 32 | 33 | send( 34 | frame: 35 | | CancelFrame 36 | | ErrorFrame 37 | | PayloadFrame 38 | | RequestChannelFrame 39 | | RequestFnfFrame 40 | | RequestNFrame 41 | | RequestResponseFrame 42 | | RequestStreamFrame 43 | | ExtFrame 44 | ): void { 45 | // to emulate that frames can not be sent if Stream was not connected or was disconnected 46 | if (this.wasConnected && !this.handler) { 47 | return; 48 | } 49 | this.frames.push(frame); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/rsocket-examples/src/webpack/simple/client/index.js: -------------------------------------------------------------------------------- 1 | import { RSocketConnector } from "rsocket-core"; 2 | import { WebsocketClientTransport } from "rsocket-websocket-client"; 3 | 4 | (async () => { 5 | const outputDiv = document.querySelector("#output"); 6 | 7 | const connector = new RSocketConnector({ 8 | transport: new WebsocketClientTransport({ 9 | url: "ws://localhost:9090", 10 | wsCreator: (url) => new WebSocket(url), 11 | }), 12 | }); 13 | 14 | let rsocket = null; 15 | try { 16 | rsocket = await connector.connect(); 17 | } catch (e) { 18 | const div = document.createElement("div"); 19 | div.textContent = "Failed to connect to server! Is it running?"; 20 | outputDiv.appendChild(div); 21 | return; 22 | } 23 | 24 | rsocket.requestResponse( 25 | { 26 | data: Buffer.from("Hello World"), 27 | }, 28 | { 29 | onError: (e) => reject(e), 30 | onNext: (payload, isComplete) => { 31 | const div = document.createElement("div"); 32 | div.textContent = `[${new Date().toISOString()}] payload[data: ${ 33 | payload.data 34 | }; metadata: ${payload.metadata}]|${isComplete}`; 35 | outputDiv.appendChild(div); 36 | }, 37 | onComplete: () => { 38 | const div = document.createElement("div"); 39 | div.textContent = `Stream completed...`; 40 | outputDiv.appendChild(div); 41 | }, 42 | onExtension: () => {}, 43 | } 44 | ); 45 | })(); 46 | -------------------------------------------------------------------------------- /packages/rsocket-graphql-apollo-server/src/RSocketApolloGraphlQLPlugin.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ApolloServerPlugin, 3 | BaseContext, 4 | GraphQLServerListener, 5 | GraphQLServiceContext, 6 | } from "apollo-server-plugin-base"; 7 | import { RSocket } from "rsocket-core"; 8 | import { RSocketApolloServer } from "./RSocketApolloServer"; 9 | 10 | type RSocketApolloGraphlQLPluginOptions = { 11 | apolloServer?: RSocketApolloServer; 12 | makeRSocketServer: ({ handler }: { handler: Partial }) => any; 13 | }; 14 | 15 | export class RSocketApolloGraphlQLPlugin 16 | implements ApolloServerPlugin 17 | { 18 | private apolloServer: RSocketApolloServer; 19 | constructor(private options: RSocketApolloGraphlQLPluginOptions) {} 20 | 21 | async serverWillStart( 22 | service: GraphQLServiceContext 23 | ): Promise { 24 | if (!this.apolloServer) { 25 | throw new Error( 26 | "serverWillStart called without valid apolloServer reference. Did you forget to call setApolloServer?" 27 | ); 28 | } 29 | const handler = this.apolloServer.getHandler(); 30 | let rSocketServer = this.options.makeRSocketServer({ handler }); 31 | let closeable = await rSocketServer.bind(); 32 | return { 33 | async drainServer() { 34 | closeable.close(); 35 | }, 36 | }; 37 | } 38 | 39 | setApolloServer(apolloServer: RSocketApolloServer) { 40 | this.apolloServer = apolloServer; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/rsocket-composite-metadata/__tests__/encodeAndAddWellKnownMetadata.ts: -------------------------------------------------------------------------------- 1 | import { 2 | encodeAndAddWellKnownMetadata, 3 | WellKnownMimeType, 4 | } from "rsocket-composite-metadata"; 5 | import { readUInt24BE } from "rsocket-core"; 6 | 7 | describe("encodeWellKnownMetadataHeader", () => { 8 | it("encodes the header as per spec when WellKnownMimeType given", () => { 9 | const metadata = encodeAndAddWellKnownMetadata( 10 | Buffer.from([]), 11 | WellKnownMimeType.MESSAGE_RSOCKET_MIMETYPE, 12 | Buffer.from("test") 13 | ); 14 | 15 | // 122 | 128 16 | const maskedId = metadata.readUInt8(0); 17 | const length = readUInt24BE(metadata, 1); 18 | const value = metadata.slice(4, metadata.length); 19 | 20 | expect(maskedId).toBe(250); 21 | expect(length).toBe(4); 22 | expect(value.length).toBe(4); 23 | expect(value.toString("utf-8")).toBe("test"); 24 | }); 25 | 26 | it("encodes the header as per spec when identifier given", () => { 27 | const metadata = encodeAndAddWellKnownMetadata( 28 | Buffer.from([]), 29 | // MESSAGE_RSOCKET_MIMETYPE 30 | 122, 31 | Buffer.from("test") 32 | ); 33 | 34 | // 122 | 128 35 | const maskedId = metadata.readUInt8(0); 36 | const length = readUInt24BE(metadata, 1); 37 | const value = metadata.slice(4, metadata.length); 38 | 39 | expect(maskedId).toBe(250); 40 | expect(length).toBe(4); 41 | expect(value.length).toBe(4); 42 | expect(value.toString("utf-8")).toBe("test"); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /packages/rsocket-examples/src/graphql/apollo/client-server/resolvers.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import { PubSub } from "graphql-subscriptions"; 17 | 18 | const pubsub = new PubSub(); 19 | 20 | export const resolvers = { 21 | Query: { 22 | echo: (parent, args, context, info) => { 23 | const { message } = args; 24 | return { 25 | message, 26 | }; 27 | }, 28 | }, 29 | Mutation: { 30 | createMessage: async (_, { message }, context, info) => { 31 | await pubsub.publish("POST_CREATED", { 32 | messageCreated: { 33 | message, 34 | }, 35 | }); 36 | }, 37 | }, 38 | Subscription: { 39 | messageCreated: { 40 | // subscribe must return an AsyncIterator 41 | // https://www.apollographql.com/docs/apollo-server/data/subscriptions/#resolving-a-subscription 42 | subscribe: () => pubsub.asyncIterator(["POST_CREATED"]), 43 | }, 44 | }, 45 | }; 46 | -------------------------------------------------------------------------------- /packages/rsocket-graphql-apollo-link/src/RSocketLink.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { split } from "@apollo/client/core"; 18 | import { getMainDefinition } from "@apollo/client/utilities"; 19 | import { RSocketQueryLink } from "./RSocketQueryLink"; 20 | import { RSocketSubscriptionLink } from "./RSocketSubscriptionLink"; 21 | import { RSocket } from "rsocket-core"; 22 | 23 | export type makeRSocketLinkConfig = { 24 | rsocket: RSocket; 25 | route?: string; 26 | }; 27 | 28 | export const makeRSocketLink = ({ rsocket, route }: makeRSocketLinkConfig) => { 29 | return split( 30 | ({ query }) => { 31 | const definition = getMainDefinition(query); 32 | return ( 33 | definition.kind === "OperationDefinition" && 34 | definition.operation === "subscription" 35 | ); 36 | }, 37 | new RSocketSubscriptionLink(rsocket, { route }), 38 | new RSocketQueryLink(rsocket, { route }) 39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /packages/rsocket-composite-metadata/__tests__/encodeAndAddCustomMetadata.spec.ts: -------------------------------------------------------------------------------- 1 | import { encodeAndAddCustomMetadata } from "rsocket-composite-metadata"; 2 | import { hex } from "./test-utils/hex"; 3 | 4 | describe("encodeAndAddCustomMetadata", () => { 5 | it("throws if custom mimtype length is less than 1", () => { 6 | expect(() => 7 | encodeAndAddCustomMetadata(Buffer.from([]), "", Buffer.from("1234")) 8 | ).toThrow( 9 | "Custom mime type must have a strictly positive length that fits on 7 unsigned bits, ie 1-128" 10 | ); 11 | }); 12 | 13 | it("throws if custom mimtype length is greater than 127", () => { 14 | let mime = ""; 15 | while (mime.length < 130) { 16 | mime += "a"; 17 | } 18 | expect(() => 19 | encodeAndAddCustomMetadata(Buffer.from([]), mime, Buffer.from("1234")) 20 | ).toThrow( 21 | "Custom mime type must have a strictly positive length that fits on 7 unsigned bits, ie 1-128" 22 | ); 23 | }); 24 | 25 | it("encodes the header and payload as per spec", () => { 26 | const { c, u, s, t, o, m } = hex; 27 | const metadata = encodeAndAddCustomMetadata( 28 | Buffer.from([]), 29 | "custom", 30 | Buffer.from("1234") 31 | ); 32 | const expectedHeaderLength8 = "05"; 33 | const expectedPayloadLength24 = "000004"; 34 | const expectedHeader = `${expectedHeaderLength8}${c}${u}${s}${t}${o}${m}${expectedPayloadLength24}`; 35 | const expectedPayload = `${hex["1"]}${hex["2"]}${hex["3"]}${hex["4"]}`; 36 | expect(metadata.toString("hex")).toBe( 37 | `${expectedHeader}${expectedPayload}` 38 | ); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /packages/rsocket-core/__tests__/Codec.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | readUInt24BE, 3 | readUInt64BE, 4 | writeUInt24BE, 5 | writeUInt64BE, 6 | } from "../src/Codecs"; 7 | 8 | describe("Codecs", () => { 9 | describe("{read,write}UInt24BE", () => { 10 | [ 11 | 0, 12 | 1, 13 | Math.pow(2, 24) - 5, 14 | Math.pow(2, 24) - 3, 15 | Math.pow(2, 24) - 2, 16 | Math.pow(2, 24) - 1, 17 | ].forEach((val) => { 18 | it("reads/writes 0x" + val.toString(16), () => { 19 | const buffer = new Buffer(3); 20 | const offset = writeUInt24BE(buffer, val, 0); 21 | expect(offset).toBe(3); 22 | expect(readUInt24BE(buffer, 0)).toBe(val); 23 | }); 24 | }); 25 | }); 26 | 27 | describe("{read,write}UInt64BE", () => { 28 | [ 29 | 0, 30 | 1, 31 | Math.pow(2, 31) - 1, 32 | Math.pow(2, 31), 33 | Math.pow(2, 32) - 1, 34 | Math.pow(2, 32), 35 | Math.pow(2, 33) - 1, 36 | Math.pow(2, 33), 37 | Math.pow(2, 34) - 1, 38 | Math.pow(2, 34), 39 | Number.MAX_SAFE_INTEGER - 4, 40 | Number.MAX_SAFE_INTEGER - 3, 41 | Number.MAX_SAFE_INTEGER - 2, 42 | Number.MAX_SAFE_INTEGER - 1, 43 | Number.MAX_SAFE_INTEGER, 44 | ].forEach((val) => { 45 | it("writes and reads back 0x" + val.toString(16), () => { 46 | const buffer = new Buffer(8); 47 | const offset = writeUInt64BE(buffer, val, 0); 48 | expect(offset).toBe(8); 49 | expect(readUInt64BE(buffer, 0)).toBe(val); 50 | }); 51 | }); 52 | 53 | // Ensure that the binary representation is correct 54 | it("writes values in canonical form", () => { 55 | const buffer = new Buffer(8); 56 | buffer.fill(0); 57 | writeUInt64BE(buffer, Number.MAX_SAFE_INTEGER, 0); 58 | expect(buffer.toString("hex")).toBe("001fffffffffffff"); 59 | }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /packages/rsocket-core/__tests__/test-utils/toMatchYields.ts: -------------------------------------------------------------------------------- 1 | // Modified from https://github.com/doniyor2109/jest-generator 2 | 3 | const toOrdinalSuffix = (num) => { 4 | const int = parseInt(num), 5 | digits = [int % 10, int % 100], 6 | ordinals = ["st", "nd", "rd", "th"], 7 | oPattern = [1, 2, 3, 4], 8 | tPattern = [11, 12, 13, 14, 15, 16, 17, 18, 19]; 9 | return oPattern.includes(digits[0]) && !tPattern.includes(digits[1]) 10 | ? int + ordinals[digits[0] - 1] 11 | : int + ordinals[3]; 12 | }; 13 | 14 | export default function toMatchYields(iterator, yieldValues) { 15 | let yieldIndex = 0; 16 | let pass = true; 17 | let received; 18 | let expected; 19 | let iteratorValue; 20 | 21 | do { 22 | const [expectedYieldValue] = yieldValues[yieldIndex] || []; 23 | const [, argumentForYield] = yieldValues[yieldIndex - 1] || []; 24 | 25 | if (argumentForYield instanceof Error) { 26 | iteratorValue = iterator.throw(argumentForYield); 27 | } else { 28 | iteratorValue = iterator.next(argumentForYield); 29 | } 30 | 31 | const yieldedValue = iteratorValue.value; 32 | const isYieldValueSameAsExpected = this.equals( 33 | yieldedValue, 34 | expectedYieldValue 35 | ); 36 | 37 | if (!isYieldValueSameAsExpected) { 38 | expected = expectedYieldValue; 39 | received = yieldedValue; 40 | pass = false; 41 | break; 42 | } 43 | 44 | yieldIndex++; 45 | } while (iteratorValue.done === false); 46 | 47 | const expectedMessage = this.utils.printExpected(expected); 48 | const receivedMessage = this.utils.printReceived(received); 49 | 50 | return { 51 | pass, 52 | actual: received, 53 | message: () => `${toOrdinalSuffix( 54 | yieldIndex + 1 55 | )} generator value produced did not match with expected.\n 56 | Produced: \n 57 | ${receivedMessage}\n 58 | Expected:\n 59 | ${expectedMessage} 60 | `, 61 | }; 62 | } 63 | -------------------------------------------------------------------------------- /packages/rsocket-examples/src/tcp/ClienRequestFnfnWithLeaseExampleTcp.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { RSocket, RSocketConnector } from "rsocket-core"; 18 | import { TcpClientTransport } from "rsocket-tcp-client"; 19 | import { exit } from "process"; 20 | 21 | let serverCloseable; 22 | 23 | function makeConnector() { 24 | return new RSocketConnector({ 25 | lease: {}, 26 | transport: new TcpClientTransport({ 27 | connectionOptions: { 28 | host: "127.0.0.1", 29 | port: 9090, 30 | }, 31 | }), 32 | }); 33 | } 34 | 35 | async function fnf(rsocket: RSocket) { 36 | return new Promise((resolve, reject) => { 37 | return rsocket.fireAndForget( 38 | { 39 | data: Buffer.from("Hello World"), 40 | }, 41 | { 42 | onError: (e) => { 43 | reject(e); 44 | }, 45 | onComplete: () => { 46 | resolve(null); 47 | }, 48 | } 49 | ); 50 | }); 51 | } 52 | 53 | async function main() { 54 | const connector = makeConnector(); 55 | 56 | const rsocket = await connector.connect(); 57 | 58 | await fnf(rsocket); 59 | } 60 | 61 | main() 62 | .then(() => exit()) 63 | .catch((error: Error) => { 64 | console.error(error); 65 | exit(1); 66 | }) 67 | .finally(() => { 68 | serverCloseable.close(); 69 | }); 70 | -------------------------------------------------------------------------------- /packages/rsocket-core/__tests__/KeepAliveHandler.spec.ts: -------------------------------------------------------------------------------- 1 | import { KeepAliveHandler } from "../src/RSocketSupport"; 2 | import { mock } from "jest-mock-extended"; 3 | import { 4 | Demultiplexer, 5 | DuplexConnection, 6 | Flags, 7 | FrameHandler, 8 | FrameTypes, 9 | Multiplexer, 10 | } from "../src"; 11 | import { Closeable, Outbound } from "../src"; 12 | 13 | jest.useFakeTimers(); 14 | 15 | describe("KeepAliveHandler", () => { 16 | it("Closes the connection with an error if no KeepAlive frames received after timeout", () => { 17 | const keepAliveTimeoutDuration = 10000; 18 | const mockConnection = mock(); 19 | const handler = new KeepAliveHandler( 20 | mockConnection, 21 | keepAliveTimeoutDuration 22 | ); 23 | 24 | handler.start(); 25 | 26 | jest.advanceTimersByTime(keepAliveTimeoutDuration + 1000); 27 | 28 | expect(mockConnection.close).toBeCalledTimes(1); 29 | }); 30 | 31 | it("Handling KeepAlive frame extends timeout duration", () => { 32 | const keepAliveTimeoutDuration = 10000; 33 | const mockOutbound = mock(); 34 | const mockMultiplexerDemultiplexer = mock< 35 | Multiplexer & Demultiplexer & FrameHandler & Closeable 36 | >({ 37 | connectionOutbound: mockOutbound, 38 | }); 39 | const mockConnection = mock({ 40 | multiplexerDemultiplexer: mockMultiplexerDemultiplexer, 41 | }); 42 | const handler = new KeepAliveHandler( 43 | mockConnection, 44 | keepAliveTimeoutDuration 45 | ); 46 | 47 | handler.start(); 48 | 49 | jest.advanceTimersByTime(9000); 50 | 51 | // handling a keep alive frame resets "last received" to "now" 52 | handler.handle({ 53 | type: FrameTypes.KEEPALIVE, 54 | streamId: 0, 55 | data: undefined, 56 | flags: Flags.RESPOND, 57 | lastReceivedPosition: 0, 58 | }); 59 | 60 | jest.advanceTimersByTime(9000); 61 | 62 | expect(mockConnection.close).toBeCalledTimes(0); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /packages/rsocket-core/src/Deferred.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Closeable } from "./Common"; 18 | 19 | export class Deferred implements Closeable { 20 | private _done: boolean = false; 21 | private _error: Error | undefined; 22 | private onCloseCallbacks: Array<(reason?: Error) => void> = []; 23 | 24 | get done(): boolean { 25 | return this._done; 26 | } 27 | 28 | /** 29 | * Signals to an observer that the Deferred operation has been closed, which invokes 30 | * the provided `onClose` callback. 31 | */ 32 | close(error?: Error): void { 33 | if (this.done) { 34 | console.warn( 35 | `Trying to close for the second time. ${ 36 | error ? `Dropping error [${error}].` : "" 37 | }` 38 | ); 39 | return; 40 | } 41 | 42 | this._done = true; 43 | this._error = error; 44 | 45 | if (error) { 46 | for (const callback of this.onCloseCallbacks) { 47 | callback(error); 48 | } 49 | return; 50 | } 51 | 52 | for (const callback of this.onCloseCallbacks) { 53 | callback(); 54 | } 55 | } 56 | 57 | /** 58 | * Registers a callback to be called when the Closeable is closed. optionally with an Error. 59 | */ 60 | onClose(callback: (reason?: Error) => void): void { 61 | if (this._done) { 62 | callback(this._error); 63 | return; 64 | } 65 | this.onCloseCallbacks.push(callback); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /packages/rsocket-graphql-apollo-server/src/utilities.ts: -------------------------------------------------------------------------------- 1 | import { Payload } from "rsocket-core"; 2 | import { 3 | decodeCompositeMetadata, 4 | decodeRoutes, 5 | WellKnownMimeType, 6 | } from "rsocket-composite-metadata"; 7 | import MESSAGE_RSOCKET_ROUTING = WellKnownMimeType.MESSAGE_RSOCKET_ROUTING; 8 | 9 | const APPLICATION_GRAPHQL_JSON = "application/graphql+json"; 10 | 11 | function hasGraphQLJsonMimeType(metadata: Map) { 12 | return ( 13 | metadata.get(WellKnownMimeType.MESSAGE_RSOCKET_MIMETYPE.toString()) === 14 | APPLICATION_GRAPHQL_JSON 15 | ); 16 | } 17 | 18 | export function parsePayloadForQuery(payload: Payload) { 19 | const { data } = payload; 20 | const decoded = data.toString(); 21 | return JSON.parse(decoded); 22 | } 23 | 24 | export function mapMetadata(payload: Payload) { 25 | const mappedMetaData = new Map(); 26 | if (payload.metadata) { 27 | const decodedCompositeMetaData = decodeCompositeMetadata(payload.metadata); 28 | 29 | for (let metaData of decodedCompositeMetaData) { 30 | switch (metaData.mimeType) { 31 | case MESSAGE_RSOCKET_ROUTING.toString(): { 32 | const tags = []; 33 | for (let decodedRoute of decodeRoutes(metaData.content)) { 34 | tags.push(decodedRoute); 35 | } 36 | const joinedRoute = tags.join("."); 37 | mappedMetaData.set(MESSAGE_RSOCKET_ROUTING.toString(), joinedRoute); 38 | break; 39 | } 40 | default: { 41 | mappedMetaData.set(metaData.mimeType, metaData.content.toString()); 42 | break; 43 | } 44 | } 45 | } 46 | } 47 | return mappedMetaData; 48 | } 49 | 50 | export function isObject(val: unknown): val is Record { 51 | return typeof val === "object" && val !== null; 52 | } 53 | 54 | export function isAsyncGenerator( 55 | val: unknown 56 | ): val is AsyncGenerator { 57 | return ( 58 | isObject(val) && 59 | typeof Object(val)[Symbol.asyncIterator] === "function" && 60 | typeof val.return === "function" 61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /packages/rsocket-adapter-rxjs/src/ObserverToRSocketSubscriber.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | "use strict"; 17 | import { 18 | Cancellable, 19 | OnExtensionSubscriber, 20 | OnNextSubscriber, 21 | OnTerminalSubscriber, 22 | } from "rsocket-core"; 23 | import { Codec } from "rsocket-messaging"; 24 | import { Observer, Subscription } from "rxjs"; 25 | 26 | export default class ObserverToRSocketSubscriber 27 | extends Subscription 28 | implements Observer, Cancellable, OnExtensionSubscriber 29 | { 30 | constructor( 31 | private readonly subscriber: T extends void | null | undefined 32 | ? OnTerminalSubscriber 33 | : OnTerminalSubscriber & OnNextSubscriber & OnExtensionSubscriber, 34 | private readonly codec: T extends void | null | undefined 35 | ? undefined 36 | : Codec 37 | ) { 38 | super(); 39 | } 40 | onExtension( 41 | extendedType: number, 42 | content: Buffer, 43 | canBeIgnored: boolean 44 | ): void {} 45 | 46 | cancel(): void { 47 | this.unsubscribe(); 48 | } 49 | 50 | next(value?: T): void { 51 | if (isVoid(value)) { 52 | return; 53 | } 54 | 55 | this.unsubscribe(); 56 | 57 | (this.subscriber as OnNextSubscriber).onNext( 58 | { 59 | data: this.codec?.encode(value), 60 | }, 61 | true 62 | ); 63 | } 64 | 65 | error(err?: any): void { 66 | this.subscriber.onError(err); 67 | } 68 | 69 | complete(): void { 70 | this.subscriber.onComplete(); 71 | } 72 | } 73 | 74 | function isVoid(value: any): value is void { 75 | return value == undefined; 76 | } 77 | -------------------------------------------------------------------------------- /packages/rsocket-examples/src/webpack/browser-bundle/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Webpack Browser Bundle Example 3 | 4 | This folder provides and example of using Webpack to create a "library" which can be loaded in an HTML file or used in a 5 | browser context without NPM or other bundling tools. 6 | 7 | ## Files 8 | 9 | __rsocket.js__ 10 | 11 | [src/rsocket.js](src/rsocket.js) demonstrates how to write a "library" that exposes functionality for creating an RSocket 12 | connection using the WebSocket transport. Additionally this "library" exposes a function for creating a buffer from a 13 | given value. 14 | 15 | For your own use cases you will likely need to alter the implementation to expose the functionality you need. 16 | 17 | __webpack.config.js__ 18 | 19 | [webpack.config.js](./webpack.config.js) demonstrates how to configure webpack to create a library file which exposes the exports 20 | from the [src/rsocket.js](src/rsocket.js) in the global scope of any HTML file which loads the built library file. 21 | 22 | __index.html__ 23 | 24 | [src/app.js](src/app.js) demonstrates how to use the global `rsocket` variable which is exposed by the `rsocket.js` library built by Webpack. 25 | 26 | Note: `src/index.html` does not show how to load the built `rsocket.js` file as that will be up to you/your implementation to decide. 27 | 28 | Note: For this example, when running the `serve` npm script webpack will automatically host the `index.html` file and inject the `rsocket.js` and `app.js` scripts into the footer of the page. 29 | 30 | ## Run the server 31 | 32 | **Open a terminal:** 33 | 34 | Open a terminal in the `simple/server` directory one level up from this README. 35 | 36 | **Install dependencies:** 37 | 38 | ```bash 39 | npm install 40 | ``` 41 | 42 | **Run the server:** 43 | 44 | ```bash 45 | npm run start 46 | ``` 47 | 48 | ## Run the client 49 | 50 | **Open a terminal in this folder and install dependencies:** 51 | 52 | ```bash 53 | npm install 54 | ``` 55 | 56 | **Run the NPM server script:** 57 | 58 | ``` 59 | npm run serve 60 | ``` 61 | 62 | The above script will run the webpack dev server, which will first compile the "app" and then host the index.html. 63 | 64 | **Open in browser:** 65 | 66 | Visit [localhost:9000](http://localhost:9000). 67 | -------------------------------------------------------------------------------- /packages/rsocket-examples/src/ClientRequestChannelExample.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { RSocketConnector } from "rsocket-core"; 18 | import { WebsocketClientTransport } from "rsocket-websocket-client"; 19 | import { exit } from "process"; 20 | import WebSocket from "ws"; 21 | 22 | async function main() { 23 | const connector = new RSocketConnector({ 24 | setup: { 25 | keepAlive: 100, 26 | lifetime: 10000, 27 | }, 28 | transport: new WebsocketClientTransport({ 29 | url: "ws://localhost:8080", 30 | wsCreator: (url) => new WebSocket(url) as any, 31 | }), 32 | }); 33 | 34 | const rsocket = await connector.connect(); 35 | 36 | await new Promise((resolve, reject) => { 37 | const requester = rsocket.requestChannel( 38 | { 39 | data: Buffer.from("Hello World"), 40 | }, 41 | 1, 42 | false, 43 | { 44 | onError: (e) => reject(e), 45 | onNext: (payload, isComplete) => { 46 | console.log( 47 | `payload[data: ${payload.data}; metadata: ${payload.metadata}]|${isComplete}` 48 | ); 49 | 50 | requester.request(1); 51 | 52 | if (isComplete) { 53 | resolve(payload); 54 | } 55 | }, 56 | onComplete: () => { 57 | resolve(null); 58 | }, 59 | onExtension: () => {}, 60 | request: (n) => { 61 | console.log(`request(${n})`); 62 | requester.onNext( 63 | { 64 | data: Buffer.from("Message"), 65 | }, 66 | true 67 | ); 68 | }, 69 | cancel: () => {}, 70 | } 71 | ); 72 | }); 73 | } 74 | 75 | main().then(() => exit()); 76 | -------------------------------------------------------------------------------- /packages/rsocket-tcp-client/src/__tests__/__snapshots__/TcpDuplexConnection.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`TcpDuplexConnection when receiving data when buffer contains a single frame deserializes received frames and calls the configured handler 1`] = ` 4 | { 5 | "data": { 6 | "data": [ 7 | 104, 8 | 101, 9 | 108, 10 | 108, 11 | 111, 12 | 32, 13 | 119, 14 | 111, 15 | 114, 16 | 108, 17 | 100, 18 | ], 19 | "type": "Buffer", 20 | }, 21 | "dataMimeType": "application/octet-stream", 22 | "flags": 256, 23 | "keepAlive": 60000, 24 | "lifetime": 300000, 25 | "majorVersion": 1, 26 | "metadata": { 27 | "data": [ 28 | 104, 29 | 101, 30 | 108, 31 | 108, 32 | 111, 33 | 32, 34 | 119, 35 | 111, 36 | 114, 37 | 108, 38 | 100, 39 | ], 40 | "type": "Buffer", 41 | }, 42 | "metadataMimeType": "application/octet-stream", 43 | "minorVersion": 0, 44 | "resumeToken": null, 45 | "streamId": 0, 46 | "type": 1, 47 | } 48 | `; 49 | 50 | exports[`TcpDuplexConnection when receiving data when buffer contains multiple frames deserializes received frames and calls the configured handler for each frame 1`] = ` 51 | [ 52 | { 53 | "data": { 54 | "data": [ 55 | 104, 56 | 101, 57 | 108, 58 | 108, 59 | 111, 60 | 32, 61 | 119, 62 | 111, 63 | 114, 64 | 108, 65 | 100, 66 | ], 67 | "type": "Buffer", 68 | }, 69 | "flags": 32, 70 | "metadata": null, 71 | "streamId": 1, 72 | "type": 10, 73 | }, 74 | ] 75 | `; 76 | 77 | exports[`TcpDuplexConnection when receiving data when buffer contains multiple frames deserializes received frames and calls the configured handler for each frame 2`] = ` 78 | [ 79 | { 80 | "data": { 81 | "data": [ 82 | 104, 83 | 101, 84 | 108, 85 | 108, 86 | 111, 87 | 32, 88 | 119, 89 | 111, 90 | 114, 91 | 108, 92 | 100, 93 | 32, 94 | 50, 95 | ], 96 | "type": "Buffer", 97 | }, 98 | "flags": 32, 99 | "metadata": null, 100 | "streamId": 1, 101 | "type": 10, 102 | }, 103 | ] 104 | `; 105 | -------------------------------------------------------------------------------- /packages/rsocket-adapter-rxjs/src/RSocketPublisherToObservable.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | "use strict"; 17 | import { 18 | Cancellable, 19 | OnExtensionSubscriber, 20 | OnNextSubscriber, 21 | OnTerminalSubscriber, 22 | Payload, 23 | } from "rsocket-core"; 24 | import { Codec } from "rsocket-messaging"; 25 | import { Observable, Subscriber, TeardownLogic, Unsubscribable } from "rxjs"; 26 | 27 | export default class RSocketPublisherToObservable 28 | extends Observable 29 | implements 30 | OnTerminalSubscriber, 31 | OnNextSubscriber, 32 | OnExtensionSubscriber, 33 | Unsubscribable 34 | { 35 | private observer: Subscriber; 36 | private cancellable: Cancellable; 37 | 38 | constructor( 39 | private readonly exchangeFunction: ( 40 | subscriber: OnNextSubscriber & 41 | OnTerminalSubscriber & 42 | OnExtensionSubscriber 43 | ) => Cancellable, 44 | private readonly responseCodec?: Codec 45 | ) { 46 | super(); 47 | } 48 | 49 | onNext(payload: Payload, _isComplete: boolean): void { 50 | this.observer.next(this.responseCodec.decode(payload.data)); 51 | this.observer.complete(); 52 | } 53 | 54 | onError(error: Error): void { 55 | this.observer.error(error); 56 | } 57 | 58 | onComplete(): void { 59 | this.observer.complete(); 60 | } 61 | 62 | onExtension( 63 | extendedType: number, 64 | content: Buffer, 65 | canBeIgnored: boolean 66 | ): void {} 67 | 68 | unsubscribe(): void { 69 | this.cancellable.cancel(); 70 | } 71 | 72 | protected _subscribe(subscriber: Subscriber): TeardownLogic { 73 | if (this.observer) { 74 | throw new Error("Subscribing twice is disallowed"); 75 | } 76 | 77 | this.observer = subscriber; 78 | this.cancellable = this.exchangeFunction(this); 79 | 80 | return this; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /packages/rsocket-core/__tests__/__snapshots__/ClientServerMultiplexerDemultiplexer.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ClientServerMultiplexerDemultiplexer when receiving data when buffer contains a single frame deserializes received frames and calls the configured handler 1`] = ` 4 | { 5 | "data": { 6 | "data": [ 7 | 104, 8 | 101, 9 | 108, 10 | 108, 11 | 111, 12 | 32, 13 | 119, 14 | 111, 15 | 114, 16 | 108, 17 | 100, 18 | ], 19 | "type": "Buffer", 20 | }, 21 | "dataMimeType": "application/octet-stream", 22 | "flags": 256, 23 | "keepAlive": 60000, 24 | "lifetime": 300000, 25 | "majorVersion": 1, 26 | "metadata": { 27 | "data": [ 28 | 104, 29 | 101, 30 | 108, 31 | 108, 32 | 111, 33 | 32, 34 | 119, 35 | 111, 36 | 114, 37 | 108, 38 | 100, 39 | ], 40 | "type": "Buffer", 41 | }, 42 | "metadataMimeType": "application/octet-stream", 43 | "minorVersion": 0, 44 | "resumeToken": null, 45 | "streamId": 0, 46 | "type": 1, 47 | } 48 | `; 49 | 50 | exports[`ClientServerMultiplexerDemultiplexer when receiving data when buffer contains multiple frames deserializes received frames and calls the configured handler for each frame 1`] = ` 51 | [ 52 | { 53 | "data": { 54 | "data": [ 55 | 104, 56 | 101, 57 | 108, 58 | 108, 59 | 111, 60 | 32, 61 | 119, 62 | 111, 63 | 114, 64 | 108, 65 | 100, 66 | ], 67 | "type": "Buffer", 68 | }, 69 | "flags": 32, 70 | "metadata": undefined, 71 | "streamId": 1, 72 | "type": 10, 73 | }, 74 | ] 75 | `; 76 | 77 | exports[`ClientServerMultiplexerDemultiplexer when receiving data when buffer contains multiple frames deserializes received frames and calls the configured handler for each frame 2`] = ` 78 | [ 79 | { 80 | "data": { 81 | "data": [ 82 | 104, 83 | 101, 84 | 108, 85 | 108, 86 | 111, 87 | 32, 88 | 119, 89 | 111, 90 | 114, 91 | 108, 92 | 100, 93 | 32, 94 | 50, 95 | ], 96 | "type": "Buffer", 97 | }, 98 | "flags": 32, 99 | "metadata": undefined, 100 | "streamId": 1, 101 | "type": 10, 102 | }, 103 | ] 104 | `; 105 | -------------------------------------------------------------------------------- /packages/rsocket-core/src/Reassembler.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Payload } from "./RSocket"; 18 | 19 | export interface FragmentsHolder { 20 | hasFragments: boolean; 21 | data: Buffer | undefined | null; 22 | metadata: Buffer | undefined | null; 23 | } 24 | 25 | export function add( 26 | holder: FragmentsHolder, 27 | dataFragment: Buffer, 28 | metadataFragment?: Buffer | undefined | null 29 | ): boolean { 30 | if (!holder.hasFragments) { 31 | holder.hasFragments = true; 32 | holder.data = dataFragment; 33 | if (metadataFragment) { 34 | holder.metadata = metadataFragment; 35 | } 36 | return true; 37 | } 38 | 39 | // TODO: add validation 40 | holder.data = holder.data 41 | ? Buffer.concat([holder.data, dataFragment]) 42 | : dataFragment; 43 | if (holder.metadata && metadataFragment) { 44 | holder.metadata = Buffer.concat([holder.metadata, metadataFragment]); 45 | } 46 | 47 | return true; 48 | } 49 | 50 | export function reassemble( 51 | holder: FragmentsHolder, 52 | dataFragment: Buffer, 53 | metadataFragment: Buffer | undefined | null 54 | ): Payload { 55 | // TODO: add validation 56 | holder.hasFragments = false; 57 | 58 | const data = holder.data 59 | ? Buffer.concat([holder.data, dataFragment]) 60 | : dataFragment; 61 | 62 | holder.data = undefined; 63 | 64 | if (holder.metadata) { 65 | const metadata = metadataFragment 66 | ? Buffer.concat([holder.metadata, metadataFragment]) 67 | : holder.metadata; 68 | 69 | holder.metadata = undefined; 70 | 71 | return { 72 | data, 73 | metadata, 74 | }; 75 | } 76 | 77 | return { 78 | data, 79 | }; 80 | } 81 | 82 | export function cancel(holder: FragmentsHolder): void { 83 | holder.hasFragments = false; 84 | holder.data = undefined; 85 | holder.metadata = undefined; 86 | } 87 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | pull_request: 7 | branches: [ $default-branch ] 8 | 9 | jobs: 10 | lint: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | 17 | - name: Install Node 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version-file: .nvmrc 21 | 22 | - name: Install Yarn 23 | run: npm install -g yarn 24 | 25 | - name: Cache dependencies 26 | uses: actions/cache@v4 27 | with: 28 | path: ~/.yarn 29 | key: ${{ runner.OS }}-node-${{ hashFiles('**/yarn.lock') }} 30 | restore-keys: | 31 | ${{ runner.OS }}-node- 32 | ${{ runner.OS }}- 33 | 34 | - name: Install dependencies 35 | run: yarn --frozen-lockfile 36 | 37 | - name: Lint 38 | run: yarn lint 39 | 40 | test: 41 | runs-on: ubuntu-latest 42 | 43 | steps: 44 | - name: Checkout 45 | uses: actions/checkout@v4 46 | 47 | - name: Install Node 48 | uses: actions/setup-node@v4 49 | with: 50 | node-version-file: .nvmrc 51 | 52 | - name: Install Yarn 53 | run: npm install -g yarn 54 | 55 | - name: Cache dependencies 56 | uses: actions/cache@v4 57 | with: 58 | path: ~/.yarn 59 | key: ${{ runner.OS }}-node-${{ hashFiles('**/yarn.lock') }} 60 | restore-keys: | 61 | ${{ runner.OS }}-node- 62 | ${{ runner.OS }}- 63 | 64 | - name: Install dependencies 65 | run: yarn --frozen-lockfile 66 | 67 | - name: Test 68 | run: yarn test 69 | 70 | compile: 71 | runs-on: ubuntu-latest 72 | 73 | steps: 74 | - name: Checkout 75 | uses: actions/checkout@v4 76 | 77 | - name: Install Node 78 | uses: actions/setup-node@v4 79 | with: 80 | node-version-file: .nvmrc 81 | 82 | - name: Install Yarn 83 | run: npm install -g yarn 84 | 85 | - name: Cache dependencies 86 | uses: actions/cache@v4 87 | with: 88 | path: ~/.yarn 89 | key: ${{ runner.OS }}-node-${{ hashFiles('**/yarn.lock') }} 90 | restore-keys: | 91 | ${{ runner.OS }}-node- 92 | ${{ runner.OS }}- 93 | 94 | - name: Install dependencies 95 | run: yarn --frozen-lockfile 96 | 97 | - name: Compile 98 | run: yarn build 99 | -------------------------------------------------------------------------------- /packages/rsocket-core/__tests__/RSocketServer.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Closeable, 3 | DuplexConnection, 4 | ErrorCodes, 5 | Frame, 6 | FrameTypes, 7 | Outbound, 8 | RSocketError, 9 | RSocketServer, 10 | ServerTransport, 11 | } from "../src"; 12 | import { mock } from "jest-mock-extended"; 13 | import { 14 | ClientServerInputMultiplexerDemultiplexer, 15 | StreamIdGenerator, 16 | } from "../src/ClientServerMultiplexerDemultiplexer"; 17 | 18 | describe("RSocketServer", () => { 19 | describe("When receiving the first frame", () => { 20 | const unsupportedFirstFrameTypes = [ 21 | FrameTypes.RESERVED, 22 | FrameTypes.LEASE, 23 | FrameTypes.KEEPALIVE, 24 | FrameTypes.REQUEST_RESPONSE, 25 | FrameTypes.REQUEST_FNF, 26 | FrameTypes.REQUEST_STREAM, 27 | FrameTypes.REQUEST_CHANNEL, 28 | FrameTypes.REQUEST_N, 29 | FrameTypes.CANCEL, 30 | FrameTypes.PAYLOAD, 31 | FrameTypes.ERROR, 32 | FrameTypes.METADATA_PUSH, 33 | FrameTypes.RESUME_OK, 34 | FrameTypes.EXT, 35 | ]; 36 | 37 | for (const frameTypeKey of unsupportedFirstFrameTypes) { 38 | it(`${FrameTypes[frameTypeKey]} is rejected with an UNSUPPORTED_SETUP error`, async function () { 39 | const mockTransport = mock(); 40 | const mockClosable = mock(); 41 | const mockOutbound = mock(); 42 | const mockConnection = mock({ 43 | multiplexerDemultiplexer: 44 | new ClientServerInputMultiplexerDemultiplexer( 45 | StreamIdGenerator.create(-1), 46 | mockOutbound, 47 | mockOutbound 48 | ), 49 | }); 50 | mockTransport.bind.mockImplementation( 51 | jest.fn(async (acceptor) => { 52 | // @ts-ignore 53 | const frame = { 54 | type: frameTypeKey, 55 | } as Frame; 56 | await acceptor(frame, mockConnection); 57 | return Promise.resolve(mockClosable); 58 | }) 59 | ); 60 | const server = new RSocketServer({ 61 | transport: mockTransport, 62 | acceptor: undefined, 63 | }); 64 | await server.bind(); 65 | expect(mockConnection.close).toBeCalled(); 66 | const call0Args = mockConnection.close.mock.calls[0]; 67 | const error: RSocketError = call0Args[0]; 68 | expect(ErrorCodes[error.code]).toEqual( 69 | ErrorCodes[ErrorCodes.UNSUPPORTED_SETUP] 70 | ); 71 | }); 72 | } 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /packages/rsocket-websocket-client/src/WebsocketClientTransport.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | ClientTransport, 19 | Closeable, 20 | Demultiplexer, 21 | Deserializer, 22 | DuplexConnection, 23 | FrameHandler, 24 | Multiplexer, 25 | Outbound, 26 | } from "rsocket-core"; 27 | import { WebsocketDuplexConnection } from "./WebsocketDuplexConnection"; 28 | 29 | export type ClientOptions = { 30 | url: string; 31 | wsCreator?: (url: string) => WebSocket; 32 | debug?: boolean; 33 | }; 34 | 35 | export class WebsocketClientTransport implements ClientTransport { 36 | private readonly url: string; 37 | private readonly factory: (url: string) => WebSocket; 38 | 39 | constructor(options: ClientOptions) { 40 | this.url = options.url; 41 | this.factory = options.wsCreator ?? ((url: string) => new WebSocket(url)); 42 | } 43 | 44 | connect( 45 | multiplexerDemultiplexerFactory: ( 46 | outbound: Outbound & Closeable 47 | ) => Multiplexer & Demultiplexer & FrameHandler 48 | ): Promise { 49 | return new Promise((resolve, reject) => { 50 | const websocket = this.factory(this.url); 51 | 52 | websocket.binaryType = "arraybuffer"; 53 | 54 | const openListener = () => { 55 | websocket.removeEventListener("open", openListener); 56 | websocket.removeEventListener("error", errorListener); 57 | resolve( 58 | new WebsocketDuplexConnection( 59 | websocket, 60 | new Deserializer(), 61 | multiplexerDemultiplexerFactory 62 | ) 63 | ); 64 | }; 65 | 66 | const errorListener = (ev: ErrorEvent) => { 67 | websocket.removeEventListener("open", openListener); 68 | websocket.removeEventListener("error", errorListener); 69 | reject(ev.error); 70 | }; 71 | 72 | websocket.addEventListener("open", openListener); 73 | websocket.addEventListener("error", errorListener); 74 | }); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /packages/rsocket-tcp-client/src/__tests__/TcpClientTransport.spec.ts: -------------------------------------------------------------------------------- 1 | import { TcpClientTransport } from "../index"; 2 | import { TcpDuplexConnection } from "../TcpDuplexConnection"; 3 | import * as net from "net"; 4 | import sinon from "sinon"; 5 | import EventEmitter from "events"; 6 | import { Demultiplexer, FrameHandler, Multiplexer } from "rsocket-core/src"; 7 | import { mock } from "jest-mock-extended"; 8 | 9 | describe("TcpClientTransport", function () { 10 | describe("connect", () => { 11 | it("resolves to an instance of DuplexConnection on successful connection", async () => { 12 | // arrange 13 | const netStub = new EventEmitter(); 14 | const multiplexerDemultiplexer = mock< 15 | Multiplexer & Demultiplexer & FrameHandler 16 | >(); 17 | const socketStub = sinon.createStubInstance(net.Socket); 18 | 19 | const transport = new TcpClientTransport({ 20 | connectionOptions: { 21 | host: "localhost", 22 | port: 9090, 23 | }, 24 | // @ts-ignore 25 | socketCreator: () => { 26 | return netStub; 27 | }, 28 | }); 29 | 30 | // act 31 | const connectionPromise = transport.connect( 32 | () => multiplexerDemultiplexer 33 | ); 34 | 35 | netStub.emit("connect", socketStub); 36 | 37 | // assert 38 | await expect(connectionPromise).resolves.toBeInstanceOf( 39 | TcpDuplexConnection 40 | ); 41 | }); 42 | 43 | it("rejects if the connection cannot be established", async () => { 44 | // arrange 45 | const connectionRefusedError = new Error(); 46 | // @ts-ignore 47 | connectionRefusedError.address = "127.0.0.1"; 48 | // @ts-ignore 49 | connectionRefusedError.code = "ECONNREFUSED"; 50 | // @ts-ignore 51 | connectionRefusedError.errno = -4078; 52 | // @ts-ignore 53 | connectionRefusedError.port = 9090; 54 | // @ts-ignore 55 | connectionRefusedError.syscall = "connect"; 56 | 57 | const socketStub = new EventEmitter(); 58 | const multiplexerDemultiplexer = mock< 59 | Multiplexer & Demultiplexer & FrameHandler 60 | >(); 61 | 62 | const transport = new TcpClientTransport({ 63 | connectionOptions: { 64 | host: "localhost", 65 | port: 9090, 66 | }, 67 | // @ts-ignore 68 | socketCreator: () => { 69 | return socketStub; 70 | }, 71 | }); 72 | 73 | // act 74 | const connectionPromise = transport.connect( 75 | () => multiplexerDemultiplexer 76 | ); 77 | 78 | socketStub.emit("error", connectionRefusedError); 79 | 80 | // assert 81 | await expect(connectionPromise).rejects.toEqual(connectionRefusedError); 82 | }); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [rsocket-js](https://github.com/rsocket/rsocket-js) 2 | 3 | [![Build](https://github.com/rsocket/rsocket-js/actions/workflows/build.yml/badge.svg?branch=1.0.x)](https://github.com/rsocket/rsocket-js/actions/workflows/build.yml) 4 | 5 | A JavaScript implementation of the [RSocket](https://github.com/rsocket/rsocket) 6 | protocol intended for use in browsers and/or Node.js. From [rsocket.io](http://rsocket.io/): 7 | 8 | > [RSocket] is an application protocol providing 9 | > [Reactive Streams](http://www.reactive-streams.org/) semantics over an 10 | > asynchronous, binary boundary. 11 | > 12 | > It enables the following symmetric interaction models via async message 13 | > passing over a single connection: 14 | > 15 | > - request/response (stream of 1) 16 | > - request/stream (finite stream of many) 17 | > - fire-and-forget (no response) 18 | > - event subscription (infinite stream of many) 19 | > - channel (bi-directional streams) 20 | 21 | ## Status 22 | 23 | This branch contains a rewrite (with significant changes) of rsocket-js from [Flow](https://flow.org/) to [TypeScript](https://www.typescriptlang.org/). Please see [#158](https://github.com/rsocket/rsocket-js/issues/158) for additional details. 24 | 25 | The artifacts published from this branch are considered UNSTABLE and may be subject to breaking changes while in preview. 26 | 27 | **Please see the [master](https://github.com/rsocket/rsocket-js/tree/master) branch for sources related to `0.x.x` versions.** 28 | 29 | ## Installation 30 | 31 | Individual packages published from this monorepo are distributed via NPM. 32 | 33 | Packages are independently versioned. 34 | 35 | - [rsocket-core](https://www.npmjs.com/package/rsocket-core) 36 | - [rsocket-messaging](https://www.npmjs.com/package/rsocket-messaging) 37 | - [rsocket-composite-metadata](https://www.npmjs.com/package/rsocket-composite-metadata) 38 | - [rsocket-tcp-client](https://www.npmjs.com/package/rsocket-tcp-client) 39 | - [rsocket-tcp-server](https://www.npmjs.com/package/rsocket-tcp-server) 40 | - [rsocket-websocket-client](https://www.npmjs.com/package/rsocket-websocket-client) 41 | - [rsocket-websocket-server](https://www.npmjs.com/package/rsocket-websocket-server) 42 | - [rsocket-adapter-rxjs](https://www.npmjs.com/package/rsocket-adapter-rxjs) 43 | - [rsocket-graphql-apollo-link](https://www.npmjs.com/package/rsocket-graphql-apollo-link) 44 | - [rsocket-graphql-apollo-server](https://www.npmjs.com/package/rsocket-graphql-apollo-server) 45 | 46 | ## Contributing 47 | 48 | TODO: add `CONTRIBUTING.md` 49 | 50 | ## Documentation & Examples 51 | 52 | See [packages/rsocket-examples](https://github.com/rsocket/rsocket-js/tree/1.0.x-alpha/packages/rsocket-examples/src) for examples. 53 | 54 | Guides for `0.x.x` versions can be found on https://rsocket.io/guides/rsocket-js. 55 | 56 | ## License 57 | 58 | See LICENSE file. 59 | -------------------------------------------------------------------------------- /packages/rsocket-composite-metadata/src/RoutingMetadata.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export class RoutingMetadata implements Iterable { 18 | _buffer: Buffer; 19 | 20 | constructor(buffer: Buffer) { 21 | this._buffer = buffer; 22 | } 23 | 24 | iterator(): Iterator { 25 | return decodeRoutes(this._buffer); 26 | } 27 | 28 | [Symbol.iterator](): Iterator { 29 | return decodeRoutes(this._buffer); 30 | } 31 | } 32 | 33 | /** 34 | * Encode given set of routes into {@link Buffer} following the Routing Metadata Layout 35 | * 36 | * @param routes non-empty set of routes 37 | * @returns {Buffer} with encoded content 38 | */ 39 | export function encodeRoutes(...routes: string[]): Buffer { 40 | if (routes.length < 1) { 41 | throw new Error("routes should be non empty array"); 42 | } 43 | 44 | return Buffer.concat(routes.map((route) => encodeRoute(route))); 45 | } 46 | 47 | export function encodeRoute(route: string): Buffer { 48 | const encodedRoute = Buffer.from(route, "utf8"); 49 | 50 | if (encodedRoute.length > 255) { 51 | throw new Error( 52 | `route length should fit into unsigned byte length but the given one is ${encodedRoute.length}` 53 | ); 54 | } 55 | 56 | const encodedLength = Buffer.allocUnsafe(1); 57 | 58 | encodedLength.writeUInt8(encodedRoute.length); 59 | 60 | return Buffer.concat([encodedLength, encodedRoute]); 61 | } 62 | 63 | export function* decodeRoutes( 64 | routeMetadataBuffer: Buffer 65 | ): Generator { 66 | const length = routeMetadataBuffer.byteLength; 67 | let offset = 0; 68 | 69 | while (offset < length) { 70 | const routeLength = routeMetadataBuffer.readUInt8(offset++); 71 | 72 | if (offset + routeLength > length) { 73 | throw new Error( 74 | `Malformed RouteMetadata. Offset(${offset}) + RouteLength(${routeLength}) is greater than TotalLength` 75 | ); 76 | } 77 | 78 | const route = routeMetadataBuffer.toString( 79 | "utf8", 80 | offset, 81 | offset + routeLength 82 | ); 83 | offset += routeLength; 84 | yield route; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /packages/rsocket-examples/src/webpack/browser-bundle/src/app.js: -------------------------------------------------------------------------------- 1 | let state = "CONNECTING"; 2 | let outputDiv = document.querySelector("#output"); 3 | let _rsocket = null; 4 | let errorColor = "#eb4034"; 5 | let infoColor = "#348CEBFF"; 6 | let messageColor = "#2ccd20"; 7 | 8 | function sendMessage(message) { 9 | if (state !== "CONNECTED") { 10 | const div = document.createElement("div"); 11 | div.textContent = `[${new Date().toISOString()}] not connected. cannot send messages!`; 12 | div.style.color = errorColor; 13 | outputDiv.appendChild(div); 14 | return; 15 | } 16 | const bufferData = rsocket.createBuffer(message || ""); 17 | _rsocket.requestResponse( 18 | { 19 | data: bufferData, 20 | }, 21 | { 22 | onError: function (e) { 23 | console.error(e); 24 | }, 25 | onNext: function (payload, isComplete) { 26 | const div = document.createElement("div"); 27 | div.textContent = `[${new Date().toISOString()}] received: payload[data: ${ 28 | payload.data 29 | }; metadata: ${payload.metadata}]|${isComplete}`; 30 | div.style.color = messageColor; 31 | outputDiv.appendChild(div); 32 | }, 33 | onComplete: function () { 34 | const div = document.createElement("div"); 35 | div.textContent = `Stream completed...`; 36 | outputDiv.appendChild(div); 37 | }, 38 | onExtension: function () {}, 39 | } 40 | ); 41 | } 42 | 43 | let sendButton = document.querySelector("#send-button"); 44 | sendButton.addEventListener("click", function () { 45 | let input = document.querySelector("#input-field"); 46 | let value = input.value; 47 | if (!value.length) { 48 | const div = document.createElement("div"); 49 | div.textContent = `[${new Date().toISOString()}] please include a message!`; 50 | div.style.color = errorColor; 51 | outputDiv.appendChild(div); 52 | return; 53 | } 54 | const div = document.createElement("div"); 55 | div.textContent = `[${new Date().toISOString()}] sending: ${value}`; 56 | div.style.color = infoColor; 57 | outputDiv.appendChild(div); 58 | sendMessage(value); 59 | }); 60 | 61 | rsocket 62 | .connect({ 63 | url: "ws://localhost:9090", 64 | }) 65 | .then(function (_r) { 66 | state = "CONNECTED"; 67 | _rsocket = _r; 68 | const div = document.createElement("div"); 69 | div.textContent = `[${new Date().toISOString()}] connected!`; 70 | div.style.color = infoColor; 71 | outputDiv.appendChild(div); 72 | }) 73 | .catch(function (err) { 74 | const errorMessage = 75 | err?.message || 76 | "failed to connect to rsocket! check the console for more details."; 77 | if (err) { 78 | console.error("failed to connect rsocket: " + err.message); 79 | } else { 80 | console.error("failed to connect rsocket!"); 81 | } 82 | const div = document.createElement("div"); 83 | div.textContent = `[${new Date().toISOString()}] ${errorMessage}`; 84 | div.style.color = errorColor; 85 | outputDiv.appendChild(div); 86 | }); 87 | -------------------------------------------------------------------------------- /packages/rsocket-graphql-apollo-link/src/RSocketQueryLink.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | "use strict"; 18 | 19 | import { 20 | ApolloLink, 21 | FetchResult, 22 | Observable, 23 | Operation, 24 | } from "@apollo/client/core"; 25 | import { Payload, RSocket } from "rsocket-core"; 26 | import { 27 | encodeCompositeMetadata, 28 | encodeRoutes, 29 | WellKnownMimeType, 30 | } from "rsocket-composite-metadata"; 31 | import { print } from "graphql"; 32 | 33 | type QueryLinkOptions = { 34 | /** 35 | * The route that the RSocket server is listening for GraphQL messages on. 36 | */ 37 | route?: string; 38 | }; 39 | 40 | export class RSocketQueryLink extends ApolloLink { 41 | constructor( 42 | public readonly client: RSocket, 43 | public readonly options: QueryLinkOptions 44 | ) { 45 | super(); 46 | } 47 | 48 | public request(operation: Operation): Observable | null { 49 | const json = JSON.stringify({ 50 | ...operation, 51 | // per spec query should be a string (https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md#example-1) 52 | query: print(operation.query), 53 | }); 54 | const encodedData = Buffer.from(json); 55 | 56 | const metadata = new Map(); 57 | metadata.set( 58 | WellKnownMimeType.MESSAGE_RSOCKET_MIMETYPE, 59 | Buffer.from(WellKnownMimeType.APPLICATION_JSON.toString()) 60 | ); 61 | if (this.options?.route) { 62 | metadata.set( 63 | WellKnownMimeType.MESSAGE_RSOCKET_ROUTING, 64 | encodeRoutes(this.options.route) 65 | ); 66 | } 67 | 68 | const encodedMetadata = encodeCompositeMetadata(metadata); 69 | 70 | return new Observable((observer) => { 71 | this.client.requestResponse( 72 | { 73 | data: encodedData, 74 | metadata: encodedMetadata, 75 | }, 76 | { 77 | onComplete(): void {}, 78 | onError(error: Error): void { 79 | observer.error(error); 80 | }, 81 | onExtension(): void {}, 82 | onNext(payload: Payload, isComplete: boolean): void { 83 | const { data } = payload; 84 | const decoded = data.toString(); 85 | const deserialized = JSON.parse(decoded); 86 | observer.next(deserialized); 87 | observer.complete(); 88 | }, 89 | } 90 | ); 91 | }); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /packages/rsocket-tcp-client/src/TcpClientTransport.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | ClientTransport, 19 | Closeable, 20 | Demultiplexer, 21 | Deserializer, 22 | DuplexConnection, 23 | FrameHandler, 24 | Multiplexer, 25 | Outbound, 26 | } from "rsocket-core"; 27 | import net, { SocketConnectOpts } from "net"; 28 | import { TcpDuplexConnection } from "./TcpDuplexConnection"; 29 | 30 | type TcpSocketCreator = ( 31 | options: SocketConnectOpts, 32 | connectionListener?: () => void 33 | ) => net.Socket; 34 | 35 | type TcpClientOptionsSocketFactory = ( 36 | options: net.NetConnectOpts 37 | ) => net.Socket; 38 | 39 | type TcpClientOptions = { 40 | connectionOptions: net.NetConnectOpts; 41 | socketCreator?: TcpClientOptionsSocketFactory; 42 | }; 43 | 44 | export class TcpClientTransport implements ClientTransport { 45 | private readonly connectionOptions: net.NetConnectOpts; 46 | private readonly socketCreator: TcpSocketCreator; 47 | 48 | constructor(options: TcpClientOptions) { 49 | this.connectionOptions = options.connectionOptions; 50 | this.socketCreator = 51 | options.socketCreator ?? ((options) => net.connect(options)); 52 | } 53 | 54 | connect( 55 | multiplexerDemultiplexerFactory: ( 56 | outbound: Outbound & Closeable 57 | ) => Multiplexer & Demultiplexer & FrameHandler 58 | ): Promise { 59 | return new Promise((resolve, reject) => { 60 | let socket: net.Socket; 61 | 62 | const openListener = () => { 63 | socket.removeListener("error", errorListener); 64 | socket.removeListener("close", errorListener); 65 | socket.removeListener("end", errorListener); 66 | resolve( 67 | new TcpDuplexConnection( 68 | socket, 69 | new Deserializer(), 70 | multiplexerDemultiplexerFactory 71 | ) 72 | ); 73 | }; 74 | 75 | const errorListener = (error: Error) => { 76 | socket.removeListener("error", errorListener); 77 | socket.removeListener("close", errorListener); 78 | socket.removeListener("end", errorListener); 79 | reject(error); 80 | }; 81 | 82 | socket = this.socketCreator(this.connectionOptions); 83 | 84 | socket.once("connect", openListener); 85 | socket.once("error", errorListener); 86 | socket.once("close", errorListener); 87 | socket.once("end", errorListener); 88 | }); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Test, Build, Release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | shouldPublish: 7 | description: 'Publish' 8 | required: true 9 | default: true 10 | type: boolean 11 | 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | 20 | - name: Install Node 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version-file: .nvmrc 24 | 25 | - name: Install Yarn 26 | run: npm install -g yarn 27 | 28 | - name: Cache dependencies 29 | uses: actions/cache@v4 30 | with: 31 | path: ~/.yarn 32 | key: ${{ runner.OS }}-node-${{ hashFiles('**/yarn.lock') }} 33 | restore-keys: | 34 | ${{ runner.OS }}-node- 35 | ${{ runner.OS }}- 36 | 37 | - name: Install dependencies 38 | run: yarn --frozen-lockfile 39 | 40 | - name: Lint 41 | run: yarn lint 42 | 43 | test: 44 | runs-on: ubuntu-latest 45 | 46 | steps: 47 | - name: Checkout 48 | uses: actions/checkout@v4 49 | 50 | - name: Install Node 51 | uses: actions/setup-node@v4 52 | with: 53 | node-version-file: .nvmrc 54 | 55 | - name: Install Yarn 56 | run: npm install -g yarn 57 | 58 | - name: Cache dependencies 59 | uses: actions/cache@v4 60 | with: 61 | path: ~/.yarn 62 | key: ${{ runner.OS }}-node-${{ hashFiles('**/yarn.lock') }} 63 | restore-keys: | 64 | ${{ runner.OS }}-node- 65 | ${{ runner.OS }}- 66 | 67 | - name: Install dependencies 68 | run: yarn --frozen-lockfile 69 | 70 | - name: Test 71 | run: yarn test 72 | 73 | compile_publish: 74 | needs: 75 | - lint 76 | - test 77 | 78 | runs-on: ubuntu-latest 79 | 80 | steps: 81 | - name: Checkout 82 | uses: actions/checkout@v4 83 | 84 | - name: Install Node 85 | uses: actions/setup-node@v4 86 | with: 87 | node-version-file: .nvmrc 88 | 89 | - name: Install Yarn 90 | run: npm install -g yarn 91 | 92 | - name: Cache dependencies 93 | uses: actions/cache@v4 94 | with: 95 | path: ~/.yarn 96 | key: ${{ runner.OS }}-node-${{ hashFiles('**/yarn.lock') }} 97 | restore-keys: | 98 | ${{ runner.OS }}-node- 99 | ${{ runner.OS }}- 100 | 101 | - name: Install dependencies 102 | run: yarn --frozen-lockfile 103 | 104 | - name: Compile 105 | run: yarn build 106 | 107 | - uses: fregante/setup-git-user@v1 108 | 109 | - name: Set NPM Token 110 | run: echo //registry.npmjs.org/:_authToken=${NPM_TOKEN} > .npmrc 111 | env: 112 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 113 | 114 | - name: Publish 115 | if: ${{ github.event.inputs.shouldPublish == 'true' }} 116 | run: yarn lerna publish from-package --yes --no-verify-access 117 | 118 | -------------------------------------------------------------------------------- /packages/rsocket-websocket-client/src/WebsocketDuplexConnection.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | Closeable, 19 | Deferred, 20 | Demultiplexer, 21 | Deserializer, 22 | DuplexConnection, 23 | Frame, 24 | FrameHandler, 25 | Multiplexer, 26 | Outbound, 27 | serializeFrame, 28 | } from "rsocket-core"; 29 | 30 | export class WebsocketDuplexConnection 31 | extends Deferred 32 | implements DuplexConnection, Outbound 33 | { 34 | readonly multiplexerDemultiplexer: Multiplexer & Demultiplexer & FrameHandler; 35 | 36 | constructor( 37 | private websocket: WebSocket, 38 | private deserializer: Deserializer, 39 | multiplexerDemultiplexerFactory: ( 40 | outbound: Outbound & Closeable 41 | ) => Multiplexer & Demultiplexer & FrameHandler 42 | ) { 43 | super(); 44 | 45 | websocket.addEventListener("close", this.handleClosed); 46 | websocket.addEventListener("error", this.handleError); 47 | websocket.addEventListener("message", this.handleMessage); 48 | 49 | this.multiplexerDemultiplexer = multiplexerDemultiplexerFactory(this); 50 | } 51 | 52 | get availability(): number { 53 | return this.done ? 0 : 1; 54 | } 55 | 56 | close(error?: Error) { 57 | if (this.done) { 58 | super.close(error); 59 | return; 60 | } 61 | 62 | this.websocket.removeEventListener("close", this.handleClosed); 63 | this.websocket.removeEventListener("error", this.handleError); 64 | this.websocket.removeEventListener("message", this.handleMessage); 65 | 66 | this.websocket.close(); 67 | 68 | delete this.websocket; 69 | 70 | super.close(error); 71 | } 72 | 73 | send(frame: Frame): void { 74 | if (this.done) { 75 | return; 76 | } 77 | 78 | const buffer = serializeFrame(frame); 79 | 80 | this.websocket.send(buffer); 81 | } 82 | 83 | private handleClosed = (e: CloseEvent): void => { 84 | this.close( 85 | new Error( 86 | e.reason || "WebsocketDuplexConnection: Socket closed unexpectedly." 87 | ) 88 | ); 89 | }; 90 | 91 | private handleError = (e: ErrorEvent): void => { 92 | this.close(e.error); 93 | }; 94 | 95 | private handleMessage = (message: MessageEvent): void => { 96 | try { 97 | const buffer = Buffer.from(message.data); 98 | const frame = this.deserializer.deserializeFrame(buffer); 99 | 100 | this.multiplexerDemultiplexer.handle(frame); 101 | } catch (error) { 102 | this.close(error); 103 | } 104 | }; 105 | } 106 | -------------------------------------------------------------------------------- /packages/rsocket-adapter-rxjs/src/RSocketPublisherToPrefetchingObservable.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | "use strict"; 17 | import { 18 | Cancellable, 19 | OnExtensionSubscriber, 20 | OnNextSubscriber, 21 | OnTerminalSubscriber, 22 | Payload, 23 | Requestable, 24 | } from "rsocket-core"; 25 | import { Codec } from "rsocket-messaging"; 26 | import { 27 | asyncScheduler, 28 | Observable, 29 | SchedulerLike, 30 | Subscriber, 31 | TeardownLogic, 32 | Unsubscribable, 33 | } from "rxjs"; 34 | 35 | export default class RSocketPublisherToPrefetchingObservable< 36 | T, 37 | TSignalSender extends Requestable & Cancellable & OnExtensionSubscriber 38 | > 39 | extends Observable 40 | implements 41 | OnTerminalSubscriber, 42 | OnNextSubscriber, 43 | OnExtensionSubscriber, 44 | Unsubscribable 45 | { 46 | private readonly limit; 47 | private observer: Subscriber; 48 | protected subscriber: TSignalSender; 49 | 50 | private received: number = 0; 51 | 52 | constructor( 53 | private readonly exchangeFunction: ( 54 | subscriber: OnNextSubscriber & 55 | OnTerminalSubscriber & 56 | OnExtensionSubscriber, 57 | n: number 58 | ) => TSignalSender, 59 | protected readonly prefetch: number, 60 | private readonly responseCodec?: Codec, 61 | protected readonly scheduler: SchedulerLike = asyncScheduler 62 | ) { 63 | super(); 64 | 65 | this.limit = prefetch - (prefetch >> 2); 66 | } 67 | 68 | onNext(payload: Payload, isComplete: boolean): void { 69 | this.received++; 70 | this.observer.next(this.responseCodec.decode(payload.data)); 71 | 72 | if (isComplete) { 73 | this.observer.complete(); 74 | return; 75 | } 76 | 77 | if (this.received % this.limit === 0) { 78 | this.scheduler.schedule(() => this.subscriber.request(this.limit)); 79 | return; 80 | } 81 | } 82 | 83 | onError(error: Error): void { 84 | this.observer.error(error); 85 | } 86 | 87 | onComplete(): void { 88 | this.observer.complete(); 89 | } 90 | 91 | onExtension( 92 | extendedType: number, 93 | content: Buffer, 94 | canBeIgnored: boolean 95 | ): void {} 96 | 97 | unsubscribe(): void { 98 | this.subscriber.cancel(); 99 | } 100 | 101 | protected _subscribe(observer: Subscriber): TeardownLogic { 102 | if (this.observer) { 103 | throw new Error("Subscribing twice is disallowed"); 104 | } 105 | 106 | this.observer = observer; 107 | this.subscriber = this.exchangeFunction(this, this.prefetch); 108 | 109 | return this; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | **/dist 3 | **/build 4 | 5 | .gitpod.yml 6 | results 7 | .nyc_output 8 | *.tsbuildinfo 9 | coverage 10 | 11 | # Created by https://www.toptal.com/developers/gitignore/api/intellij,yarn,macos 12 | # Edit at https://www.toptal.com/developers/gitignore?templates=intellij,yarn,macos 13 | 14 | ### Intellij ### 15 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 16 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 17 | .idea/ 18 | 19 | # CMake 20 | cmake-build-*/ 21 | 22 | # Mongo Explorer plugin 23 | .idea/**/mongoSettings.xml 24 | 25 | # File-based project format 26 | *.iws 27 | 28 | # IntelliJ 29 | out/ 30 | 31 | # mpeltonen/sbt-idea plugin 32 | .idea_modules/ 33 | 34 | # JIRA plugin 35 | atlassian-ide-plugin.xml 36 | 37 | # Cursive Clojure plugin 38 | .idea/replstate.xml 39 | 40 | # Crashlytics plugin (for Android Studio and IntelliJ) 41 | com_crashlytics_export_strings.xml 42 | crashlytics.properties 43 | crashlytics-build.properties 44 | fabric.properties 45 | 46 | # Editor-based Rest Client 47 | .idea/httpRequests 48 | 49 | # Android studio 3.1+ serialized cache file 50 | .idea/caches/build_file_checksums.ser 51 | 52 | ### Intellij Patch ### 53 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 54 | 55 | *.iml 56 | # modules.xml 57 | .idea/misc.xml 58 | # *.ipr 59 | 60 | # Sonarlint plugin 61 | # https://plugins.jetbrains.com/plugin/7973-sonarlint 62 | .idea/**/sonarlint/ 63 | 64 | # SonarQube Plugin 65 | # https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin 66 | .idea/**/sonarIssues.xml 67 | 68 | # Markdown Navigator plugin 69 | # https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced 70 | .idea/**/markdown-navigator.xml 71 | .idea/**/markdown-navigator-enh.xml 72 | .idea/**/markdown-navigator/ 73 | 74 | # Cache file creation bug 75 | # See https://youtrack.jetbrains.com/issue/JBR-2257 76 | .idea/$CACHE_FILE$ 77 | 78 | # CodeStream plugin 79 | # https://plugins.jetbrains.com/plugin/12206-codestream 80 | .idea/codestream.xml 81 | 82 | ### yarn ### 83 | # https://yarnpkg.com/advanced/qa#which-files-should-be-gitignored 84 | 85 | yarn-error.log 86 | .yarn/* 87 | !.yarn/releases 88 | !.yarn/plugins 89 | !.yarn/sdks 90 | !.yarn/versions 91 | 92 | # if you are NOT using Zero-installs, then: 93 | # comment the following lines 94 | !.yarn/cache 95 | 96 | # and uncomment the following lines 97 | # .pnp.* 98 | 99 | ### npm ### 100 | # the project uses Yarn so we will not check in package-lock files 101 | package-lock.json 102 | 103 | ### macOS ### 104 | # General 105 | .DS_Store 106 | .AppleDouble 107 | .LSOverride 108 | 109 | # Icon must end with two \r 110 | Icon 111 | 112 | 113 | # Thumbnails 114 | ._* 115 | 116 | # Files that might appear in the root of a volume 117 | .DocumentRevisions-V100 118 | .fseventsd 119 | .Spotlight-V100 120 | .TemporaryItems 121 | .Trashes 122 | .VolumeIcon.icns 123 | .com.apple.timemachine.donotpresent 124 | 125 | # Directories potentially created on remote AFP share 126 | .AppleDB 127 | .AppleDesktop 128 | Network Trash Folder 129 | Temporary Items 130 | .apdisk 131 | 132 | # End of https://www.toptal.com/developers/gitignore/api/intellij,yarn,macos 133 | -------------------------------------------------------------------------------- /packages/rsocket-adapter-rxjs/src/RSocketPublisher2PrefetchingObservableToObserver2BufferingRSocketSubscriber.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | "use strict"; 17 | import { 18 | Cancellable, 19 | OnExtensionSubscriber, 20 | OnNextSubscriber, 21 | OnTerminalSubscriber, 22 | Payload, 23 | Requestable, 24 | } from "rsocket-core"; 25 | import { Codec } from "rsocket-messaging"; 26 | import { asyncScheduler, SchedulerLike, Subscriber, TeardownLogic } from "rxjs"; 27 | import ObserverToBufferingRSocketSubscriber from "./ObserverToBufferingRSocketSubscriber"; 28 | import RSocketPublisherToPrefetchingObservable from "./RSocketPublisherToPrefetchingObservable"; 29 | import { applyMixins } from "./Utils"; 30 | 31 | interface RSocketPublisher2PrefetchingObservableToObserver2BufferingRSocketSubscriber< 32 | IN, 33 | OUT 34 | > extends ObserverToBufferingRSocketSubscriber, 35 | RSocketPublisherToPrefetchingObservable< 36 | IN, 37 | OnTerminalSubscriber & 38 | OnNextSubscriber & 39 | OnExtensionSubscriber & 40 | Requestable & 41 | Cancellable 42 | > { 43 | readonly subscriber: OnNextSubscriber & 44 | OnTerminalSubscriber & 45 | OnExtensionSubscriber & 46 | Requestable & 47 | Cancellable; 48 | forEach(x: any); 49 | } 50 | 51 | class RSocketPublisher2PrefetchingObservableToObserver2BufferingRSocketSubscriber< 52 | IN, 53 | OUT 54 | > extends RSocketPublisherToPrefetchingObservable< 55 | IN, 56 | OnTerminalSubscriber & 57 | OnNextSubscriber & 58 | OnExtensionSubscriber & 59 | Requestable & 60 | Cancellable 61 | > { 62 | constructor( 63 | readonly firstPayload: Payload, 64 | readonly isCompleted: boolean, 65 | subscriber: OnTerminalSubscriber & 66 | OnNextSubscriber & 67 | OnExtensionSubscriber & 68 | Requestable & 69 | Cancellable, 70 | requested: number, 71 | prefetch: number, 72 | outputCodec: Codec, 73 | inputCodec: Codec, 74 | scheduler: SchedulerLike = asyncScheduler 75 | ) { 76 | super(() => subscriber, prefetch, outputCodec, scheduler); 77 | this.init(requested, subscriber, inputCodec); 78 | } 79 | 80 | protected _subscribe(observer: Subscriber): TeardownLogic { 81 | super._subscribe(observer); 82 | 83 | this.onNext(this.firstPayload, this.isCompleted); 84 | 85 | if (!this.isCompleted) { 86 | this.scheduler.schedule(() => this.subscriber.request(this.prefetch - 1)); 87 | } 88 | 89 | return this; 90 | } 91 | } 92 | 93 | applyMixins( 94 | RSocketPublisher2PrefetchingObservableToObserver2BufferingRSocketSubscriber, 95 | [ObserverToBufferingRSocketSubscriber] 96 | ); 97 | 98 | export default RSocketPublisher2PrefetchingObservableToObserver2BufferingRSocketSubscriber; 99 | -------------------------------------------------------------------------------- /packages/rsocket-examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rsocket-examples", 3 | "version": "1.0.0-alpha.4", 4 | "license": "Apache-2.0", 5 | "private": true, 6 | "files": [ 7 | "dist", 8 | "LICENSE" 9 | ], 10 | "scripts": { 11 | "clean": "rimraf -rf ./dist", 12 | "start-client-request-channel": "ts-node -r tsconfig-paths/register src/ClientRequestChannelExample.ts", 13 | "start-client-server-request-channel-resume": "ts-node -r tsconfig-paths/register src/ClientServerRequestChannelResumeExample.ts", 14 | "start-client-request-fnf-with-lease": "ts-node -r tsconfig-paths/register src/tcp/ClienRequestFnfnWithLeaseExampleTcp.ts", 15 | "start-client-composite-metadata-route": "ts-node -r tsconfig-paths/register src/ClientCompositeMetadataRouteExample.ts", 16 | "start-client-server-request-stream-tcp": "ts-node -r tsconfig-paths/register src/tcp/ClientServerRequestStreamExampleTcp.ts", 17 | "start-client-server-request-stream-websocket": "ts-node -r tsconfig-paths/register src/websocket/ClientServerRequestStreamExampleWebSocket.ts", 18 | "start-client-server-request-response-tcp": "ts-node -r tsconfig-paths/register src/tcp/ClientServerRequestResponseExampleTcp.ts", 19 | "start-client-server-request-response-websocket": "ts-node -r tsconfig-paths/register src/websocket/ClientServerRequestResponseExampleWebSocket.ts", 20 | "start-client-server-composite-metadata-route": "ts-node -r tsconfig-paths/register src/ClientServerCompositeMetadataRouteExample.ts", 21 | "start-client-server-rxjs-messaging-composite-metadata-route": "ts-node -r tsconfig-paths/register src/rxjs/RxjsMessagingCompositeMetadataRouteExample.ts", 22 | "start-client-server-rxjs-requester-responder": "ts-node -r tsconfig-paths/register src/rxjs/RxjsRequesterResponderExample.ts", 23 | "start-client-apollo-graphql": "ts-node -r tsconfig-paths/register src/graphql/apollo/client/example.ts", 24 | "start-client-server-apollo-graphql": "ts-node -r tsconfig-paths/register src/graphql/apollo/client-server/example.ts", 25 | "start-client-server-composite-metadata-auth-example-client": "ts-node -r tsconfig-paths/register src/composite-metadata/bearer-token-auth/client.ts", 26 | "start-client-server-composite-metadata-auth-example-server": "ts-node -r tsconfig-paths/register src/composite-metadata/bearer-token-auth/server.ts", 27 | "start-client-server-composite-metadata-auth-setup-frame-example-client": "ts-node -r tsconfig-paths/register src/composite-metadata/bearer-token-auth-setup-frame/client.ts", 28 | "start-client-server-composite-metadata-auth-setup-frame-example-server": "ts-node -r tsconfig-paths/register src/composite-metadata/bearer-token-auth-setup-frame/server.ts" 29 | }, 30 | "dependencies": { 31 | "@apollo/client": "^3.5.10", 32 | "graphql": "^16.0.0", 33 | "graphql-tag": "^2.12.6", 34 | "graphql-subscriptions": "^2.0.0", 35 | "rsocket-adapter-rxjs": "^1.0.0-alpha.4", 36 | "rsocket-composite-metadata": "^1.0.0-alpha.3", 37 | "rsocket-core": "^1.0.0-alpha.3", 38 | "rsocket-tcp-client": "^1.0.0-alpha.3", 39 | "rsocket-tcp-server": "^1.0.0-alpha.3", 40 | "rsocket-websocket-client": "^1.0.0-alpha.3", 41 | "rsocket-websocket-server": "^1.0.0-alpha.3", 42 | "ws": "^8.18" 43 | }, 44 | "devDependencies": { 45 | "rimraf": "~3.0.2", 46 | "ts-node": "~10.4.0", 47 | "tsconfig-paths": "~3.11.0", 48 | "typescript": "~4.5.2" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/rsocket-examples/src/websocket/ClientServerRequestResponseExampleWebSocket.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | OnExtensionSubscriber, 19 | OnNextSubscriber, 20 | OnTerminalSubscriber, 21 | Payload, 22 | RSocketConnector, 23 | RSocketServer, 24 | } from "rsocket-core"; 25 | import { WebsocketClientTransport } from "rsocket-websocket-client"; 26 | import { WebsocketServerTransport } from "rsocket-websocket-server"; 27 | import { exit } from "process"; 28 | import WebSocket from "ws"; 29 | 30 | async function main() { 31 | const server = new RSocketServer({ 32 | transport: new WebsocketServerTransport({ 33 | wsCreator: (options) => { 34 | return new WebSocket.Server({ 35 | port: 8080, 36 | }); 37 | }, 38 | }), 39 | acceptor: { 40 | accept: async () => ({ 41 | requestResponse: ( 42 | payload: Payload, 43 | responderStream: OnTerminalSubscriber & 44 | OnNextSubscriber & 45 | OnExtensionSubscriber 46 | ) => { 47 | const timeout = setTimeout( 48 | () => 49 | responderStream.onNext( 50 | { 51 | data: Buffer.concat([Buffer.from("Echo: "), payload.data]), 52 | }, 53 | true 54 | ), 55 | 1000 56 | ); 57 | return { 58 | cancel: () => { 59 | clearTimeout(timeout); 60 | console.log("cancelled"); 61 | }, 62 | onExtension: () => { 63 | console.log("Received Extension request"); 64 | }, 65 | }; 66 | }, 67 | }), 68 | }, 69 | }); 70 | 71 | const connector = new RSocketConnector({ 72 | transport: new WebsocketClientTransport({ 73 | url: "ws://localhost:8080", 74 | wsCreator: (url) => new WebSocket(url) as any, 75 | }), 76 | }); 77 | 78 | const serverCloseable = await server.bind(); 79 | 80 | const rsocket = await connector.connect(); 81 | 82 | await new Promise((resolve, reject) => 83 | rsocket.requestResponse( 84 | { 85 | data: Buffer.from("Hello World"), 86 | }, 87 | { 88 | onError: (e) => reject(e), 89 | onNext: (payload, isComplete) => { 90 | console.log( 91 | `payload[data: ${payload.data}; metadata: ${payload.metadata}]|${isComplete}` 92 | ); 93 | resolve(payload); 94 | }, 95 | onComplete: () => { 96 | resolve(null); 97 | }, 98 | onExtension: () => {}, 99 | } 100 | ) 101 | ); 102 | 103 | serverCloseable.close(); 104 | } 105 | 106 | main() 107 | .then(() => exit()) 108 | .catch((error: Error) => { 109 | console.error(error); 110 | exit(1); 111 | }); 112 | -------------------------------------------------------------------------------- /packages/rsocket-examples/src/tcp/ClientServerRequestResponseExampleTcp.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | OnExtensionSubscriber, 19 | OnNextSubscriber, 20 | OnTerminalSubscriber, 21 | Payload, 22 | RSocket, 23 | RSocketConnector, 24 | RSocketServer, 25 | } from "rsocket-core"; 26 | import { TcpClientTransport } from "rsocket-tcp-client"; 27 | import { TcpServerTransport } from "rsocket-tcp-server"; 28 | import { exit } from "process"; 29 | 30 | let serverCloseable; 31 | 32 | function makeServer() { 33 | return new RSocketServer({ 34 | transport: new TcpServerTransport({ 35 | listenOptions: { 36 | port: 9090, 37 | host: "127.0.0.1", 38 | }, 39 | }), 40 | acceptor: { 41 | accept: async () => ({ 42 | requestResponse: ( 43 | payload: Payload, 44 | responderStream: OnTerminalSubscriber & 45 | OnNextSubscriber & 46 | OnExtensionSubscriber 47 | ) => { 48 | const timeout = setTimeout( 49 | () => 50 | responderStream.onNext( 51 | { 52 | data: Buffer.concat([Buffer.from("Echo: "), payload.data]), 53 | }, 54 | true 55 | ), 56 | 1000 57 | ); 58 | return { 59 | cancel: () => { 60 | clearTimeout(timeout); 61 | console.log("cancelled"); 62 | }, 63 | onExtension: () => { 64 | console.log("Received Extension request"); 65 | }, 66 | }; 67 | }, 68 | }), 69 | }, 70 | }); 71 | } 72 | 73 | function makeConnector() { 74 | return new RSocketConnector({ 75 | transport: new TcpClientTransport({ 76 | connectionOptions: { 77 | host: "127.0.0.1", 78 | port: 9090, 79 | }, 80 | }), 81 | }); 82 | } 83 | 84 | async function requestResponse(rsocket: RSocket) { 85 | return new Promise((resolve, reject) => { 86 | return rsocket.requestResponse( 87 | { 88 | data: Buffer.from("Hello World"), 89 | }, 90 | { 91 | onError: (e) => { 92 | reject(e); 93 | }, 94 | onNext: (payload, isComplete) => { 95 | console.log( 96 | `payload[data: ${payload.data}; metadata: ${payload.metadata}]|${isComplete}` 97 | ); 98 | resolve(payload); 99 | }, 100 | onComplete: () => {}, 101 | onExtension: () => {}, 102 | } 103 | ); 104 | }); 105 | } 106 | 107 | async function main() { 108 | const server = makeServer(); 109 | const connector = makeConnector(); 110 | 111 | serverCloseable = await server.bind(); 112 | const rsocket = await connector.connect(); 113 | 114 | await requestResponse(rsocket); 115 | } 116 | 117 | main() 118 | .then(() => exit()) 119 | .catch((error: Error) => { 120 | console.error(error); 121 | exit(1); 122 | }) 123 | .finally(() => { 124 | serverCloseable.close(); 125 | }); 126 | -------------------------------------------------------------------------------- /packages/rsocket-adapter-rxjs/src/Observer2BufferingSubscriberToPublisher2PrefetchingObservable.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | "use strict"; 18 | import { 19 | Cancellable, 20 | OnExtensionSubscriber, 21 | OnNextSubscriber, 22 | OnTerminalSubscriber, 23 | Requestable, 24 | } from "rsocket-core"; 25 | import { Codec } from "rsocket-messaging"; 26 | import { 27 | asyncScheduler, 28 | Observable, 29 | Observer, 30 | SchedulerLike, 31 | Subscriber, 32 | TeardownLogic, 33 | Unsubscribable, 34 | } from "rxjs"; 35 | import ObserverToBufferingRSocketSubscriber from "./ObserverToBufferingRSocketSubscriber"; 36 | import RSocketPublisherToPrefetchingObservable from "./RSocketPublisherToPrefetchingObservable"; 37 | import { applyMixins } from "./Utils"; 38 | 39 | interface Observer2BufferingSubscriberToPublisher2PrefetchingObservable 40 | extends ObserverToBufferingRSocketSubscriber, 41 | RSocketPublisherToPrefetchingObservable< 42 | Out, 43 | OnNextSubscriber & 44 | OnTerminalSubscriber & 45 | OnExtensionSubscriber & 46 | Requestable & 47 | Cancellable 48 | > { 49 | subscriber: OnNextSubscriber & 50 | OnTerminalSubscriber & 51 | OnExtensionSubscriber & 52 | Requestable & 53 | Cancellable; 54 | forEach(next: any): any; 55 | } 56 | 57 | class Observer2BufferingSubscriberToPublisher2PrefetchingObservable 58 | extends RSocketPublisherToPrefetchingObservable< 59 | Out, 60 | OnNextSubscriber & 61 | OnTerminalSubscriber & 62 | OnExtensionSubscriber & 63 | Requestable & 64 | Cancellable 65 | > 66 | implements 67 | Observer, 68 | Unsubscribable, 69 | OnNextSubscriber, 70 | OnTerminalSubscriber, 71 | OnExtensionSubscriber, 72 | Requestable, 73 | Cancellable 74 | { 75 | constructor( 76 | exchangeFunction: ( 77 | s: OnNextSubscriber & 78 | OnTerminalSubscriber & 79 | OnExtensionSubscriber & 80 | Requestable & 81 | Cancellable, 82 | n: number 83 | ) => OnNextSubscriber & 84 | OnTerminalSubscriber & 85 | OnExtensionSubscriber & 86 | Requestable & 87 | Cancellable, 88 | prefetch: number, 89 | private readonly restObservable: Observable, 90 | inputCodec: Codec, 91 | outputCodec: Codec, 92 | scheduler: SchedulerLike = asyncScheduler 93 | ) { 94 | super(exchangeFunction, prefetch, outputCodec, scheduler); 95 | this.init(0, undefined, inputCodec); 96 | } 97 | 98 | _subscribe(s: Subscriber): TeardownLogic { 99 | super._subscribe(s); 100 | 101 | this.restObservable.subscribe(this); 102 | 103 | return this; 104 | } 105 | 106 | unsubscribe(): void { 107 | this.subscriber.cancel(); 108 | super.unsubscribe(); 109 | } 110 | } 111 | 112 | applyMixins(Observer2BufferingSubscriberToPublisher2PrefetchingObservable, [ 113 | ObserverToBufferingRSocketSubscriber, 114 | ]); 115 | 116 | export default Observer2BufferingSubscriberToPublisher2PrefetchingObservable; 117 | -------------------------------------------------------------------------------- /packages/rsocket-examples/src/graphql/apollo/client/example.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { RSocket, RSocketConnector } from "rsocket-core"; 18 | import { makeRSocketLink } from "rsocket-graphql-apollo-link"; 19 | import { WebsocketClientTransport } from "rsocket-websocket-client"; 20 | import { 21 | ApolloClient, 22 | InMemoryCache, 23 | NormalizedCacheObject, 24 | } from "@apollo/client/core"; 25 | import gql from "graphql-tag"; 26 | import WebSocket from "ws"; 27 | import { exit } from "process"; 28 | import { WellKnownMimeType } from "rsocket-composite-metadata"; 29 | import { DocumentNode } from "@apollo/client"; 30 | 31 | let rsocketClient: RSocket; 32 | 33 | function makeRSocketConnector() { 34 | return new RSocketConnector({ 35 | setup: { 36 | dataMimeType: WellKnownMimeType.APPLICATION_JSON.toString(), 37 | metadataMimeType: 38 | WellKnownMimeType.MESSAGE_RSOCKET_COMPOSITE_METADATA.toString(), 39 | }, 40 | transport: new WebsocketClientTransport({ 41 | url: "ws://localhost:7000/rsocket", 42 | wsCreator: (url) => new WebSocket(url) as any, 43 | }), 44 | }); 45 | } 46 | 47 | function makeApolloClient({ rsocketClient }) { 48 | return new ApolloClient({ 49 | cache: new InMemoryCache(), 50 | link: makeRSocketLink({ 51 | rsocket: rsocketClient, 52 | route: "graphql", 53 | }), 54 | }); 55 | } 56 | 57 | function subcribe( 58 | client: ApolloClient, 59 | variables: Record, 60 | query: DocumentNode 61 | ) { 62 | return client.subscribe({ 63 | variables, 64 | query, 65 | }); 66 | } 67 | 68 | async function main() { 69 | // client setup 70 | const connector = makeRSocketConnector(); 71 | rsocketClient = await connector.connect(); 72 | 73 | const apolloClient = makeApolloClient({ rsocketClient }); 74 | 75 | const greeting = await apolloClient.query({ 76 | variables: {}, 77 | query: gql` 78 | query greeting { 79 | greeting 80 | } 81 | `, 82 | }); 83 | 84 | console.log(greeting); 85 | 86 | const echo = await apolloClient.query({ 87 | variables: { 88 | input: "Hello World", 89 | }, 90 | query: gql` 91 | query echo($input: String) { 92 | echo(input: $input) { 93 | message 94 | } 95 | } 96 | `, 97 | }); 98 | 99 | console.log(echo); 100 | 101 | await new Promise((resolve, reject) => { 102 | let subscription = subcribe( 103 | apolloClient, 104 | {}, 105 | gql` 106 | subscription greetings { 107 | greetings 108 | } 109 | ` 110 | ).subscribe({ 111 | next(data) { 112 | console.log("subscription event:", data); 113 | }, 114 | error(err) { 115 | console.log(`subscription error: ${err}`); 116 | }, 117 | complete() { 118 | resolve(null); 119 | }, 120 | }); 121 | }); 122 | } 123 | 124 | main() 125 | .catch((error: Error) => { 126 | console.error(error); 127 | exit(1); 128 | }) 129 | .finally(async () => { 130 | rsocketClient.close(); 131 | }); 132 | -------------------------------------------------------------------------------- /packages/rsocket-tcp-server/src/TcpServerTransport.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | Closeable, 19 | Deferred, 20 | Demultiplexer, 21 | DuplexConnection, 22 | Frame, 23 | FrameHandler, 24 | Multiplexer, 25 | Outbound, 26 | ServerTransport, 27 | } from "rsocket-core"; 28 | import net from "net"; 29 | import { TcpDuplexConnection } from "./TcpDuplexConnection"; 30 | 31 | type TcpServerCreator = (options: net.ServerOpts) => net.Server; 32 | 33 | type TcpServerOptionsSocketFactory = (options: net.ServerOpts) => net.Server; 34 | 35 | type TcpServerOptions = { 36 | serverOptions?: net.ServerOpts; 37 | listenOptions: net.ListenOptions; 38 | socketCreator?: TcpServerOptionsSocketFactory; 39 | }; 40 | 41 | export class TcpServerTransport implements ServerTransport { 42 | private readonly serverOptions: net.ServerOpts | undefined | null; 43 | private readonly listenOptions: net.ListenOptions; 44 | private readonly serverCreator: TcpServerCreator; 45 | 46 | constructor(options: TcpServerOptions) { 47 | this.serverOptions = options.serverOptions; 48 | this.listenOptions = options.listenOptions; 49 | this.serverCreator = 50 | options.socketCreator ?? ((options) => new net.Server(options)); 51 | } 52 | 53 | bind( 54 | connectionAcceptor: ( 55 | frame: Frame, 56 | connection: DuplexConnection 57 | ) => Promise, 58 | multiplexerDemultiplexerFactory: ( 59 | frame: Frame, 60 | outbound: Outbound & Closeable 61 | ) => Multiplexer & Demultiplexer & FrameHandler 62 | ): Promise { 63 | return new Promise((resolve, reject) => { 64 | const socketServer = this.serverCreator(this.serverOptions); 65 | 66 | const earlyCloseListener = (error?: Error) => { 67 | reject(error); 68 | }; 69 | 70 | socketServer.addListener("close", earlyCloseListener); 71 | socketServer.addListener("error", earlyCloseListener); 72 | socketServer.addListener("listening", () => { 73 | const serverCloseable = new ServerCloseable(socketServer); 74 | const connectionListener = (socket: net.Socket) => { 75 | TcpDuplexConnection.create( 76 | socket, 77 | connectionAcceptor, 78 | multiplexerDemultiplexerFactory 79 | ); 80 | }; 81 | const closeListener = (error?: Error) => { 82 | serverCloseable.close(error); 83 | }; 84 | 85 | socketServer.addListener("connection", connectionListener); 86 | socketServer.removeListener("close", earlyCloseListener); 87 | socketServer.removeListener("error", earlyCloseListener); 88 | 89 | socketServer.addListener("close", closeListener); 90 | socketServer.addListener("error", closeListener); 91 | 92 | resolve(serverCloseable); 93 | }); 94 | 95 | socketServer.listen(this.listenOptions); 96 | }); 97 | } 98 | } 99 | 100 | class ServerCloseable extends Deferred { 101 | constructor(private readonly server: net.Server) { 102 | super(); 103 | } 104 | 105 | close(error?: Error) { 106 | if (this.done) { 107 | super.close(error); 108 | return; 109 | } 110 | 111 | this.server.close(); 112 | super.close(); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /packages/rsocket-core/src/Resume.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { ErrorCodes, ExtFrame, RSocketError } from "."; 18 | import { sizeOfFrame } from "./Codecs"; 19 | import { 20 | CancelFrame, 21 | ErrorFrame, 22 | MetadataPushFrame, 23 | PayloadFrame, 24 | RequestChannelFrame, 25 | RequestFnfFrame, 26 | RequestNFrame, 27 | RequestResponseFrame, 28 | RequestStreamFrame, 29 | } from "./Frames"; 30 | 31 | export class FrameStore { 32 | private readonly storedFrames: Array< 33 | | CancelFrame 34 | | ErrorFrame 35 | | PayloadFrame 36 | | MetadataPushFrame 37 | | RequestChannelFrame 38 | | RequestFnfFrame 39 | | RequestNFrame 40 | | RequestResponseFrame 41 | | RequestStreamFrame 42 | | ExtFrame 43 | > = []; 44 | private _lastReceivedFramePosition: number = 0; 45 | private _firstAvailableFramePosition: number = 0; 46 | private _lastSentFramePosition: number = 0; 47 | 48 | get lastReceivedFramePosition(): number { 49 | return this._lastReceivedFramePosition; 50 | } 51 | 52 | get firstAvailableFramePosition(): number { 53 | return this._firstAvailableFramePosition; 54 | } 55 | 56 | get lastSentFramePosition(): number { 57 | return this._lastSentFramePosition; 58 | } 59 | 60 | store( 61 | frame: 62 | | CancelFrame 63 | | ErrorFrame 64 | | PayloadFrame 65 | | MetadataPushFrame 66 | | RequestChannelFrame 67 | | RequestFnfFrame 68 | | RequestNFrame 69 | | RequestResponseFrame 70 | | RequestStreamFrame 71 | | ExtFrame 72 | ): void { 73 | this._lastSentFramePosition += sizeOfFrame(frame); 74 | this.storedFrames.push(frame); 75 | } 76 | 77 | record( 78 | frame: 79 | | CancelFrame 80 | | ErrorFrame 81 | | PayloadFrame 82 | | MetadataPushFrame 83 | | RequestChannelFrame 84 | | RequestFnfFrame 85 | | RequestNFrame 86 | | RequestResponseFrame 87 | | RequestStreamFrame 88 | | ExtFrame 89 | ): void { 90 | this._lastReceivedFramePosition += sizeOfFrame(frame); 91 | } 92 | 93 | dropTo(lastReceivedPosition: number): void { 94 | let bytesToDrop = lastReceivedPosition - this._firstAvailableFramePosition; 95 | while (bytesToDrop > 0 && this.storedFrames.length > 0) { 96 | const storedFrame = this.storedFrames.shift(); 97 | bytesToDrop -= sizeOfFrame(storedFrame); 98 | } 99 | 100 | if (bytesToDrop !== 0) { 101 | throw new RSocketError( 102 | ErrorCodes.CONNECTION_ERROR, 103 | `State inconsistency. Expected bytes to drop ${ 104 | lastReceivedPosition - this._firstAvailableFramePosition 105 | } but actual ${bytesToDrop}` 106 | ); 107 | } 108 | 109 | this._firstAvailableFramePosition = lastReceivedPosition; 110 | } 111 | 112 | drain( 113 | consumer: ( 114 | frame: 115 | | CancelFrame 116 | | ErrorFrame 117 | | PayloadFrame 118 | | MetadataPushFrame 119 | | RequestChannelFrame 120 | | RequestFnfFrame 121 | | RequestNFrame 122 | | RequestResponseFrame 123 | | RequestStreamFrame 124 | | ExtFrame 125 | ) => void 126 | ) { 127 | for (const frame of this.storedFrames) { 128 | consumer(frame); 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /packages/rsocket-websocket-server/src/WebsocketServerTransport.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | Closeable, 19 | Deferred, 20 | Demultiplexer, 21 | DuplexConnection, 22 | Frame, 23 | FrameHandler, 24 | Multiplexer, 25 | Outbound, 26 | ServerTransport, 27 | } from "rsocket-core"; 28 | import WebSocket, { Server } from "ws"; 29 | import { WebsocketDuplexConnection } from "./WebsocketDuplexConnection"; 30 | 31 | export type SocketFactory = (options: SocketOptions) => Server; 32 | 33 | export type SocketOptions = { 34 | host?: string; 35 | port?: number; 36 | }; 37 | 38 | export type ServerOptions = SocketOptions & { 39 | wsCreator?: SocketFactory; 40 | debug?: boolean; 41 | }; 42 | 43 | const defaultFactory: SocketFactory = (options: SocketOptions) => { 44 | return new Server({ 45 | host: options.host, 46 | port: options.port, 47 | }); 48 | }; 49 | 50 | export class WebsocketServerTransport implements ServerTransport { 51 | private readonly host: string; 52 | private readonly port: number; 53 | private readonly factory: SocketFactory; 54 | 55 | constructor(options: ServerOptions) { 56 | this.host = options.host; 57 | this.port = options.port; 58 | this.factory = options.wsCreator ?? defaultFactory; 59 | } 60 | 61 | async bind( 62 | connectionAcceptor: ( 63 | frame: Frame, 64 | connection: DuplexConnection 65 | ) => Promise, 66 | multiplexerDemultiplexerFactory: ( 67 | frame: Frame, 68 | outbound: Outbound & Closeable 69 | ) => Multiplexer & Demultiplexer & FrameHandler 70 | ): Promise { 71 | const websocketServer: Server = await this.connectServer(); 72 | const serverCloseable = new ServerCloseable(websocketServer); 73 | 74 | const connectionListener = (websocket: WebSocket) => { 75 | websocket.binaryType = "nodebuffer"; 76 | const duplex = WebSocket.createWebSocketStream(websocket); 77 | WebsocketDuplexConnection.create( 78 | duplex, 79 | connectionAcceptor, 80 | multiplexerDemultiplexerFactory 81 | ); 82 | }; 83 | 84 | const closeListener = (error?: Error) => { 85 | serverCloseable.close(error); 86 | }; 87 | 88 | websocketServer.addListener("connection", connectionListener); 89 | websocketServer.addListener("close", closeListener); 90 | websocketServer.addListener("error", closeListener); 91 | 92 | return serverCloseable; 93 | } 94 | 95 | private connectServer(): Promise { 96 | return new Promise((resolve, reject) => { 97 | const websocketServer = this.factory({ 98 | host: this.host, 99 | port: this.port, 100 | }); 101 | 102 | const earlyCloseListener = (error?: Error) => { 103 | reject(error); 104 | }; 105 | 106 | websocketServer.addListener("close", earlyCloseListener); 107 | websocketServer.addListener("error", earlyCloseListener); 108 | websocketServer.addListener("listening", () => resolve(websocketServer)); 109 | }); 110 | } 111 | } 112 | 113 | class ServerCloseable extends Deferred { 114 | constructor(private readonly server: Server) { 115 | super(); 116 | } 117 | 118 | close(error?: Error) { 119 | if (this.done) { 120 | super.close(error); 121 | return; 122 | } 123 | 124 | this.server.close(); 125 | super.close(); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /packages/rsocket-core/src/RSocket.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Closeable } from "./Common"; 18 | 19 | /** 20 | * A single unit of data exchanged between the peers of a `RSocket`. 21 | */ 22 | export type Payload = { 23 | data: Buffer | null | undefined; 24 | metadata?: Buffer; 25 | }; 26 | 27 | export type SetupPayload = { 28 | metadataMimeType: string; 29 | dataMimeType: string; 30 | keepAliveInterval: number; 31 | keepAliveMaxLifetime: number; 32 | flags: number; 33 | resumeToken: Buffer | null | undefined; 34 | data: Buffer | null | undefined; 35 | metadata?: Buffer; 36 | }; 37 | 38 | export interface Cancellable { 39 | cancel(): void; 40 | } 41 | 42 | export interface Requestable { 43 | request(requestN: number): void; 44 | } 45 | 46 | export interface OnExtensionSubscriber { 47 | onExtension( 48 | extendedType: number, 49 | content: Buffer | null | undefined, 50 | canBeIgnored: boolean 51 | ): void; 52 | } 53 | 54 | export interface OnNextSubscriber { 55 | onNext(payload: Payload, isComplete: boolean): void; 56 | } 57 | 58 | export interface OnTerminalSubscriber { 59 | onError(error: Error): void; 60 | onComplete(): void; 61 | } 62 | 63 | export interface SocketAcceptor { 64 | accept(payload: SetupPayload, remotePeer: RSocket): Promise>; 65 | } 66 | 67 | /** 68 | * A contract providing different interaction models per the [ReactiveSocket protocol] 69 | (https://github.com/ReactiveSocket/reactivesocket/blob/master/Protocol.md). 70 | */ 71 | export interface RSocket extends Closeable { 72 | /** 73 | * Fire and Forget interaction model of `ReactiveSocket`. The returned 74 | * Publisher resolves when the passed `payload` is successfully handled. 75 | */ 76 | fireAndForget( 77 | payload: Payload, 78 | responderStream: OnTerminalSubscriber 79 | ): Cancellable; 80 | 81 | /** 82 | * Request-Response interaction model of `ReactiveSocket`. The returned 83 | * Publisher resolves with the response. 84 | */ 85 | requestResponse( 86 | payload: Payload, 87 | responderStream: OnTerminalSubscriber & 88 | OnNextSubscriber & 89 | OnExtensionSubscriber 90 | ): Cancellable & OnExtensionSubscriber; 91 | 92 | /** 93 | * Request-Stream interaction model of `ReactiveSocket`. The returned 94 | * Publisher returns values representing the response(s). 95 | */ 96 | requestStream( 97 | payload: Payload, 98 | initialRequestN: number, 99 | responderStream: OnTerminalSubscriber & 100 | OnNextSubscriber & 101 | OnExtensionSubscriber 102 | ): Requestable & Cancellable & OnExtensionSubscriber; 103 | 104 | /** 105 | * Request-Channel interaction model of `ReactiveSocket`. The returned 106 | * Publisher returns values representing the response(boolean) 107 | */ 108 | requestChannel( 109 | payload: Payload, 110 | initialRequestN: number, 111 | isCompleted: boolean, 112 | responderStream: OnTerminalSubscriber & 113 | OnNextSubscriber & 114 | OnExtensionSubscriber & 115 | Requestable & 116 | Cancellable 117 | ): OnTerminalSubscriber & 118 | OnNextSubscriber & 119 | OnExtensionSubscriber & 120 | Requestable & 121 | Cancellable; 122 | 123 | /** 124 | * Metadata-Push interaction model of `ReactiveSocket`. The returned Publisher 125 | * resolves when the passed `payload` is successfully handled. 126 | */ 127 | metadataPush(metadata: Buffer, responderStream: OnTerminalSubscriber): void; 128 | } 129 | -------------------------------------------------------------------------------- /packages/rsocket-core/src/Transport.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Availability, Closeable } from "./Common"; 18 | import { 19 | CancelFrame, 20 | ErrorFrame, 21 | ExtFrame, 22 | Frame, 23 | FrameTypes, 24 | KeepAliveFrame, 25 | LeaseFrame, 26 | MetadataPushFrame, 27 | PayloadFrame, 28 | RequestChannelFrame, 29 | RequestFnfFrame, 30 | RequestNFrame, 31 | RequestResponseFrame, 32 | RequestStreamFrame, 33 | ResumeFrame, 34 | ResumeOkFrame, 35 | SetupFrame, 36 | } from "./Frames"; 37 | 38 | export interface Outbound { 39 | /** 40 | * Send a single frame on the connection. 41 | */ 42 | send(s: Frame): void; 43 | } 44 | 45 | export interface Stream extends Outbound { 46 | connect(handler: StreamFrameHandler): void; 47 | 48 | disconnect(handler: StreamFrameHandler): void; 49 | 50 | send( 51 | frame: 52 | | CancelFrame 53 | | ErrorFrame 54 | | PayloadFrame 55 | | RequestChannelFrame 56 | | RequestFnfFrame 57 | | RequestNFrame 58 | | RequestResponseFrame 59 | | RequestStreamFrame 60 | | ExtFrame 61 | ): void; 62 | } 63 | 64 | export interface FrameHandler { 65 | handle(frame: Frame): void; 66 | close(error?: Error): void; 67 | } 68 | 69 | export interface ConnectionFrameHandler extends FrameHandler { 70 | handle( 71 | frame: 72 | | SetupFrame 73 | | ResumeFrame 74 | | ResumeOkFrame 75 | | LeaseFrame 76 | | KeepAliveFrame 77 | | ErrorFrame 78 | | MetadataPushFrame 79 | ): void; 80 | 81 | pause(): void; 82 | resume(): void; 83 | } 84 | 85 | export interface StreamRequestHandler extends FrameHandler { 86 | handle( 87 | frame: 88 | | RequestFnfFrame 89 | | RequestResponseFrame 90 | | RequestStreamFrame 91 | | RequestChannelFrame, 92 | stream?: Stream 93 | ): void; 94 | } 95 | 96 | export interface StreamLifecycleHandler { 97 | handleReady(streamId: number, stream: Outbound & Stream): boolean; 98 | 99 | handleReject(error: Error): void; 100 | } 101 | 102 | export interface StreamFrameHandler extends FrameHandler { 103 | readonly streamType: 104 | | FrameTypes.REQUEST_CHANNEL 105 | | FrameTypes.REQUEST_FNF 106 | | FrameTypes.REQUEST_RESPONSE 107 | | FrameTypes.REQUEST_STREAM; 108 | readonly streamId: number; 109 | 110 | handle( 111 | frame: PayloadFrame | ErrorFrame | CancelFrame | RequestNFrame | ExtFrame 112 | ): void; 113 | } 114 | 115 | export interface Multiplexer { 116 | readonly connectionOutbound: Outbound; 117 | 118 | createRequestStream( 119 | streamHandler: StreamFrameHandler & StreamLifecycleHandler 120 | ): void; 121 | } 122 | 123 | export interface Demultiplexer { 124 | connectionInbound(handler: ConnectionFrameHandler): void; 125 | 126 | handleRequestStream(handler: StreamRequestHandler): void; 127 | } 128 | 129 | /** 130 | * Represents a network connection with input/output used by a ReactiveSocket to 131 | * send/receive data. 132 | */ 133 | export interface DuplexConnection extends Closeable, Availability { 134 | readonly multiplexerDemultiplexer: Multiplexer & Demultiplexer; 135 | } 136 | 137 | export interface ClientTransport { 138 | connect( 139 | multiplexerDemultiplexerFactory: ( 140 | outbound: Outbound & Closeable 141 | ) => Multiplexer & Demultiplexer & FrameHandler 142 | ): Promise; 143 | } 144 | 145 | export interface ServerTransport { 146 | bind( 147 | connectionAcceptor: ( 148 | frame: Frame, 149 | connection: DuplexConnection 150 | ) => Promise, 151 | multiplexerDemultiplexerFactory: ( 152 | frame: Frame, 153 | outbound: Outbound & Closeable 154 | ) => Multiplexer & Demultiplexer & FrameHandler 155 | ): Promise; 156 | } 157 | -------------------------------------------------------------------------------- /packages/rsocket-composite-metadata/src/WellKnownAuthType.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export class WellKnownAuthType { 18 | constructor(readonly string: string, readonly identifier: number) {} 19 | 20 | /** 21 | * Find the {@link WellKnownAuthType} for the given identifier (as an {@link number}). Valid 22 | * identifiers are defined to be integers between 0 and 127, inclusive. Identifiers outside of 23 | * this range will produce the {@link #UNPARSEABLE_AUTH_TYPE}. Additionally, some identifiers in 24 | * that range are still only reserved and don't have a type associated yet: this method returns 25 | * the {@link #UNKNOWN_RESERVED_AUTH_TYPE} when passing such an identifier, which lets call sites 26 | * potentially detect this and keep the original representation when transmitting the associated 27 | * metadata buffer. 28 | * 29 | * @param id the looked up identifier 30 | * @return the {@link WellKnownAuthType}, or {@link #UNKNOWN_RESERVED_AUTH_TYPE} if the id is out 31 | * of the specification's range, or {@link #UNKNOWN_RESERVED_AUTH_TYPE} if the id is one that 32 | * is merely reserved but unknown to this implementation. 33 | */ 34 | static fromIdentifier(id: number): WellKnownAuthType { 35 | if (id < 0x00 || id > 0x7f) { 36 | return WellKnownAuthType.UNPARSEABLE_AUTH_TYPE; 37 | } 38 | return WellKnownAuthType.TYPES_BY_AUTH_ID[id]; 39 | } 40 | 41 | /** 42 | * Find the {@link WellKnownAuthType} for the given {@link String} representation. If the 43 | * representation is {@code null} or doesn't match a {@link WellKnownAuthType}, the {@link 44 | * #UNPARSEABLE_AUTH_TYPE} is returned. 45 | * 46 | * @param authTypeString the looked up mime type 47 | * @return the matching {@link WellKnownAuthType}, or {@link #UNPARSEABLE_AUTH_TYPE} if none 48 | * matches 49 | */ 50 | static fromString(authTypeString: string): WellKnownAuthType { 51 | if (!authTypeString) { 52 | throw new Error("type must be non-null"); 53 | } 54 | 55 | // force UNPARSEABLE if by chance UNKNOWN_RESERVED_MIME_TYPE's text has been used 56 | if ( 57 | authTypeString === WellKnownAuthType.UNKNOWN_RESERVED_AUTH_TYPE.string 58 | ) { 59 | return WellKnownAuthType.UNPARSEABLE_AUTH_TYPE; 60 | } 61 | 62 | return ( 63 | WellKnownAuthType.TYPES_BY_AUTH_STRING.get(authTypeString) || 64 | WellKnownAuthType.UNPARSEABLE_AUTH_TYPE 65 | ); 66 | } 67 | 68 | /** @see #string() */ 69 | toString(): string { 70 | return this.string; 71 | } 72 | } 73 | 74 | export namespace WellKnownAuthType { 75 | export const UNPARSEABLE_AUTH_TYPE: WellKnownAuthType = new WellKnownAuthType( 76 | "UNPARSEABLE_AUTH_TYPE_DO_NOT_USE", 77 | -2 78 | ); 79 | export const UNKNOWN_RESERVED_AUTH_TYPE: WellKnownAuthType = 80 | new WellKnownAuthType("UNKNOWN_YET_RESERVED_DO_NOT_USE", -1); 81 | 82 | export const SIMPLE: WellKnownAuthType = new WellKnownAuthType( 83 | "simple", 84 | 0x00 85 | ); 86 | export const BEARER: WellKnownAuthType = new WellKnownAuthType( 87 | "bearer", 88 | 0x01 89 | ); 90 | 91 | export const TYPES_BY_AUTH_ID: WellKnownAuthType[] = new Array(128); 92 | export const TYPES_BY_AUTH_STRING: Map = new Map(); 93 | 94 | const ALL_MIME_TYPES: WellKnownAuthType[] = [ 95 | UNPARSEABLE_AUTH_TYPE, 96 | UNKNOWN_RESERVED_AUTH_TYPE, 97 | SIMPLE, 98 | BEARER, 99 | ]; 100 | 101 | TYPES_BY_AUTH_ID.fill(UNKNOWN_RESERVED_AUTH_TYPE); 102 | 103 | for (const value of ALL_MIME_TYPES) { 104 | if (value.identifier >= 0) { 105 | TYPES_BY_AUTH_ID[value.identifier] = value; 106 | TYPES_BY_AUTH_STRING.set(value.string, value); 107 | } 108 | } 109 | 110 | if (Object.seal) { 111 | Object.seal(TYPES_BY_AUTH_ID); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /packages/rsocket-examples/src/ClientCompositeMetadataRouteExample.ts: -------------------------------------------------------------------------------- 1 | import { RSocket, RSocketConnector } from "@rsocket/core"; 2 | import { TcpClientTransport } from "@rsocket/transport-tcp-client"; 3 | import { 4 | encodeCompositeMetadata, 5 | encodeRoute, 6 | WellKnownMimeType, 7 | } from "@rsocket/composite-metadata"; 8 | import Logger from "./shared/logger"; 9 | import { exit } from "process"; 10 | import MESSAGE_RSOCKET_ROUTING = WellKnownMimeType.MESSAGE_RSOCKET_ROUTING; 11 | import MESSAGE_RSOCKET_COMPOSITE_METADATA = WellKnownMimeType.MESSAGE_RSOCKET_COMPOSITE_METADATA; 12 | 13 | /** 14 | * This example assumes you have a RSocket server running on 127.0.0.1:9000 that will respond 15 | * to requests at the following routes: 16 | * - login (requestResponse) 17 | * - message (requestResponse) 18 | * - messages.incoming (requestStream) 19 | */ 20 | 21 | function makeConnector() { 22 | const connectorConnectionOptions = { 23 | host: "127.0.0.1", 24 | port: 9000, 25 | }; 26 | console.log( 27 | `Creating connector to ${JSON.stringify(connectorConnectionOptions)}` 28 | ); 29 | return new RSocketConnector({ 30 | setup: { 31 | metadataMimeType: MESSAGE_RSOCKET_COMPOSITE_METADATA.string, 32 | }, 33 | transport: new TcpClientTransport({ 34 | connectionOptions: connectorConnectionOptions, 35 | }), 36 | }); 37 | } 38 | 39 | function createRoute(route?: string) { 40 | let compositeMetaData = undefined; 41 | if (route) { 42 | const encodedRoute = encodeRoute(route); 43 | 44 | const map = new Map(); 45 | map.set(MESSAGE_RSOCKET_ROUTING, encodedRoute); 46 | compositeMetaData = encodeCompositeMetadata(map); 47 | } 48 | return compositeMetaData; 49 | } 50 | 51 | async function requestResponse(rsocket: RSocket, route: string, data: string) { 52 | console.log(`Executing requestResponse: ${JSON.stringify({ route, data })}`); 53 | return new Promise((resolve, reject) => { 54 | return rsocket.requestResponse( 55 | { 56 | data: Buffer.from(data), 57 | metadata: createRoute(route), 58 | }, 59 | { 60 | onError: (e) => { 61 | reject(e); 62 | }, 63 | onNext: (payload, isComplete) => { 64 | Logger.info( 65 | `requestResponse onNext payload[data: ${payload.data}; metadata: ${payload.metadata}]|${isComplete}` 66 | ); 67 | resolve(payload); 68 | }, 69 | onComplete: () => { 70 | Logger.info(`requestResponse onComplete`); 71 | resolve(null); 72 | }, 73 | onExtension: () => {}, 74 | } 75 | ); 76 | }); 77 | } 78 | 79 | async function main() { 80 | const connector = makeConnector(); 81 | const rsocket = await connector.connect(); 82 | 83 | await requestResponse(rsocket, "login", "user1"); 84 | 85 | await requestResponse( 86 | rsocket, 87 | "message", 88 | JSON.stringify({ user: "user1", content: "a message" }) 89 | ); 90 | 91 | await new Promise((resolve, reject) => { 92 | let payloadsReceived = 0; 93 | const maxPayloads = 10; 94 | const requester = rsocket.requestStream( 95 | { 96 | data: Buffer.from("Hello World"), 97 | metadata: createRoute("messages.incoming"), 98 | }, 99 | 3, 100 | { 101 | onError: (e) => reject(e), 102 | onNext: (payload, isComplete) => { 103 | Logger.info( 104 | `[client] payload[data: ${payload.data}; metadata: ${payload.metadata}]|isComplete: ${isComplete}` 105 | ); 106 | 107 | payloadsReceived++; 108 | 109 | // request 5 more payloads every 5th payload, until a max total payloads received 110 | if (payloadsReceived % 2 == 0 && payloadsReceived < maxPayloads) { 111 | requester.request(2); 112 | } else if (payloadsReceived >= maxPayloads) { 113 | requester.cancel(); 114 | setTimeout(() => { 115 | resolve(null); 116 | }); 117 | } 118 | 119 | if (isComplete) { 120 | resolve(null); 121 | } 122 | }, 123 | onComplete: () => { 124 | Logger.info(`requestStream onComplete`); 125 | resolve(null); 126 | }, 127 | onExtension: () => {}, 128 | } 129 | ); 130 | }); 131 | } 132 | 133 | main() 134 | .then(() => exit()) 135 | .catch((error: Error) => { 136 | console.error(error); 137 | exit(1); 138 | }); 139 | -------------------------------------------------------------------------------- /packages/rsocket-tcp-client/src/TcpDuplexConnection.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | Closeable, 19 | Deferred, 20 | Demultiplexer, 21 | Deserializer, 22 | DuplexConnection, 23 | Frame, 24 | FrameHandler, 25 | Multiplexer, 26 | Outbound, 27 | serializeFrameWithLength, 28 | } from "rsocket-core"; 29 | import net from "net"; 30 | 31 | export class TcpDuplexConnection 32 | extends Deferred 33 | implements DuplexConnection, Outbound 34 | { 35 | private error: Error; 36 | private remainingBuffer: Buffer = Buffer.allocUnsafe(0); 37 | 38 | readonly multiplexerDemultiplexer: Multiplexer & Demultiplexer & FrameHandler; 39 | 40 | constructor( 41 | private socket: net.Socket, 42 | // dependency injected to facilitate testing 43 | private readonly deserializer: Deserializer, 44 | multiplexerDemultiplexerFactory: ( 45 | outbound: Outbound & Closeable 46 | ) => Multiplexer & Demultiplexer & FrameHandler 47 | ) { 48 | super(); 49 | 50 | /** 51 | * Emitted when an error occurs. The 'close' event will be called directly following this event. 52 | */ 53 | socket.on("error", this.handleError); 54 | 55 | /** 56 | * Emitted once the socket is fully closed. The argument hadError is a boolean which says 57 | * if the socket was closed due to a transmission error. 58 | */ 59 | socket.on("close", this.handleClosed); 60 | 61 | /** 62 | * Emitted when data is received. The argument data will be a Buffer or String. Encoding of data is set by 63 | * socket.setEncoding(). The data will be lost if there is no listener when a Socket emits a 'data' event. 64 | */ 65 | socket.on("data", this.handleData); 66 | 67 | this.multiplexerDemultiplexer = multiplexerDemultiplexerFactory(this); 68 | } 69 | 70 | get availability(): number { 71 | return this.done ? 0 : 1; 72 | } 73 | 74 | close(error?: Error) { 75 | if (this.done) { 76 | return; 77 | } 78 | 79 | this.socket.off("error", this.handleError); 80 | this.socket.off("close", this.handleClosed); 81 | this.socket.off("data", this.handleData); 82 | 83 | this.socket.end(); 84 | 85 | delete this.socket; 86 | 87 | super.close(error); 88 | } 89 | 90 | send(frame: Frame): void { 91 | if (this.done) { 92 | return; 93 | } 94 | 95 | const buffer = serializeFrameWithLength(frame); 96 | 97 | this.socket.write(buffer); 98 | } 99 | 100 | /** 101 | * Handles close event from the underlying socket. 102 | * @param hadError 103 | * @private 104 | */ 105 | private handleClosed = (hadError: boolean): void => { 106 | const message = hadError 107 | ? `TcpDuplexConnection: ${this.error.message}` 108 | : "TcpDuplexConnection: Socket closed unexpectedly."; 109 | this.close(new Error(message)); 110 | }; 111 | 112 | /** 113 | * Handles error events from the underlying socket. `handleClosed` is expected to be called 114 | * immediately following `handleError`. 115 | * @param error 116 | * @private 117 | */ 118 | private handleError = (error: Error): void => { 119 | this.error = error; 120 | }; 121 | 122 | private handleData = (chunks: Buffer): void => { 123 | try { 124 | // Combine partial frame data from previous chunks with the next chunk, 125 | // then extract any complete frames plus any remaining data. 126 | const buffer = Buffer.concat([this.remainingBuffer, chunks]); 127 | let lastOffset = 0; 128 | const frames = this.deserializer.deserializeFrames(buffer); 129 | for (const [frame, offset] of frames) { 130 | lastOffset = offset; 131 | this.multiplexerDemultiplexer.handle(frame); 132 | } 133 | this.remainingBuffer = buffer.slice(lastOffset, buffer.length); 134 | } catch (error) { 135 | this.close(error); 136 | } 137 | }; 138 | } 139 | -------------------------------------------------------------------------------- /packages/rsocket-graphql-apollo-link/src/RSocketSubscriptionLink.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | "use strict"; 18 | 19 | import { 20 | ApolloError, 21 | ApolloLink, 22 | FetchResult, 23 | Observable, 24 | Observer, 25 | Operation, 26 | } from "@apollo/client/core"; 27 | import { MAX_REQUEST_COUNT, Payload, RSocket } from "rsocket-core"; 28 | import { ExecutionResult, print } from "graphql"; 29 | import { 30 | encodeCompositeMetadata, 31 | encodeRoutes, 32 | WellKnownMimeType, 33 | } from "rsocket-composite-metadata"; 34 | 35 | type SubscribeOperation = { 36 | query: String; 37 | variables: Record; 38 | operationName: string; 39 | extensions: Record; 40 | }; 41 | 42 | type SubscriptionLinkOptions = { 43 | /** 44 | * The route that the RSocket server is listening for GraphQL messages on. 45 | */ 46 | route?: string; 47 | }; 48 | 49 | class SubscriptionClient { 50 | constructor( 51 | public readonly client: RSocket, 52 | private readonly options: SubscriptionLinkOptions 53 | ) {} 54 | 55 | subscribe, Extensions = unknown>( 56 | operation: SubscribeOperation, 57 | observer: Observer> 58 | ): () => void { 59 | const metadata = new Map(); 60 | metadata.set( 61 | WellKnownMimeType.MESSAGE_RSOCKET_MIMETYPE, 62 | Buffer.from(WellKnownMimeType.APPLICATION_JSON.toString()) 63 | ); 64 | if (this.options?.route) { 65 | metadata.set( 66 | WellKnownMimeType.MESSAGE_RSOCKET_ROUTING, 67 | encodeRoutes(this.options.route) 68 | ); 69 | } 70 | 71 | const encodedMetadata = encodeCompositeMetadata(metadata); 72 | 73 | let requestStream = this.client.requestStream( 74 | { 75 | data: Buffer.from(JSON.stringify(operation)), 76 | metadata: encodedMetadata, 77 | }, 78 | MAX_REQUEST_COUNT, 79 | { 80 | onComplete(): void { 81 | observer.complete(); 82 | }, 83 | onError(error: Error): void { 84 | observer.error(error); 85 | }, 86 | onExtension(): void {}, 87 | onNext(payload: Payload, isComplete: boolean): void { 88 | const { data } = payload; 89 | const decoded = data.toString(); 90 | const deserialized = JSON.parse(decoded); 91 | observer.next(deserialized); 92 | if (isComplete) { 93 | observer.complete(); 94 | } 95 | }, 96 | } 97 | ); 98 | 99 | return () => { 100 | requestStream.cancel(); 101 | }; 102 | } 103 | } 104 | 105 | export class RSocketSubscriptionLink extends ApolloLink { 106 | private client: SubscriptionClient; 107 | constructor( 108 | client: RSocket, 109 | private readonly options: SubscriptionLinkOptions 110 | ) { 111 | super(); 112 | this.client = new SubscriptionClient(client, options); 113 | } 114 | 115 | public request(operation: Operation): Observable | null { 116 | return new Observable((observer) => { 117 | const serializedQuery = print(operation.query); 118 | return this.client.subscribe( 119 | { 120 | ...operation, 121 | query: serializedQuery, 122 | }, 123 | { 124 | next(value: ExecutionResult) { 125 | observer.next(value); 126 | }, 127 | complete() { 128 | observer.complete(); 129 | }, 130 | error(err: any) { 131 | if (err instanceof Error) { 132 | return observer.error(err); 133 | } 134 | 135 | return observer.error( 136 | new ApolloError({ 137 | graphQLErrors: Array.isArray(err) ? err : [err], 138 | }) 139 | ); 140 | }, 141 | } 142 | ); 143 | }); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /packages/rsocket-tcp-server/src/TcpDuplexConnection.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | Closeable, 19 | Deferred, 20 | Demultiplexer, 21 | deserializeFrames, 22 | DuplexConnection, 23 | Frame, 24 | FrameHandler, 25 | Multiplexer, 26 | Outbound, 27 | serializeFrameWithLength, 28 | } from "rsocket-core"; 29 | import net from "net"; 30 | 31 | export class TcpDuplexConnection 32 | extends Deferred 33 | implements DuplexConnection, Outbound 34 | { 35 | private error: Error; 36 | private remainingBuffer: Buffer = Buffer.allocUnsafe(0); 37 | 38 | readonly multiplexerDemultiplexer: Multiplexer & Demultiplexer & FrameHandler; 39 | 40 | constructor( 41 | private socket: net.Socket, 42 | frame: Frame, 43 | multiplexerDemultiplexerFactory: ( 44 | frame: Frame, 45 | outbound: Outbound & Closeable 46 | ) => Multiplexer & Demultiplexer & FrameHandler 47 | ) { 48 | super(); 49 | 50 | socket.on("close", this.handleClosed); 51 | socket.on("error", this.handleError); 52 | socket.on("data", this.handleData); 53 | 54 | this.multiplexerDemultiplexer = multiplexerDemultiplexerFactory( 55 | frame, 56 | this 57 | ); 58 | } 59 | 60 | get availability(): number { 61 | return this.done ? 0 : 1; 62 | } 63 | 64 | close(error?: Error) { 65 | if (this.done) { 66 | super.close(error); 67 | return; 68 | } 69 | 70 | this.socket.off("close", this.handleClosed); 71 | this.socket.off("error", this.handleError); 72 | this.socket.off("data", this.handleData); 73 | 74 | this.socket.end(); 75 | 76 | delete this.socket; 77 | 78 | super.close(error); 79 | } 80 | 81 | send(frame: Frame): void { 82 | if (this.done) { 83 | return; 84 | } 85 | 86 | const buffer = serializeFrameWithLength(frame); 87 | 88 | this.socket.write(buffer); 89 | } 90 | 91 | private handleClosed = (hadError: boolean): void => { 92 | const message = hadError 93 | ? `TcpDuplexConnection: ${this.error.message}` 94 | : "TcpDuplexConnection: Socket closed unexpectedly."; 95 | this.close(new Error(message)); 96 | }; 97 | 98 | private handleError = (error: Error): void => { 99 | this.error = error; 100 | this.close(error); 101 | }; 102 | 103 | private handleData = (chunks: Buffer): void => { 104 | try { 105 | // Combine partial frame data from previous chunks with the next chunk, 106 | // then extract any complete frames plus any remaining data. 107 | const buffer = Buffer.concat([this.remainingBuffer, chunks]); 108 | let lastOffset = 0; 109 | for (const [frame, offset] of deserializeFrames(buffer)) { 110 | lastOffset = offset; 111 | this.multiplexerDemultiplexer.handle(frame); 112 | } 113 | this.remainingBuffer = buffer.slice(lastOffset, buffer.length); 114 | } catch (error) { 115 | this.close(error); 116 | } 117 | }; 118 | 119 | static create( 120 | socket: net.Socket, 121 | connectionAcceptor: ( 122 | frame: Frame, 123 | connection: DuplexConnection 124 | ) => Promise, 125 | multiplexerDemultiplexerFactory: ( 126 | frame: Frame, 127 | outbound: Outbound & Closeable 128 | ) => Multiplexer & Demultiplexer & FrameHandler 129 | ): void { 130 | // TODO: timeout on no data? 131 | socket.once("data", async (buffer) => { 132 | const [frame, offset] = deserializeFrames(buffer).next().value; 133 | const connection = new TcpDuplexConnection( 134 | socket, 135 | frame, 136 | multiplexerDemultiplexerFactory 137 | ); 138 | if (connection.done) { 139 | return; 140 | } 141 | try { 142 | socket.pause(); 143 | await connectionAcceptor(frame, connection); 144 | socket.resume(); 145 | if (offset < buffer.length) { 146 | connection.handleData(buffer.slice(offset, buffer.length)); 147 | } 148 | } catch (error) { 149 | connection.close(error); 150 | } 151 | }); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /packages/rsocket-websocket-server/src/WebsocketDuplexConnection.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | Closeable, 19 | Deferred, 20 | Demultiplexer, 21 | deserializeFrame, 22 | DuplexConnection, 23 | Frame, 24 | FrameHandler, 25 | Multiplexer, 26 | Outbound, 27 | serializeFrame, 28 | } from "rsocket-core"; 29 | import { Duplex } from "stream"; 30 | 31 | export class WebsocketDuplexConnection 32 | extends Deferred 33 | implements DuplexConnection, Outbound 34 | { 35 | readonly multiplexerDemultiplexer: Multiplexer & Demultiplexer & FrameHandler; 36 | 37 | constructor( 38 | private websocketDuplex: Duplex, 39 | frame: Frame, 40 | multiplexerDemultiplexerFactory: ( 41 | frame: Frame, 42 | outbound: Outbound & Closeable 43 | ) => Multiplexer & Demultiplexer & FrameHandler 44 | ) { 45 | super(); 46 | 47 | websocketDuplex.on("close", this.handleClosed); 48 | websocketDuplex.on("error", this.handleError); 49 | websocketDuplex.on("data", this.handleMessage); 50 | 51 | this.multiplexerDemultiplexer = multiplexerDemultiplexerFactory( 52 | frame, 53 | this 54 | ); 55 | } 56 | 57 | get availability(): number { 58 | return this.websocketDuplex.destroyed ? 0 : 1; 59 | } 60 | 61 | close(error?: Error) { 62 | if (this.done) { 63 | super.close(error); 64 | return; 65 | } 66 | 67 | this.websocketDuplex.removeAllListeners(); 68 | this.websocketDuplex.end(); 69 | 70 | delete this.websocketDuplex; 71 | 72 | super.close(error); 73 | } 74 | 75 | send(frame: Frame): void { 76 | if (this.done) { 77 | return; 78 | } 79 | 80 | // if (__DEV__) { 81 | // if (this._options.debug) { 82 | // console.log(printFrame(frame)); 83 | // } 84 | // } 85 | const buffer = 86 | /* this._options.lengthPrefixedFrames 87 | ? serializeFrameWithLength(frame, this._encoders) 88 | :*/ serializeFrame(frame); 89 | // if (!this._socket) { 90 | // throw new Error( 91 | // "RSocketWebSocketClient: Cannot send frame, not connected." 92 | // ); 93 | // } 94 | this.websocketDuplex.write(buffer); 95 | } 96 | 97 | private handleClosed = (e: CloseEvent): void => { 98 | this.close( 99 | new Error( 100 | e.reason || "WebsocketDuplexConnection: Socket closed unexpectedly." 101 | ) 102 | ); 103 | }; 104 | 105 | private handleError = (e: ErrorEvent): void => { 106 | this.close(e.error); 107 | }; 108 | 109 | private handleMessage = (buffer: Buffer): void => { 110 | try { 111 | const frame = 112 | /* this._options.lengthPrefixedFrames 113 | ? deserializeFrameWithLength(buffer, this._encoders) 114 | : */ deserializeFrame(buffer); 115 | // if (__DEV__) { 116 | // if (this._options.debug) { 117 | // console.log(printFrame(frame)); 118 | // } 119 | // } 120 | this.multiplexerDemultiplexer.handle(frame); 121 | } catch (error) { 122 | this.close(error); 123 | } 124 | }; 125 | 126 | static create( 127 | socket: Duplex, 128 | connectionAcceptor: ( 129 | frame: Frame, 130 | connection: DuplexConnection 131 | ) => Promise, 132 | multiplexerDemultiplexerFactory: ( 133 | frame: Frame, 134 | outbound: Outbound & Closeable 135 | ) => Multiplexer & Demultiplexer & FrameHandler 136 | ): void { 137 | // TODO: timeout on no data? 138 | socket.once("data", async (buffer) => { 139 | const frame = deserializeFrame(buffer); 140 | const connection = new WebsocketDuplexConnection( 141 | socket, 142 | frame, 143 | multiplexerDemultiplexerFactory 144 | ); 145 | if (connection.done) { 146 | return; 147 | } 148 | try { 149 | socket.pause(); 150 | await connectionAcceptor(frame, connection); 151 | socket.resume(); 152 | } catch (error) { 153 | connection.close(error); 154 | } 155 | }); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /packages/rsocket-adapter-rxjs/src/ObserverToBufferingRSocketSubscriber.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | "use strict"; 17 | import { 18 | Cancellable, 19 | OnExtensionSubscriber, 20 | OnNextSubscriber, 21 | OnTerminalSubscriber, 22 | Requestable, 23 | } from "rsocket-core"; 24 | import { Codec } from "rsocket-messaging"; 25 | import { Observer, Subscription } from "rxjs"; 26 | import { applyMixins } from "./Utils"; 27 | 28 | interface ObserverToBufferingRSocketSubscriber 29 | extends Subscription, 30 | Array, 31 | Observer, 32 | Cancellable, 33 | Requestable, 34 | OnExtensionSubscriber {} 35 | class ObserverToBufferingRSocketSubscriber 36 | extends Subscription 37 | implements Observer, Cancellable, Requestable, OnExtensionSubscriber 38 | { 39 | protected requested: number; 40 | protected subscriber: OnTerminalSubscriber & 41 | OnNextSubscriber & 42 | OnExtensionSubscriber; 43 | protected inputCodec: Codec; 44 | protected wip: number; 45 | private e: Error; 46 | private done: boolean; 47 | 48 | constructor( 49 | requested: number, 50 | subscriber: OnTerminalSubscriber & OnNextSubscriber & OnExtensionSubscriber, 51 | inputCodec: Codec 52 | ) { 53 | super(); 54 | this.init(requested, subscriber, inputCodec); 55 | } 56 | 57 | protected init( 58 | requested: number, 59 | subscriber: OnTerminalSubscriber & OnNextSubscriber & OnExtensionSubscriber, 60 | inputCodec: Codec 61 | ) { 62 | this.requested = requested; 63 | this.subscriber = subscriber; 64 | this.inputCodec = inputCodec; 65 | this.wip = 0; 66 | } 67 | 68 | request(n: number) { 69 | const requested = this.requested; 70 | this.requested = requested + n; 71 | 72 | if (this.wip == 0 && requested > 0) { 73 | return; 74 | } 75 | 76 | this.drain(); 77 | } 78 | 79 | cancel(): void { 80 | if (this.closed || this.done) { 81 | return; 82 | } 83 | 84 | this.unsubscribe(); 85 | 86 | this.drain(); 87 | } 88 | 89 | onExtension( 90 | extendedType: number, 91 | content: Buffer, 92 | canBeIgnored: boolean 93 | ): void {} 94 | 95 | next(value: T) { 96 | this.push(value); 97 | 98 | this.drain(); 99 | } 100 | 101 | error(err: any) { 102 | if (this.closed || this.done) { 103 | return; 104 | } 105 | 106 | this.e = err; 107 | this.done = true; 108 | 109 | this.drain(); 110 | } 111 | 112 | complete() { 113 | if (this.done || this.closed) { 114 | return; 115 | } 116 | 117 | this.done = true; 118 | 119 | this.drain(); 120 | } 121 | 122 | private drain() { 123 | let m = this.wip; 124 | this.wip = m + 1; 125 | if (m) { 126 | return; 127 | } 128 | 129 | m = 1; 130 | 131 | for (;;) { 132 | let requested = this.requested; 133 | let delivered = 0; 134 | while (delivered < requested) { 135 | const next = this.shift(); 136 | 137 | if (next == undefined) { 138 | if (this.done) { 139 | if (this.e) { 140 | this.subscriber.onError(this.e); 141 | } else { 142 | this.subscriber.onComplete(); 143 | } 144 | return; 145 | } 146 | 147 | if (this.closed) { 148 | return; 149 | } 150 | 151 | break; 152 | } 153 | 154 | const isTerminated = this.length == 0 && this.done; 155 | this.subscriber.onNext( 156 | { 157 | data: this.inputCodec.encode(next), 158 | }, 159 | isTerminated 160 | ); 161 | 162 | if (isTerminated) { 163 | return; 164 | } 165 | 166 | delivered++; 167 | } 168 | 169 | this.requested -= delivered; 170 | if (m === this.wip) { 171 | this.wip = 0; 172 | return; 173 | } 174 | 175 | m = this.wip; 176 | } 177 | } 178 | } 179 | 180 | applyMixins(ObserverToBufferingRSocketSubscriber, [Array]); 181 | 182 | export default ObserverToBufferingRSocketSubscriber; 183 | -------------------------------------------------------------------------------- /packages/rsocket-examples/src/webpack/simple/server/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | rsocket-adapter-rxjs@^1.0.0-alpha.4: 6 | version "1.0.0-alpha.4" 7 | resolved "https://registry.yarnpkg.com/rsocket-adapter-rxjs/-/rsocket-adapter-rxjs-1.0.0-alpha.4.tgz#f03cc7081aa3275fab595aabf675a2726d0749a1" 8 | integrity sha512-rfrsui1/M1pBG8A47wsgpBwHU8xdy/xgdGFIC3ICQuBLBhJeENDt+dcZXu8/MaEyWQH3GXvIGBeargd0FZTF8A== 9 | dependencies: 10 | rsocket-core "^1.0.0-alpha.3" 11 | rsocket-messaging "^1.0.0-alpha.3" 12 | rxjs "^7.4.0" 13 | 14 | rsocket-composite-metadata@^1.0.0-alpha.3: 15 | version "1.0.0-alpha.3" 16 | resolved "https://registry.yarnpkg.com/rsocket-composite-metadata/-/rsocket-composite-metadata-1.0.0-alpha.3.tgz#f003d6d052da65f7013d840f781861365eed6efc" 17 | integrity sha512-o+msmc1pXnejLw4TDgMllynS9/L3TGJM25kqedTxqU6wVF9jnTA95w7T4scGwwX6qiAryT90HQGUaT+An44X3w== 18 | dependencies: 19 | rsocket-core "^1.0.0-alpha.3" 20 | 21 | rsocket-core@^1.0.0-alpha-rxjs-adapter-optional-metadata.0, rsocket-core@^1.0.0-alpha.3: 22 | version "1.0.0-alpha-rxjs-adapter-optional-metadata.0" 23 | resolved "https://registry.yarnpkg.com/rsocket-core/-/rsocket-core-1.0.0-alpha-rxjs-adapter-optional-metadata.0.tgz#2889c165910893f7f5a2b56c7500d811e101b1fa" 24 | integrity sha512-FXieO2E8d4UC7mW7K01IRMtgwZ1BZk7l4ZEvgnD8xEKdt//9ks5IfiDZPvi1w9P4YFp/IrI7lL3dqhkOF0Ez/g== 25 | 26 | rsocket-messaging@^1.0.0-alpha.3: 27 | version "1.0.0-alpha.3" 28 | resolved "https://registry.yarnpkg.com/rsocket-messaging/-/rsocket-messaging-1.0.0-alpha.3.tgz#58c8255a2d18fc5c701251707700d96aace92625" 29 | integrity sha512-fr/BDCLI4DMyzVI5hHsABVpKlKxGX8rbfdgcXgY6VKuBlDIX0oqGApqLdSq0YfvNpgjlpuOXYj4vlhLku959Yw== 30 | dependencies: 31 | rsocket-composite-metadata "^1.0.0-alpha.3" 32 | rsocket-core "^1.0.0-alpha.3" 33 | 34 | rsocket-tcp-client@^1.0.0-alpha.3: 35 | version "1.0.0-alpha-rxjs-adapter-optional-metadata.0" 36 | resolved "https://registry.yarnpkg.com/rsocket-tcp-client/-/rsocket-tcp-client-1.0.0-alpha-rxjs-adapter-optional-metadata.0.tgz#16cbff2fa0d7621a534cbb41e580a539f1cbef3d" 37 | integrity sha512-yMJx3oTuY0MpbiUcb0aE/3RCdcqtjQQ6SmIOXDpeltd9xHeYfh3PmR3cD2N+lnlAcfF1HahyMTKRkJ5k0HxG7Q== 38 | dependencies: 39 | rsocket-core "^1.0.0-alpha-rxjs-adapter-optional-metadata.0" 40 | 41 | rsocket-tcp-server@^1.0.0-alpha.3: 42 | version "1.0.0-alpha-rxjs-adapter-optional-metadata.0" 43 | resolved "https://registry.yarnpkg.com/rsocket-tcp-server/-/rsocket-tcp-server-1.0.0-alpha-rxjs-adapter-optional-metadata.0.tgz#040add90cf4efe94660ccd310797c7d516f34e79" 44 | integrity sha512-8R/P80mVZxf4IByWfauLUJ74KHiZInrNw9E69yuMzefaiV7O0Tv07jeYpCkbbF+LTMNk0Z1vQ/Ll7yfAOhXgFg== 45 | dependencies: 46 | rsocket-core "^1.0.0-alpha-rxjs-adapter-optional-metadata.0" 47 | 48 | rsocket-websocket-client@^1.0.0-alpha.3: 49 | version "1.0.0-alpha-rxjs-adapter-optional-metadata.0" 50 | resolved "https://registry.yarnpkg.com/rsocket-websocket-client/-/rsocket-websocket-client-1.0.0-alpha-rxjs-adapter-optional-metadata.0.tgz#e7d8ee55aa30be55b956150fcaf30f96b1ff1d9e" 51 | integrity sha512-CFnXY/DdP/tyDQbvgiGWk+v/A7t1aj6gqKmka8A0Qesb+NBy+1ck/Mwq68JNNhvWlmESGqPmErmPDJ9d3XeMOA== 52 | dependencies: 53 | rsocket-core "^1.0.0-alpha-rxjs-adapter-optional-metadata.0" 54 | 55 | rsocket-websocket-server@^1.0.0-alpha.3: 56 | version "1.0.0-alpha-rxjs-adapter-optional-metadata.0" 57 | resolved "https://registry.yarnpkg.com/rsocket-websocket-server/-/rsocket-websocket-server-1.0.0-alpha-rxjs-adapter-optional-metadata.0.tgz#599db4d55f5f6580bc3550720c97cd8332108214" 58 | integrity sha512-olNVyliABOMlHvcfgtDYtFwuCq79EY6XVrjhxuE57ZhA1l7wHJXAY2eSeDj1IWGnromgO7Bo9Yn06VaN+bFa6Q== 59 | dependencies: 60 | rsocket-core "^1.0.0-alpha-rxjs-adapter-optional-metadata.0" 61 | ws "~8.2.3" 62 | 63 | rxjs@^7.4.0: 64 | version "7.8.2" 65 | resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.2.tgz#955bc473ed8af11a002a2be52071bf475638607b" 66 | integrity sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA== 67 | dependencies: 68 | tslib "^2.1.0" 69 | 70 | tslib@^2.1.0: 71 | version "2.8.1" 72 | resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" 73 | integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== 74 | 75 | ws@^8.18: 76 | version "8.18.3" 77 | resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.3.tgz#b56b88abffde62791c639170400c93dcb0c95472" 78 | integrity sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg== 79 | 80 | ws@~8.2.3: 81 | version "8.2.3" 82 | resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba" 83 | integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA== 84 | -------------------------------------------------------------------------------- /packages/rsocket-core/__tests__/ClientServerMultiplexerDemultiplexer.spec.ts: -------------------------------------------------------------------------------- 1 | import { mock } from "jest-mock-extended"; 2 | import { 3 | Closeable, 4 | ConnectionFrameHandler, 5 | Flags, 6 | FrameHandler, 7 | FrameTypes, 8 | Outbound, 9 | SetupFrame, 10 | StreamFrameHandler, 11 | } from "../src"; 12 | import { 13 | ClientServerInputMultiplexerDemultiplexer, 14 | StreamIdGenerator, 15 | } from "../src/ClientServerMultiplexerDemultiplexer"; 16 | 17 | describe("ClientServerMultiplexerDemultiplexer", function () { 18 | describe("handle()", () => { 19 | it("throws if called twice", async () => { 20 | // arrange 21 | const frameHandlerStub = mock(); 22 | const outbound = mock(); 23 | const multiplexerDemultiplexer = 24 | new ClientServerInputMultiplexerDemultiplexer( 25 | StreamIdGenerator.create(-1), 26 | outbound, 27 | outbound 28 | ); 29 | // assert 30 | expect( 31 | multiplexerDemultiplexer.connectionInbound.bind( 32 | multiplexerDemultiplexer, 33 | frameHandlerStub 34 | ) 35 | ).not.toThrow(); 36 | expect( 37 | multiplexerDemultiplexer.connectionInbound.bind( 38 | multiplexerDemultiplexer, 39 | frameHandlerStub 40 | ) 41 | ).toThrow("Connection frame handler has already been installed"); 42 | expect( 43 | multiplexerDemultiplexer.handleRequestStream.bind( 44 | multiplexerDemultiplexer, 45 | frameHandlerStub 46 | ) 47 | ).not.toThrow(); 48 | expect( 49 | multiplexerDemultiplexer.handleRequestStream.bind( 50 | multiplexerDemultiplexer, 51 | frameHandlerStub 52 | ) 53 | ).toThrow("Stream handler has already been installed"); 54 | }); 55 | }); 56 | 57 | describe("when receiving data", () => { 58 | const setupFrame = { 59 | type: FrameTypes.SETUP, 60 | dataMimeType: "application/octet-stream", 61 | metadataMimeType: "application/octet-stream", 62 | keepAlive: 60000, 63 | lifetime: 300000, 64 | metadata: Buffer.from("hello world"), 65 | data: Buffer.from("hello world"), 66 | resumeToken: null, 67 | streamId: 0, 68 | majorVersion: 1, 69 | minorVersion: 0, 70 | flags: Flags.METADATA, 71 | } as SetupFrame; 72 | 73 | describe("when buffer contains a single frame", () => { 74 | it("deserializes received frames and calls the configured handler", () => { 75 | // arrange 76 | const handler = mock(); 77 | const outbound = mock(); 78 | const multiplexerDemultiplexer = 79 | new ClientServerInputMultiplexerDemultiplexer( 80 | StreamIdGenerator.create(-1), 81 | outbound, 82 | outbound 83 | ); 84 | 85 | // act 86 | multiplexerDemultiplexer.connectionInbound(handler); 87 | multiplexerDemultiplexer.handle(setupFrame); 88 | 89 | // assert 90 | expect(handler.handle).toBeCalledTimes(1); 91 | 92 | const [call0] = handler.handle.mock.calls; 93 | const [arg0] = call0; 94 | expect(arg0).toMatchSnapshot(); 95 | }); 96 | }); 97 | 98 | describe("when buffer contains multiple frames", () => { 99 | it("deserializes received frames and calls the configured handler for each frame", () => { 100 | // arrange 101 | const mockHandle = jest.fn(); 102 | const outbound = mock(); 103 | const multiplexerDemultiplexer = 104 | new ClientServerInputMultiplexerDemultiplexer( 105 | StreamIdGenerator.create(-1), 106 | outbound, 107 | outbound 108 | ); 109 | const streamHandler = mock({ 110 | streamId: 1, 111 | handle: mockHandle, 112 | }); 113 | 114 | // act 115 | multiplexerDemultiplexer.connect(streamHandler); 116 | multiplexerDemultiplexer.handle({ 117 | type: FrameTypes.PAYLOAD, 118 | flags: Flags.NEXT, 119 | data: Buffer.from("hello world"), 120 | metadata: undefined, 121 | streamId: 1, 122 | }); 123 | multiplexerDemultiplexer.handle({ 124 | type: FrameTypes.PAYLOAD, 125 | flags: Flags.NEXT, 126 | data: Buffer.from("hello world 2"), 127 | metadata: undefined, 128 | streamId: 1, 129 | }); 130 | 131 | // assert 132 | expect(mockHandle).toBeCalledTimes(2); 133 | 134 | const [call0, call1] = mockHandle.mock.calls; 135 | 136 | expect(call0).toMatchSnapshot(); 137 | expect(call1).toMatchSnapshot(); 138 | }); 139 | }); 140 | }); 141 | }); 142 | --------------------------------------------------------------------------------